文章目录
- 0. 分代收集理论
- 分代假说
- 分代GC定义
- 1. 垃圾回收算法
- 1.1 标记清除(Mark-Sweep)算法
- 优点
- 缺点
- 1.2 标记复制算法
- 优点
- 缺点
- 为什么是8:1:1?
- 1.3 标记整理算法
- 优点
- 缺点
- 2. 是否移动?
- Reference
0. 分代收集理论
分代假说
现在多数JVM GC都遵循分代收集(Generational Collection)理论,其中涉及三个经验性的分代假说:
- 弱分代假说(Weak Generational Hypothesis):绝大多说对象都是朝生夕灭的
- 对象创建先进入新生代(Young/Nursery),进行较为频繁、局限于新生代的Minor GC
- 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象,越难以消亡
- 对象经过几轮gc后进入老年代(Old/Tenured),进行次数相对少、局限于老年代的Major GC
- 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说占比极少
- 跨代引用:比如新生代对象可能被老年代所引用,所以Minor GC时需要在固定的GC Roots外,再额外增加老年代中的Roots来保证可达性分析的正确性(young引用old同理)。
- 以新生代被老年代引用为例,根据跨代引用假说,如果遍历整个老年代的引用关系来增加GC Roots,效率将会非常低,可以通过在新生代增加专门的数据结构(记忆集,Rememered),来标识跨代引用关系。比如,CMS和G1垃圾收集器都会通过写前屏障的方式,将跨代引用记录在卡表(可以看做一种记忆集)中。
分代GC定义
前面有提到Minor GC、Major GC之类的说法,其实就是不同分代中的GC行为,类似的定义包括:
- 部分收集(Partial GC): 不完整收集整个Java堆的GC。又分为:
- 新生代收集(Minor GC/Young GC):目标是新生代的GC
- 老年代收集(Major GC/Old GC):目标是老年代的GC。目前只有CMS收集器会有只针对老年代的GC。
- 混合收集(Mixed GC):目标是整个新生代和部分老年代的GC。目前只有G1收集器会有Mixed GC。
- 整堆收集(Full GC):整个Java堆和方法区的GC。
1. 垃圾回收算法
1.1 标记清除(Mark-Sweep)算法
将垃圾回收分为标记和清除两个阶段:
- 标记阶段:标记出所有活跃对象(或者标记死亡对象)
- 清除阶段:回收未被标记为活跃的对象(或者标记死亡的对象)
优点
实现简单、速度快。后面的收集算法大多在此基础上改进得来的。
缺点
- 执行效率不稳定:标记、清除的执行效率随对象数增加而降低;
- 清除后可能造成内存碎片:如果new了一个大的对象,碎片化的内存没法使用,造成内存浪费。
1.2 标记复制算法
将内存分为两个区域:一个区域用于存储存活对象,一个保留区域
- 标记处所有存活对象,移动到保留区域
- 移动到保留区域的对象进行内存整理(避免碎片化)
- 将原有区域整个清理掉,变成新的保留区域
现代的商用Java虚拟机大多采用改进的标记复制算法来进行新生代GC。
优点
效率很高,不会产生内存碎片
缺点
- 对象存活率较高时,需要很多复制操作,效率降低。比如老年代中,大部分对象存活周期都很长(前面提到过的强分代假说),所以老年代中一般不采用标记-复制算法。
- 保留区域与存活区域1:1(半区复制,Semispace Copying)的话,会有一半内存被浪费。
一些现代的垃圾收集器(ParNew等)中将新生代分为Eden区+2个Survive(Survivor)区(8:1:1,Appel式回收)解决内存浪费:new对象先分配到Eden中,将Eden与非保留区域的survivor1标记后,将存活对象移动到作为保留区域的Survivor2中,将其他区域(Eden与Survivor1)GC,survivor1成为新的保留区域。
为什么是8:1:1?
大家都知道新生代对象的寿命大部分都很短,也就是弱分代假说中的“朝生夕灭”。IBM公司有研究对对象的“朝生夕灭”做了量化,即新生代对象中98%熬不过第一轮GC。因此完全没必要按照1:1分配新生代空间。
HotSpot虚拟机默认的Eden与Survivor比例是8:1,可以保证每次新生代可用内存空间为整个新生代的90%(Eden 80%+非保留区域survivor的10%)。
但可能存在多于10%对象存活的情况。因此,当一次Minor GC后,Survivor空间不足以容纳幸存的对象,就需要老年代作为保底。
1.3 标记整理算法
- 标记阶段:标记存活对象,清除垃圾对象。
- 整理阶段:内存整理,让存活对象向内存空间的一端移动。
优点
- 相比于标记-复制算法,内存使用效率高,吞吐量高;
- 同时也不会有标记-清除算法的内存碎片化问题。
这里吞吐量的定义为:运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
缺点
- 消耗时间比较长,高并发场景可能影响系统性能
大家应该多少都听说过“Stop The World ”,也就是移动存活对象时(特别是老年代每次回收都有大量对象存活,需要大量移动操作),移动操作必须暂停用户应用程序。因此有时候老年代空间不足或内存碎片化过于严重(大对象内存申请不了),导致Full GC,就会面临STW的困境。如果想要减少STW的次数,可以适当增加新生代的比例,即大部分对象生命周期在新生代快速流转,可以适当减少老年代的STW。
标记-清除算法也要停顿用户线程,但是时间相对较短
2. 是否移动?
标记-清除算法跟其他两个算法最本质的区别,在于它没有移动操作,也因此存在内存碎片化的问题。当然,即使不移动,内存碎片化也不是没有解决办法,只能依赖更复杂的内存分配器和内存访问器来解决。但是内存访问是用户程序最频繁的操作之一,如果增加额外负担,会影响吞吐量。
可见:
- 移动对象:内存回收会更加复杂,GC停顿时间较长,但吞吐量会更划算;
- 不移动对象:内存分配时会更加复杂,GC停顿时间更短,但内存分配的额外操作会极大影响吞吐量。
因此,HotSpot虚拟机中,关注吞吐量的Parallel Scavenge收集器给予标记-整理算法,关注延迟的CMS(老年代)基于标记-清除算法(实际上CMS都会用,大多数时间标记-删除,等碎片化道影响大对象分配时,标记-整理一次)。
最新的ZGC和Shenandoah收集器使用读屏障是爱你整理过程和用户线程的并发执行
Reference
《深入理解java虚拟机:JVM高级特性与最佳时间(第3版)》 周志明