JVM 是 java虚拟机,是用来执行java字节码(二进制的形式)的虚拟计算机。jvm是运行在操作系统之上的,与硬件没有任何关系。
JVM跨平台及原理
- 跨平台:由Java编写的程序可以在不同的操作系统上运行:一次编写,多处运行。
- 原理:编译之后的字节码文件和平台无关,需要在不同的操作系统上安装一个对应版本的虚拟机(JVM)。
JVM的分类
- 类加载子系统。
- 运行时数据区 [ 我们核心关注这里 的栈、堆、方法区 ]。
- 执行引擎(一般都是JIT编译器和解释器共存):
(1)JIT编译器(主要影响性能):编译执行; 一般热点数据会进行二次编译,将字节码指令变成机器指令。将机器指令放在方法区缓存。 (2)解释器(负责响应时间,他的响应时间很快):逐行解释字节码。
JVM体系结构
注:方法区和堆是所有线程共享的内存区域;而java栈、本地方法栈和程序计数器是运行是线程私有的内存区域。
- Java堆(Heap), 是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- 方法区(Method Area), 与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 程序计数器(Program Counter Register), 是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
- JVM栈(JVM Stacks), 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 本地方法栈(Native Method Stacks), 与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
对象分配规则:
首先看一下JVM内存结构布局: JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间, 默认情况下年轻代按照8:1:1的比例来分配;
方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。
对象分配规则:
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
GC算法 垃圾回收
对象存活判断
判断对象是否存活一般有两种方式:
- 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
- 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
GC算法
GC最基础的算法有三种:标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。
- 标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
- 复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
- 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
垃圾回收器
- Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
- ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
- Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
- Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
- CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
- G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
更详细的GC算法 垃圾回收可以参考jvm系列(三):GC算法 垃圾收集器。
虚拟机类加载机制
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
在 Java 语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。
类的生命周期
类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如下图;
以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):
- 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候。
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
- 当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。
类的加载过程
- 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象。
- 连接,连接又包含三块内容:验证、准备、初始化。1)验证,文件格式、元数据、字节码、符号引用验证;2)准备,为类的静态变量分配内存,并将其初始化为默认值;3)解析,把类中的符号引用转换为直接引用。
- 初始化,为类的静态变量赋予正确的初始值。
- 使用,new出对象程序中使用。
- 卸载,执行垃圾回收。
总结: Java 虚拟机将字节流转化为 Java 类的过程。这个过程可分为加载、链接以及初始化三大步骤。
- 加载是指查找字节流,并且据此创建类的过程。加载需要借助类加载器,在 Java 虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。
- 链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。链接还分验证、准备和解析三个阶段。其中,解析阶段为非必须的。
- 初始化,则是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。
类加载器
通过一个类的全限定名来获取描述此类的二进制字节流。
双亲委派模型
从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)
- 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库。
- 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器。
除顶层启动类加载器之外,其他都有自己的父类加载器。
工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。
类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
参考: 关于Jvm知识看这一篇就够了。 Jvm系列-Jvm概述(一)。 Java虚拟机(JVM)你只要看这一篇就够了。
|