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