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

基础概念
  • 没有引用指向的对象,就是垃圾
  • 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中做一些额外的操作,这些操作被称为“写屏障”
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值