JVM底层原理篇六:GC底层算法 十大垃圾回收器 G1 CMS 三色标记 对象分配

110 阅读12分钟

基础概念

  • 没有引用指向的对象,就是垃圾
  • C/C++中,是自行回收垃圾,所以效率很高,但是开发很麻烦
  • Java是由GC来帮我们回收垃圾,可以很大的提高开发效率
  • GC调优就是让回收垃圾的效率变高,减少FGC的触发,尽量让YGC去解决问题

GC定位垃圾的算法

  • reference count:引用计数 ○ 有几个引用指向对象,就在对象上标记对应的数字 ,当标记为0时,就代表这个对象是一个垃圾 ○ 会产生循环引用的问题,A引用B,B引用C,C引用A,这时,ABC的标记都是1,就会形成一团垃圾,无法被回收 ○ Python的垃圾回收就是引用计数

  • Root Searching:根可达算法 ○ 根对象:程序启动之后,马上就会需要到的那些对象,就是根对象 ○ 官方对根对象的定义:JVM栈, 本地方法栈,常量池,方法区静态引用的Class,JNI指向的对象(C/C++对象)

GC清理垃圾的算法

  • Mark-Sweep:标记清除算法 ○ 对垃圾对象做好标记后,直接清除 ○ 需要扫描两遍,原有对象不需要移动,但是容易产生碎片 ○ 在存活对象多的情况下,效率比较高,适用于Old区

  • Copying:拷贝 ○ 内存一分为二,把一块区域中有用的对象,拷贝到第二块区域,然后把第一块区域全部清除 ○ 需要扫描一遍,原有对象需要移动,不会产生碎片,但是会浪费空间 ○ 在对象存活少的情况下,效率比较高,适用于Eden区 ○ 补充:原有对象移动,需要调整对象的引用

  • Mark-Compact:标记压缩 ○ 把标记好用得到的对象,全部往一个地方移动,去填补空位和垃圾对象的位置 ○ 需要扫描两遍,并且原有对象需要移动,效率低,但是不会产生碎片化,也不会浪费空间 ○ 存活对象多,并且碎片化严重的时候用,适用于Old区

堆内逻辑分区

  • 分代: ○ 是指把堆中的内存,分为新生代和老年代 ○ 默认是1:2的比例,因为特性不同,所以使用的算法也不同 ○ 目前的垃圾回收器,除了Epsilon ZGC Shenandoah之外,都使用了分代模型 ○ G1使逻辑分代,物理不分代,其他的是逻辑分代+物理分代

  • 新生代(New/Young) ○ 适用Copying算法 ○ 新生代快满的时候,会触发YGC来回收(Young GC 或者 Minor GC) ○ 新生代也就是对象出生的地方,如果对象存活时间不长,会直接在新生代被YGC回收 ○ 其中分为一个Eden区和两个Suvivor(幸存者)区,默认的比例是8:1:1

  • 老生代(Old) ○ 适用于Mark-Sweep或者Mark-Compact算法 ○ 老生代快满的时候,会触发FGC来回收(Full GC 或者 Major GC),也可以调用System.gc()触发

对象分配

  • 栈上分配 ○ 线程私有小对象 ○ 无逃逸,这个对象只会存在当前方法中,没有外部变量的引用 ○ 支持标量替换,指对象中的属性是不可分解的量,比如基本类型,就可以用标量在栈中替换对象 ○ 栈可以直接弹出,不需要回收,所以效率很高 ○ 无需调整

  • 线程本地分配TLAB (Thread Local Allocation Buffer) ○ 每个线程默认能在Eden区申请1%的位置,对象可以在这块区域中分配 ○ 多线程不用竞争Eden区,可以直接申请,对象最后会留在Eden区中 ○ 无需调整

  • Eden分配 ○ 确定不是大对象,并且TLAB无法分配时,就会直接来到Eden区进行分配

  • 老年代 ○ 分配占用很大的对象,由FGC来回收

  • 对象进入老年代的过程 ○ YGC触发后,Eden区存活的对象会进入S1幸存者区,年龄+1,之后的触发,存活对象就会在S1和S2中来回移动递增年龄 ○ 到年龄后就会进入Old区,CMS年龄是6,其他的都是15

  • 动态年龄: ○ YGC完成后会计算年龄,年龄从小到大累加,累加和超过50%的时候,YGC就会把这个年龄定为下一次晋升Old区的年龄

  • 分配担保: ○ YGC触发期间,幸存者区空间不够了,一些对象会通过分配担保直接进入老年代

  • 对象的分配过程及生命周期 ○ 产生对象时,会先尝试往栈上分配,分配不下会先判断这个对象是不是很大,很大的话会直接进入Old区,否则就进入TLAB(线程本地分配) ○ Eden区会给每个线程1%的位置,TLAB会在这个位置上尝试分配,如果分配不下,就直接在eden区中分配 ○ YGC回收后,对象依旧存活,那么就会在幸存者区S1和S2中来回移动,增加年龄,CMS年龄到6进入Old区,其他GC年龄到15进入Old区 ○ Old区快满时,会调用FGC回收 在这里插入图片描述

  • 对象在内存中的状态 ○ 可达状态:可以从根对象直接导航找到的对象 ○ 可恢复状态:没有被引用指向,调用完全部对象finalize方法后,又恢复了引用指向的对象 ○ 不可达状态:所有的关联都被切断,调用完全部finalize方法依旧没有恢复引用指向的对象 ○ 补充:finalize是GC在回收对象之前调用的方法

