【Java】垃圾回收学习笔记(二):分代假说与垃圾回收算法

0. 分代收集理论

分代假说

现在多数JVM GC都遵循分代收集(Generational Collection)理论,其中涉及三个经验性的分代假说:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多说对象都是朝生夕灭
    1. 对象创建先进入新生代(Young/Nursery),进行较为频繁、局限于新生代的Minor GC
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象,越难以消亡
    1. 对象经过几轮gc后进入老年代(Old/Tenured),进行次数相对少、局限于老年代的Major GC
  3. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说占比极少
    1. 跨代引用:比如新生代对象可能被老年代所引用,所以Minor GC时需要在固定的GC Roots外,再额外增加老年代中的Roots来保证可达性分析的正确性(young引用old同理)。
    2. 以新生代被老年代引用为例,根据跨代引用假说,如果遍历整个老年代的引用关系来增加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)算法

将垃圾回收分为标记和清除两个阶段:

  1. 标记阶段:标记出所有活跃对象(或者标记死亡对象)
  2. 清除阶段:回收未被标记为活跃的对象(或者标记死亡的对象)

在这里插入图片描述

优点

实现简单、速度快。后面的收集算法大多在此基础上改进得来的。

缺点

  • 执行效率不稳定:标记、清除的执行效率随对象数增加而降低;
  • 清除后可能造成内存碎片:如果new了一个大的对象,碎片化的内存没法使用,造成内存浪费。

1.2 标记复制算法

将内存分为两个区域:一个区域用于存储存活对象,一个保留区域

  1. 标记处所有存活对象,移动到保留区域
  2. 移动到保留区域的对象进行内存整理(避免碎片化)
  3. 将原有区域整个清理掉,变成新的保留区域

在这里插入图片描述

现代的商用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版)》 周志明

  • 8
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值