GC知识点脉络
什么对象算垃圾?
何时回收垃圾?
怎么回收垃圾?
对象存活判断
Reference Count
引用计数算法,有一个地方引用这个对象,计数器加1,引用失效减1,当计数器为0,对象就不再被用
缺点:解决不了循环引用
Root Searching
根可达算法,通过一系列的GC Roots对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的,会被判定是可回收对象。例如对象循环引用,但是他们到GC Roots不可达,所以可回收
可做GC Roots的对象如下:
- JVM stack,虚拟机栈(栈帧中的本地变量表)中引用的对象
- static references in method,方法区中的类静态变量
- runtime constant pool,方法区中常量池引用的对象
- native method stack,本地方法引用的对象
- class类对象
程序启动之后马上需要的对象就是根对象
一句话总结:对象被方法局部变量、类静态变量引用,就是GC Roots,不会被回收
-
新生代 + 老年代 + 永久代(1.7)/ 元数据区(1.8)
-
新生代 : 老年代 1:3 / 1:2
-
新生代
Eden : Survivor : Survivor 8:1:1
-
老年代
老年代空间耗尽触发Major GC/Full GC,新生代老年代同时回收
-
永久代/元数据区
因为永久代(1.7)不会被GC,所以必须指定大小限制,否则可能会OOM
元数据区(1.8)可设可不设,不设最大就是物理内存
字符串常量1.7在永久代,1.8移到堆中
垃圾回收策略
两条线:YGC前和YGC时
Young GC/Minor GC
触发YGC时机
- 往Young区分配对象时空间不够
使用Copying算法
YGC流程
将Eden区和S0区的对象进行一次Root Searching,找出活跃对象,复制到S1区,并将Eden区和S0区中的不可达对象清空,交换S0与S1指针
若S1不足以容纳Eden和另一个S0中的存活对象,会使用分配担保机制将多余的对象将被移到老年代,称为过早提升(Premature Promotion) ,导致老年代中短期存活对象的增长,可能会引发严重的性能问题
Minor GC之后存活对象一般不超过10%,所以Eden和S0S1的比是8:1:1
JVM会给每个对象定义一个对象年龄(Age)计数器,记在对象头里,对象在Survivor区中每熬过一次Minor GC,年龄就加1。待到年龄到达一定岁数(默认是15岁,因为对象头只有4位用来保存对象当前年龄,可通过 -XX:MaxTenuringThreshold参数设置回收年龄),虚拟机就会将对象移动到老年代。
为什么用4位使最大值为15岁,权衡的依据是什么?
Major GC和Full GC不一样吧?
Full GC/Old GC/Major GC
FGC是对Young区和Old一起GC,OGC只对Old区进行GC
触发FGC时机
- YGC前,Old区可用空间 < Young区全部对象大小,且分配担保没开启
- YGC前,Old区可用空间 < 历次YGC后进入Old区对象的平均大小
- YGC时,分配担保失败
- CMS触发FGC时机
使用Mark-Compact算法
每次Minor GC之前,JVM会检查Old区可用空间,是否大于Young区所有对象总大小,避免出现Minor GC之后所有对象都没被回收的极端情况
若发现Minor GC之后要进入Old区的对象太多,就提前触发Full GC
若Old可用空间大于之前每次Minor GC后进入Old对象的平均大小,尝试进行Minor GC
若剩余存活对象大于Survivor区,小于Old区可用空间,就直接进入Old区,这就是分配担保机制
若剩余存活对象大于Old区可用空间,就触发Full GC,回收Old区和Young区
若Full GC之后剩余存活对象还是大于Old区可用空间,就OOM了
减少FGC的过程就是让Survivor区放得下存活对象,不能触发动态年龄和分配担保进入Old区
如何做到不FGC?
JVM优化总结起来两句话:
- 在YGC前和YGC时尽量避免让太多对象进入Old区,尽量让对象在Young区分配和回收,避免FGC,或者降低FGC次数
- 给JVM足够的内存,避免频繁YGC
结合系统运行时内存占用情况,GC后对象存活情况,根据GC策略流程,合理分配Young区(Eden、S0、S1),Old区大小,合理设置一些参数
对象分配流程
(图2)
new对象先判断能否栈上分配,能分配以后就不会被GC,对象直接出栈即释放内存
在什么栈上分配对象?
不能分配则判断是否大于虚拟机参数指定大小,超过了直接分配Old区
不是大对象则判断是否能进TLAB,不能就进Eden区(TLAB也在Eden区)
若Eden不够空间分配内存时,Eden进行一次Minor GC,将对象放进Eden
之后对象会被一次或多次Minor GC,会被移动到S0/S1/Old区,直到被GC掉
什么对象会往栈上放?
线程私有小对象
无逃逸(只在特定代码块中有效,比如new出来没人引用他)
标量替换:可用基本类型代替的对象 (-XX:-/+EliminateAllocations)
什么对象会往TLAB分配?
线程本地分配TLAB(Thread Local Allocation Buffer)(-XX:-/+UseTLAB)
占用1%Eden,每个线程独有
多线程时不用竞争Eden就可申请空间,提高效率
小对象
什么对象会直接进入Old区?
-
躲过15次GC
-
大对象
设置参数 -XX:PretenureSizeThreshold,大于等于这个值的对象直接进入Old区
避免在Young区中来回复制,耗费时间
-
动态年龄判断进入
YGC时,若发现Survivor中年龄1+年龄2+....+年龄n的所有对象大小超过Survivor区的50%,就会把年龄n以上的对象放入老年代
-
分配担保机制进入
GC算法
Mark-Sweep(标记清除)
先标记出所有需要回收的对象,标记完后再统一回收掉
优点:算法简单,适用于存活对象较多的情况,清理过程效率较高
缺点:扫描两遍(第一遍找有用的对象,第二找没用的清除掉)扫描,算法本身效率偏低,会产生大量不连续内存碎片,会导致分配大对象是无法找到足够的连续内存而提前触发另一次GC
Copying(拷贝)
将可用内存五五开分两块,每次只使用其中的一块。当这一块内存用完,就把存活的对象复制到另一块上面,然后再把已经使用过的内存一次性清理掉。
优点:分配对象内存只需移动堆顶指针,按顺序分配内存,只扫描一次,不产生内存碎片,效率高,适用于存活对象较少的Eden区
缺点:空间代价高,需调整对象引用
Mark-Compact(标记整理)
标记过程和Mark-Sweep一样,但后续步骤是把存活对象都向一端移动,然后直接清理掉端边界以外的内存
优点:不会产生碎片,方便对象分配
缺点:扫描两次,需调整对象引用,效率低
垃圾回收器
垃圾回收器组合
Serial + Serial Old
Parallel Scavenge + Paraller Old(PS+PO,JVM默认组合)
ParNew + CMS(吞吐量比G1高)
G1(响应时间比PA+CMS好)
红线连的都能组合
左边6个逻辑上和物理上都分年轻代老年代,右边几个只逻辑上分年轻代老年代
Serial追随JDK诞生,为了提高效率,诞生了PS,为了配合CMS,诞生了PN,CMS在1.4后期引入,是里程碑式的GC,开启了并发回收的过程,但是毛病较多,因此目前没有任何JDK版本默认CMS
Serial
stop-the-world(STW)
单线程,年轻代串行回收,所有线程在safe point停止(比如还没unlock就等解锁之后再停止)
单机CPU效率最高,现代计算机内存空间大,故此法会导致停顿时间长,极少用
Serial Old
同Serial,用Mark-Sweep/Mark-Compact算法,在老年代清理
Parallel Scavenge
多线程年轻代并行回收
Parallel Old
多线程老年代并行回收,使用Mark-Compact
ParNew(Parallel New)
年轻代并行回收
Parallel Scavenge的变种,对Parallel Scavenge进行增强,以便更好地和CMS配合使用
JVM参数:-XX:+UseParNewGC
GC线程数默认与CPU核心数相同,一个线程一个核心
-XX:ParallelGCThreads参数指定GC线程数(一般不修改)
CMS(Concurrent Mark Sweep)
老年代垃圾回收器
随着服务器内存变大,Serial和Parallel清理的耗时变得无法忍受
故诞生了CMS,这是里程碑式的GC,开启了并发回收的过程,即工作线程和垃圾回收线程同时进行
Parallel是并行回收,多个线程同时回收
CMS是 回收的同时还可以产生新垃圾
CMS清理过程
-
初始标记(STW)
找到并标记GC Roots,对象不多,用时短
-
并发标记
80%的时间都用在这里,和工作线程同时进行,最耗时
-
重新标记(STW)
重新标记并发标记时新产生的少量垃圾、以及被并发标记过但现在又不是垃圾的对象
-
并发清理
清理垃圾, 并发清理产生的新垃圾称为浮动垃圾,等下一次CMS清理
CMS的问题
-
占用CPU
并发标记和清理时,与工作线程同时进行,但耗时较高
CMS默认启动GC线程数是:(CPU核心数 + 3) / 4,即4核CPU就会占用1个核进行GC
导致占用了一部分工作线程的运算能力,降低总吞吐量
-
浮动垃圾 & Concurrent Mode Failure
由于并发清理时工作线程是运行的,所以Old区会预留一部分空间来分配新对象,不能像其他收集器那样等到老年代几乎完全被填满了再进行收集
可通过调整 -XX:CMSInitiatingOccupancyFraction 参数(默认92%,后来改68%
这里要确认哪个版本开始68%
)降低触发FGC的阈值(老年代已使用空间比例),要注意调太低也会浪费内存这又导致两个问题:
- 并发清理期间可能会产生从Young区过来的对象,没人引用之后就会成为浮动垃圾,只能等下次CMS清理
- 若在并发清理期间,Old区预留空间不够分配给从Young区过来的对象,会发生Concurrent Mode Failure,此时会使用Serial Old代替CMS,强行STW进行GC,极其耗时,可能导致机器卡死
-
内存碎片
CMS使用标记清理算法,会产生大量内存碎片,可能会导致Young过来的对象找不到连续的内存空间而频繁触发FGC
所以CMS会在指定次数的FGC结束之后,STW进行一次碎片整理(有参数可配置,但JDK9之后废弃了)
CMS触发FGC时机
- 内存碎片过多,导致Young过来的对象找不到连续的内存空间
- 发生Concurrent Mode Failure
G1(Garbage First)
- 将内存分为多个region,每个大小=堆内存/2048
- Young区和Old区由一个个region组合而成,总大小根据对象分配和回收情况动态变化,没有传统的那种固定空间
- Young区大小会变,创建对象时会不断增加region,直到最大比例60%
- region在对象分配时会属于Young区、Old区和H区,对象被回收后就成为空闲region,直到下次被分配对象
- 还是有Eden区、S0S1区、Old区的概念,对象分配流程和垃圾回收策略跟传统的基本类似,-XX:SurvivorRatio=8还能用
- Young区达到最大比例(默认60%)就会触发YGC
- G1多了一个H区,大对象不直接进入Old区,会进入H区
- 垃圾回收过程使用复制算法
- -XX:MaxGCPauseMills设置期望GC停顿时间
- G1是动态灵活的,会根据预设GC停顿时间,给Young区分配region,到一定程度触发GC,回收时把停顿时间控制在预设范围内,避免一次性回收过多region导致停顿时间过长,或者追求短停顿时间导致频繁GC
- mixed gc:Old区在堆中占比超过45%触发
- 避免触发mixed gc:核心是调整参数-XX:MaxGCPauseMills,与之前的回收器一样,尽量避免对象过快进入Old区,避免频繁触发mixed gc
- 适合大内存机器,解决大内存GC时间过长
内存区域分一个个region,没有物理分代,有逻辑分代,region可以是Old,Survivor,Eden,H区,且不固定,可手动指定region大小
H区即Humongous,大对象(大于region50%)占一个或多个region
新生代空间比例:5% - 60%(上下限可调)
g1会自动根据stw时间自动调整优化新老年代比例,故最好不要手工指定新老年代比例
卡表(是什么?)卡表和三色标记算法貌似不是G1独有,看《深入》
CSet(Colletction Set)
把region分为一个个card,引用的对象所在card被记为dirty
回收垃圾最多、存活对象对少的card,并把放进CSet
回收是到cset找垃圾
RSet(Remember Set)
在每个region记一个hashmap,保存本region里所有对象被别的region里的哪些对象引用说法可能有误
高效回收的关键,空间换时间
由于RSet的存在, 每次给对象赋引用的时候,就得在RSet做额外的记录,称为GC中的写屏障,不等于内存屏障
GC阶段
-
YGC
Eden空间不足
多线程并行执行
不理解
-
MixedGC(约等于CMS的GC)
到达阈值触发
前三步同CMS,最后一步是筛选回收
YGC和MixedGC有时会穿插进行:YGC进行一半,就开始MixedGC的步骤
-
FGC
Old空间不足
调用System.gc()
10以前是串行FGC,之后是并行,所以1.8使用G1尽量不要产生FGC
G1特点
- 并发收集
- 压缩空间不会延长GC暂停时间
- 更易预测的GC暂停时间
- 适用吞吐量要求不高,响应时间需要特别快的场景
三色标记
黑色 -- 自身和成员变量均已标记完成
灰色 -- 自身被标记,成员变量未被标记
白色 -- 未被标记的对象
漏标情况:本来是存活对象,由于没被遍历到,被当成垃圾回收掉了。128-0203
例如:在并发标记过程中,灰不指向白了,但黑指向白
解决漏标
- incremental update -- 增量更新算法:关注黑色对象引用的增加,将其重新标记为灰色,下次重新扫描属性。CMS用
- SATB -- 原始快照算法:关注引用的删除,当灰色对象指向白色的引用消失,要把白色引用推到GC堆栈(GC有个栈用来保存引用),保证白色对象能被GC扫描到 G1使用SATB,因为第一种,黑变灰了,还要重新扫描一遍灰对象,效率低。SATB直接扫描栈里放的引用即可
G1产生FGC怎么办
- 扩内存
- 升级CPU(回收快,业务产生对象的速度固定,垃圾回收越快,内存空间越大)
- 降低MixedGC触发的阈值,让MixedGC提早发生(默认45%)MixedGC可以看做CMS MixedGC:G1的正常回收过程,混合回收,不分区,哪个region满了就收哪个,阈值就是对象占堆空间的值,参数:XX:InitiatingHeapOccupacyPercent,到阈值之前是YGC
垃圾收集器跟内存大小的关系
- Serial:几十兆
- PS:上百兆-几个G
- CMS:20G
- G1:上百G
- ZGC:4T
没有完美的GC,只能在特定的情况下选择合适的解决方案