card table 卡表

  • 主要用于分代模型中帮助我们提升垃圾回收的效率
  • 使用算法标记垃圾的时候,Young区中的引用会指向Old区中的对象,这样的话,触发一次YGC就需要遍历一次Old区,会毫无效率可言
  • 所以JVM就设计一个cardtable来解决这个问题
  • JVM把堆中的内存分为了一个一个的card,如果Old区中的某个对象没Young区中的对象引用,则会在一个位表上,把这个对象所在的card标记为Dirty
  • 下次扫描的时候,就只需要被标记为Dirty的card即可,而这个位表就是card table
  • Collection Set ○ 简称CSET,就是记录了card table中标记了需要回收的card,GC来回收的时候,直接去CSet里面找,可以节约大量的时间

垃圾回收器

  • 基础概念 ○ 垃圾处理器指的是触发YGS或FGC时,用什么处理器来完成垃圾清理的工作 ○ STW:Stop the world的缩写,暂停世界,也就是当所有线程在安全点的时候,全部暂停,GC来回收垃圾 ○ safe point:安全点,意思是必须等一个安全的时候暂停线程,不能让数据错乱 ○ 并发垃圾回收器:也就是指垃圾回收线程和工作线程同时运行的GC ○ 并发垃圾回收器的存在就是因为无法忍受STW,但是目前没有不会产生STW的垃圾回收器

  • Serial + SerialOld ○ 单线程使用STW回收,最开始的垃圾处理器 ○ Old区使用标记压缩算法 ○ 适用于几十兆的内存

  • ParallelScavenge + ParallelOld ○ 多线程的STW回收 ○ 简称PS + PO,如果没有做调优,1.8默认就是使用这个处理器 ○ Old区使用标记压缩算法 ○ 适用于几百兆左右的内存 ○ 12G内存,碎片话比较严重时,STW时间会达到11秒左右

  • ParNew ○ ParNew中同样使用了多线程的STW回收 ○ ParNew做了一些增强来配合CMS的使用 ○ CMS在某个特定阶段的时候,ParNew可以同时运行

  • CMS(concurrent mark sweep) ○ 并发的垃圾回收器,是一个里程碑式的GC ○ 但是问题很多,并且是CMS自带的问题,无法彻底解决 ○ 所以目前所有的JDK版本,默认的垃圾回收器都没有采用CMS ○ 底层算法为:三色标记+ 增量更新 ○ CMS+ParNew可适用于16-20G左右的内存

  • G1 ○ 底层算法为:三色标记 + SATB ○ 可适用于上百G的内存,目前的主流GC ○ 1.9的默认垃圾处理器就是G1

  • ZGC ○ 底层算法为:颜色指针 + 写屏障 ○ ZGC的STW实测已经达到1-2毫秒 ○ 可适用于4个T的内存,JDK13中可适用16T的内存 ○ 没有采用分代模型

  • Shenandoah ○ 底层算法为:颜色指针 + 读屏障 ○ ZGC的竞争对手 ○ 没有采用分代模型

  • Eplison ○ 只进行内存分配,不进行内存回收的GC ○ 一般用于DEBUG调试使用,JDK11才有 ○ 没有采用分代模型 在这里插入图片描述

GC日志(PS + PO)

在这里插入图片描述 在这里插入图片描述

堆日志(PS + PO)

在这里插入图片描述

