深入理解Java虚拟机(三)之垃圾收集
2018-05-27 本文已影响49人
AntDream
深入理解Java虚拟机系列文章
- 深入理解Java虚拟机(一)之内存模型
- 深入理解Java虚拟机(二)之四种引用
- 深入理解Java虚拟机(四)之JVM调优
- 深入理解Java虚拟机(五)之类文件结构
- 深入理解Java虚拟机(六)之类加载机制
垃圾收集算法
标记-清除算法
- 最基础的收集算法,包括“标记”和“清除”2个阶段
- 首先标记出所有需要回收的对象,标记过程见前文的2次标记,标记完以后统一回收所有被标记的对象
- 主要的不足
- 标记和清除2个阶段的效率都不高
- 标记清除之后会产生大量的不连续的内存碎片,内存碎片过多会导致以后为大对象分配内存时,无法找到足够的连续内存而不得不触发再一次的GC
复制算法
- 将内存按容量分为大小相等的2块,每次只使用其中的一块。当这一块内存用完了,就将存活的对象复制到另一块内存中,然后一次性清理掉已使用过的内存空间。
- 优点:每次都是对半个内存区域进行回收,内存分配时也不用考虑内存碎片等复杂情况,实现简单,运行高效
- 不足:将内存缩小为原来的一半,代价较高
- 现在的商业虚拟机一般都采用这种复制算法回收新生代,但不是严格按照1:1这样划分内存。而是分为较大的一块Eden空间和2块较小的Survivor空间。HotSpot虚拟机默认Eden和Survivor的比例为8:1。
- 由于上述Eden和Survivor的划分,导致会出现Survivor空间不够用的情况,这时就需要依赖老年代内存进行分配担保(Handle Promotion)。
标记-整理算法
- 分为“标记”和“整理”2个过程
- “标记”过程和标记-清除算法一样
- 与标记-清除算法不一样的是,在标记完以后,会让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。这样可以避免产生大量的内存碎片的问题
- 一般用于老年代的垃圾收集
分代收集算法
- 根据对象的存活周期的不同将内存分为几块,一般把Java堆分为新生代和老年代
- 新生代用复制算法,老年代一般用标记-清除或是标记-整理算法
算法的实现
可达性分析时如何知道哪些地方存放着对象引用?---OopMap数据结构
- 在类加载完成的时候, HotSpot就把对象内什么位置上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,这些信息都使用一组称为OopMap的数据结构来实现
从哪些位置进入GC?---安全点
- HotSpot不会为每个指令都生成OopMap,这样会浪费很多空间。
- 如上所述,HotSpot会在特定的地方记录引用的信息,这些特定的地方就是安全点,也就是可以进入GC的点
- 安全点不能太少,也不能太多,基本上以“是否具有让程序长时间执行的特征”为标准进行选定。“长时间执行”的最明显特征就是指令序列复用,如方法调用、循环跳转、异常跳转等。所以具有这些功能的指令才会产生SafePoint。
GC发生时如何让所有线程停下来?---抢先式中断和主动式中断
- 抢先式中断:在GC发生时,首先把所有的线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上
-主动式中断:GC需要中断线程的时候,不直接对线程操作,而是设置一个标志,每个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方 - 抢先式中断已经很少使用
没有正在执行的线程如何响应JVM的中断请求?---安全区域(Safe Region)
- 安全区域是指在一段代码中,引用关系不会发生变化,在这个区域中开始GC是安全的,可以看作是扩展了的安全点
- 线程进入安全区域时会标识自己,这样GC时就不用管标识自己进入安全区域的线程
- 线程在离开安全区域时,会检查系统是否已经完成了根节点枚举或是整个GC过程,如果完成就继续执行下去,如果没有就必须等待,直到收到可以安全离开安全区域的信号
垃圾收集器
- 垃圾收集器是内存回收的具体实现
- 新生代垃圾收集器包括:Serial收集器、ParNew收集器、Parallel Scavenge收集器
- 老年代收集器:CMS收集器、Serial Old收集器、Parallel Old收集器
- 以及G1收集器
Serial收集器
- Serial收集器是最基本、发展历史最悠久的收集器
- Serial收集器是一个单线程的收集器
- Serial收集器在进行垃圾收集时,必须暂停所有其他的工作线程,直到收集结束,因此有“Stop The World”的称号
- 虚拟机运行在Client模式下的默认新生代收集器
- 优点:相比于其他单线程的收集器而言,简单高效。没有线程切换的开销,可以获得最高的单线程收集效率
ParNew收集器
- 是Serial收集器的多线程版本,多个线程同时进行垃圾收集
- 除了Serial收集器,目前只有ParNew收集器能与CMS收集起配合工作
- 是许多运行在Server模式下的虚拟机的首选的新生代收集器
- ParNew收集器由于存在线程交互的开销,在单个CPU环境中不会比Serial收集器有更好的效果
Parallel Scavenge收集器(吞吐量优先收集器)
- 吞吐量 = 运行用户代码的时间/(运行用户代码的时间 + 垃圾收集时间)
- GC停顿时间越短能保证良好的响应速度,适合与用户交互的程序;高吞吐量可以提高CPU的利用效率,尽快完成运算任务,适合在后台运算不需要太多交互的任务
- Parallel Scavenge收集器也是多线程的采用复制算法的垃圾收集器
- Parallel Scavenge收集器的不同之处在于,它的目标是达到一个可控制的吞吐量
- Parallel Scavenge收集器可以通过打开UseAdaptiveSizePolicy参数来开启GC的自适应调节策略
Serial Old收集器
- Serial Old收集器是Serial收集器的老年代版本,是一个单线程收集器,采用标记-整理算法
- 主要给Client模式下的虚拟机使用
- 在Server模式下,一般作为JDK1.5以及之前版本中与Parallel Scavenge收集器配合使用;或是作为CMS收集器的备用
Parallel Old收集器
- Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法
- 在注重吞吐量以及CPU资源敏感的场合,与Parallel Scavenge收集器配合使用
CMS收集器
- 以获取最短回收停顿时间为目标的收集器
- 基于标记-清除算法实现
- 分为四个过程:初始标记、并发标记、重新标记、并发清除
- 初始标记、重新标记需要暂停所有线程
- 并发标记和并发清楚耗时比较长,但都是和用户线程同时工作,所以总体上CMS收集器的内存回收工作是和用户线程同时工作的
- 缺点:
- 对CPU资源非常敏感
- 无法处理浮动垃圾,浮动垃圾就是在CMS并发清理垃圾时,用户线程同时运行产生的垃圾
- 由于是基于标记-清除算法实现的,会导致有很多内存碎片产生。虽然可以通过开启UseCMSCompactAtFullCollection参数来在收集器进行FullGC时开启内存碎片整理,但这个碎片整理过程不是并发的,停顿的时间就变长了
G1收集器
- 是一款面向服务端应用的垃圾收集器
- 特点:
- 并行与并发,G1能使用多个CPU来缩短线程暂停的时间,同时通过并发的方式使Java线程不同停下来
- 分代收集,采用不同的方式处理新生的对象和已经存活了一段时间、熬过多次GC的旧对象
- 空间整合,G1整体上看是采用标记-整理算法,从局部(2个Region之间)是基于“复制”算法实现的
- 可预测性的停顿,G1能建立可预测的停顿时间模型
- G1把Java堆划分为多个独立的Region区域,新生代和老年代不再是物理的隔离,它们都是一部分Region的集合
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
- G1收集器中每个Region都有一个对应的Rememberer Set用来记录跨Region的对象引用和跨新生代老年代的引用,在进行内存回收时,在GC根节点范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏
- G1收集器工作过程大致分为:初始标记、并发标记、最终标记、筛选回收
- 筛选回收阶段首先对各个Region的回收价值进行排序,根据期望的GC停顿时间制定回收计划。筛选回收阶段会停顿用户线程。
内存分配策略
对象优先在Eden区分配
- 大部分情况下,对象在新生代的Eden区分配,当Eden区没有足够的空间时,虚拟机将发生一次MinorGC
- 通过-Xms和-Xmx参数设置堆的大小,通过-Xmn参数设置新生代的大小,最后通过-XX:SurvivorRatio设置新生代中Eden区和一个Survivor区的空间比例
大对象直接进入老年代
- 大对象指的是需要大量连续内存空间的对象,比如很长的字符串以及数组。经常出现大对象很容易导致GC
- 虚拟机提供了参数-XX:PretenureSizeThreshold,大于这个值的对象直接在老年代中分配
长期存活的对象将进入老年代
- 虚拟机为每个对象定义了一个对象年龄计数器
- 如果对象在Eden区经过一次Minor GC后仍然存活并能被Survivor容纳的话,将被移动到Survivor空间,并且对象年龄设为1
- 对象在Survivor区每熬过一次Minor GC,年龄就增加1岁
- 当对象的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代
- 对象晋升到老年代的阈值可以通过虚拟机参数-XX:MaxTenuringThreshold来设置
动态对象年龄判定
- 如果在Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半,那年龄大于或等于该年龄的对象就可以直接进入老年代,而不用等到MaxTenuringThreshold中要求的年龄
空间分配担保
- 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。原因是,新生代中的垃圾收集采用的是复制算法,那最坏的情况就是Minor GC之后,新生代中的对象都存活,那Survivor空间中势必容不下这么多对象,就要将对象移到老年代,而老年代的空间如果不够的话就要进行Full GC了
- 如果上面的条件成立,就会进行Minor GC
- 如果条件不成立,那就要判断虚拟机的参数HandlePromotionFailure是否允许担保失败,如果不允许,就要进行一次Full GC
- 如果允许担保失败,就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,就进行一次Minor GC,否则就进行Full GC
- 以上的方案说到底是尽量避免不必要的Full GC
- 需要注意的是JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。