最近参考各种资料,尤其是《深入理解Java虚拟机 JVM高级特性和最佳实践》,大牛之作。把最近学习的Java虚拟机组成和垃圾回收机制总结一下。
你不会的都是新知识,学无止境,每天进步一点点。
一、认识Java虚拟机
在开始学Java之时,必做的一件事就是从Java官网下载并安装Java到我们的电脑之上,然后从HelloWorld开始走上编程的不归路。
上图中下载的Java安装包全称是Java SE Development Kit(单词依次翻译:Java 标准版本 开发 工具包),简称JDK,也就是供程序员使用的Java开发工具包。另外,JDK一般都是和它的2个小弟一起出现的,一个是JRE,一个是JVM,它们的关系如下图所示:
JDK=JRE+Java编译器、开发工具和更多的类库
简单来说JDK支持Java程序的开发。
JRE是Java Runtime Environment的简称
JRE=Java虚拟机+Java基础类库
是Java程序运行所需要的软件环境,简单的来说JRE支持Java程序的运行。图片中也提到了,JRE只支持java字节码的运行,是没办法把Java代码编译成.class文件的。
最内部也是最核心的就是Java虚拟机了,那么问题来了,什么是Java虚拟机?
Java虚拟机实际上是一种规范,是一种抽象机器,依附在真实的操作系统之上,这个规范描述了一个指令集,一组寄存器,一个堆栈,一个“垃圾堆”,和一个方法区。一旦一个Java虚拟机在给定的平台上运行,任何Java程序都能在这个平台上运行。
二、JVM运行时数据区
编写好的java代码交给java虚拟机,java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,称为运行时数据区,如下图所示。
图中绿色标记的区域是每个线程私有的,也就是线程安全的。灰色标记的区域是所有线程共享的,非线程安全。这幅图中我们只需要关注运行时数据区中的5个部分。
2.1 程序计数器
程序计数器的功能:
当前线程所执行的字节码的行号指示器。
.java文件编译成.class文件,最终交给jvm执行,.class文件也是要按逻辑找到行号执行的,程序计数器就是对当前线程应该执行哪一行字节码做指示的。假设线程A暂停,过一段时间后线程A继续执行,这时候线程A应该从哪个地方继续就是靠程序计数器来完成的。
每个线程一个程序计数器,不同线程之间的程序计数器互不影响。
2.2 虚拟机栈
虚拟机栈描述的是java方法执行的内存模型。
一个线程一个栈,一个方法一个栈帧。栈帧可以理解成栈中的一小块,一个栈中有多个栈帧。栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
如下,MethodStackTest类中有三个方法:methodA、methodB和methodC,methodA调用了methodB,methodB调用了methodC,在main方法中执行methodA。
代码及打印结果:
class MethodStackTest{
public static void methodA(){
methodB();
System.out.println("Method A");
}
public static void methodB(){
methodC();
System.out.println("Method B");
}
public static void methodC(){
System.out.println("Method C");
//methodA();取消注释会出现栈内存溢出
}
public static void main(String[] args) {
methodA();
}
}
结果:
Method C
Method B
Method A
这个小例子有助于解释了栈的含义,A调用了B,B调用了C,只有C执行结束,B才会结束,最终B执行完成A才会结束。
另外,如果在C中再调用A的话就一直循环调用,超过虚拟机所允许的栈的深度以后就会抛出StackOverflowError异常,如果不能申请到足够的内存,就会抛出OutOfMemory异常。
2.3 本地方法栈
本地方法栈和虚拟机栈发挥的作用相似,本地方法栈为本地方法服务。
2.4 堆
Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。堆被划分成三个不同的区域:新生代 ( Young )、老年代 ( Old )和永久区。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。Java堆也是垃圾回收的主战场,也被称为GC堆。
堆大小=新生代+老年代+永久区
新生代=Eden+From Survivor+To Survivor
默认:Eden:From:To=8:1:1
另外新生代和老年代的比例可以通过参数设置,并不一定是1:1。
2.5 方法区
方法区也被称为永久区,里面有类信息、常量、静态变量等数据,别名为非堆,为各个线程所共享。
三、垃圾回收机制
3.1 什么是垃圾?
Java中的垃圾是指已经分配内存但不再有任何引用(不完全准确,后面会再说)的对象,垃圾回收(GC)就是自动清理这部分内存。
3.2 两种垃圾判断算法
3.2.1 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
这种算法的有点事实现简单,判定效率也高,但是主流虚拟机并没有采用,主要是对于循环引用的对象无法进行回收。
3.2.3 可达性分析法
主流的JVM回收垃圾主要采用可达性分析法,算法的核心思想是选定一系列GC ROOTS对象作为起始点,从节点开始向下搜索,搜索路径称为引用链。一个对象到GC ROOTS没有任何引用链证明对象不可用。
可作为GC ROOTS的对象:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法中JNI引用的对象
3.3 四种垃圾回收算法
3.3.1 标记-清除法(Marked-Sweep)
标记清除算法是最基础的收集算法,其他收集算法都是基于这种思想。标记清除算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。
它的主要缺点:
- 标记和清除过程效率不高 。
- 标记清除之后会产生大量不连续的内存碎片。
3.3.2 复制算法(Copying)
它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次理掉。这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆订的指针,按顺序分配内存即可,实现简单,运行高效。
主要缺点:内存缩小为原来的一半。
3.3.3 标记-整理法
标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。
主要缺点:在标记-清除的基础上还需进行对象的移动,成本相对较高,好处则是不会产生内存碎片。
3.3.4 分代算法
分代算法就是根据堆内存中新生代和老年代对象的特点,不同的区域采用不同的垃圾回收算法。新生代大多数对象“朝生夕死”,每次都有大量对象被回收,因此采用复制算法,上图堆内存中FROM和TO就是为了复制算法而设计的。老年代对象存活率较高,没有额外空间进行分配担保,使用“标记-清理”或者“标记整理”算法。
3.4 七个垃圾收集器
七种作用于不同分代的垃圾收集器:
垃圾收集器这部分没有写的太详细,这里有一篇已经整理好的:http://www.jianshu.com/p/50d5c88b272d,其内容也是根据《深入理解Java虚拟机 JVM高级特性和最佳实践》整理的。
3.4.1 Serial收集器
Serial收集器是最基本、发展历史最悠久的单线程收集器,最大的特点是Stop The World,垃圾回收时要终止用户进程,等GC结束用户进程方可继续。
JVM参数-XX:+UseSerialGC
3.4.2 ParNew收集器
ParNew收集器是Serial收集器的多线程版本。
3.4.3 Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
3.4.4 Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
3.4.5 Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
3.4.6 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
3.4.7 G1收集器
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
3.4 GC范围
按范围分,GC有Minor GC、Major GC和Full GC。
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,从老年代回收内存被称为Major GC,同时回收新生代和老年代称为Full GC。
四、内存分配策略
4.1 对象优先分配在Eden区
对象优先在新生代Eden中分配,Eden中没有足够空间进行分配时,虚拟机发起一次Minor GC。
4.2 大对象直接进入老年代
大对象是指需要大量连续内存空间的Java对象,典型的是长字符串和数组,这种大对象会被分配到老年代之中。
4.3 老不死的对象进入老年代
新生代中的对象经过不断GC仍然存活,达到一定的“年龄”就会进入老年代,每经历一次Minor GC,年龄+1,达到15时(默认)就会进入老年代。晋升老年代的年龄阈值可以通过-XX:MaxTenuringThreshold参数指定。
4.4 动态对象年龄判定
JVM也不是严格执行4.3中的年龄要求,Survivor空间相同年龄所有对象大小的总和大于等于Suivivor空间的一半,年龄大于或等于该年龄的对象就会直接进入老年代,无需达到XX:MaxTenuringThreshold的要求年龄。
4.5 空间分配担保
Minor GC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果是,GC是确保安全的。JVM继续检查老年代最大的可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则正常进行一次YGC,尽管有风险(因为判断的是平均大小,有可能这次的晋升对象比平均值大很多);
如果小于,或者HandlePromotionFailure设置不允许空间分配担保,这时要进行一次Full GC。
五、小结
JVM这一部分初步整理这么多,欢迎批评指正。