CMS

  • CMS 垃圾回收流程 ○ 初始标记:单线程进行,使用STW完成,只标记根对象 ○ 并发标记:标记存活对象,和工作线程同时进行,在之前的GC中,这个阶段会占用80%的时间 ○ 预处理:并发操作,找出并发标记时漏掉的存活对象(也就是引用没来得及从YGC中换过来的对象) ○ 可终止的预处理:会尝试去做下一个阶段的事情,达成某个abort(条件)后停止,最多持续5秒,可以对abort条件进行调整来控制 ○ 重新标记:多线程进行,使用STW完成,找出并发标记阶段同时产生的垃圾 ○ 并发清理:清理被垃圾对象,和工作线程同时运行 ○ 并发重置:清除CMS过程中给对象标记的各种状态

  • CMS产生的问题 ○ 浮动垃圾:并发清理的时候产生的垃圾,无法彻底清除干净,只能下一次触发GC时来清理 ○ 碎片化(严重):当Old区内存碎片化特别严重的时候,浮动垃圾没位置了,CMS就会用SerialOld来清理内存

G1

  • 程序的两大思想分别是:分而治之和分层,而GC使用的就是分而治之的方法来管理的内存

  • 在G1中,把内存分为很多块Region,当G1去回收垃圾的时候,其实就是再回收一个一个的Region,超过Region50%的就会被定义为大对象

  • 每一个Region再被回收之后,都可成为不同的分区,而不是固定的Eden区或者Old区,所以G1只是在逻辑上分代,而物理不分代

  • 因此,G1是可以动态的调整新老分区大小的,不用重启项目

  • G1在对象分配不下的情况下,也会产生FGC,G1的FGC在JDK10之前都是单线程串行的,10之后才是并行的

  • 所以我们调优G1的目的就是尽量不要让FGC发生

  • G1调优方法: ○ 扩大内存,提高CPU性能 ○ 降低MixedGC触发的阈值,让MixedGC提前触发 ○ 参数:XX:InitiatingHeapOccupacyPercent

  • Mixed GC: ○ Mixed GC本质上就是一套完整的CMS ○ 不同的是Mixed GC最后一步回收,是筛选回收,会先去回收筛选出来的,最需要被回收的垃圾 ○ 之后会对Region进行一个整理,所以G1的碎片很少 ○ 回收过程:初始标记STW-->并发标记-->最终标记STW-->筛选回收SWT(并行)

  • RememberedSet: ○ 在G1的每个Region中,都记录了谁指向了我,而这些信息,就是记录在RememberedSet中 ○ 简称RSet,主要的作用是为了方便垃圾回收而设计的集合,也是G1可以高效回收垃圾的关键 ○ 缺点是RSet本身也是会浪费空间的,所以在ZGC中,取消了这个机制,直接把信息记录在了指针里

  • 特性: ○ 并发收集:并发标记,并发回收 ○ 算法:三色标记 + STAB ○ Mixed GC压缩空间时,不会延长GC的STW时间,这个时间能够预测并且控制 ○ 适用于不需要特别高吞吐量但是需要响应特别快的场景

三色标记

  • 三色标记中,使用了三种颜色表示了对象的状态 ○ 白色:未被标记的对象 ○ 灰色:自身被标记,成员变量未被标记 ○ 黑色:自身和成员变量均已被标记
  • 通过颜色的辨别,可以让GC找到需要回收的垃圾,但是这个过程是并发执行的,所以会存在漏标的情况
  • 在并发标记的过程中,有一个黑色对象的引用指向了白色对象,与此同时,其他指向这个白色对象的引用消失了,这种情况下,白色对象就会被漏标
  • 解决办法: ○ 增量更新: 黑色建立新的引用时,把黑色的对象重新标记为灰色,下次重新扫描属性(CMS使用的算法) ○ STAB:当一个灰色对象指向白色对象的引用消失的时候,就把这个引用放去GC的堆栈里面,保证白色引用还可以被GC扫描到(G1使用的算法)

补充知识

  • G1之所以使用SATB,是因为G1里面有很多的Region,如果对象被重新标记为灰色,就得重新扫描,效率会很低
  • 第二个原因是,G1中的每个Region都包含有一个RememberedSet,而RememberedSet里面记录的引用可以很好的配合SATB来使用
  • G1中,由于RSet的存在,每次给对象赋引用的时候,都会在RSet中做一些额外的操作,这些操作被称为“写屏障”