大纲
这篇文章应该发在ZGC之前的,不过由于一些原因,现在重新补发一下。
学完这篇文章,你将学到一些常见的垃圾回收算法,以及详细解释CMS与G1垃圾回收器的运行机制,话不多说,先上脑图~
什么是垃圾
B b = new B(); --创建了一个B对象
b = null; --堆内B对象则成为了垃圾
每个线程在运行时会创建一个虚拟机栈,每个方法运行时会封装成一个栈帧对象,包含了:
- 局部变量表
- 操作数栈
- 动态链接
- 完成出口
public class GCRoots {
public Object Object = null;//是引用,它不是根
public static Object so = new Object();//静态变量
public final static Object fo = new Object();//常量
public static void main(String[] args){
Object o1 = new Object(); //o1局部变量(方法执行完成时出虚拟机栈,o1可回收)
o1.hashCode();//JNI的指针(native方法中入参和出参,有指针)
}
public static void none(){
GCRoots c = new GCRoots();//c是根
c = null;//把与GCRoots对象的根 切断了
}
}
JVM进行垃圾回收本质:回收堆内存,垃圾会自动回收
垃圾回收、JVM早期、python采用引用计数法,记录对象引用次数,因为循环引用的存在,因此需要做额外处理,因此垃圾回收算法不如java
@Data
public class GCRoots {
private Object instance;
public static void each(){
GCRoots A = new GCRoots();
GCRoots B = new GCRoots();
A.instance = B;
B.instance = A;
A = null;
B = null;
}
}
GC roots有哪些?讲一讲可达性分析算法
GC roots即GC根的集合,包含四种根:
- 静态变量
- 局部变量
- 常量
- JNI(指针)
常见的垃圾回收算法
复制算法
- 主要用于年轻代
- 实现简单、运行高效
- 没有内存碎片
- 内存空间利用率只有一半
标记清除与标记整理算法
标记清除算法:
- 存在内存碎片
- CMS采用:因为并发清理时,不允许对象移动
- 不需要STW
标记整理算法:
- 标记
- 整理
- 清除
- Serial Old/Parallel Old/G1采用
- 必须要STW
对象分配原则/堆内存管理
- JVM将堆内存分为新生代与老年代,新生代又分为Eden,from,to,也可称为S0,S1
- 当对象分配空间时,优先分配到新生代的Eden区
- 发生young gc,eden区存活对象O1进入from区,age为1
- 当发生第二次yong gc(扫描Eden+from区),eden区存活对象O2进入to区,age为1,O1也进入to区,age为2
- 第三次扫描区域为Eden区+to区,对象进入from区,age+1
- 多次gc过后,当对象age=15时,进入old区
新生代回收方式:
S0S1采用了复制回收算法
appel式回收,空间利用率达到90%
依据原则:绝大部分对象朝生夕死
例外:
- 对象过大时,From或者To区放不下,直接进入老年代,
对象进入老年代
大对象直接进入老年代
Serial与ParNew垃圾回收器可以通过参数控制:-XX:PretenureSizeThreshold=4M 参数来控制
长期存活的对象进入老年代
对象在Eden每经历一次Minor GC,分代对象年龄+1,当增加到一定程度(默认15岁,CMS默认6岁),就会晋升到老年代,可以通过参数 -XX:MaxTenuringThreshold 来设置
动态年龄判断
Survivor区有一批对象,年龄1+年龄2+…+年龄n的多个年龄对象总和超过了Survivor区域的50% ,此时就会把年龄n(含)以上的对象都放入老年代。这个规则是希望那些可能长期存活的对象,尽早进入老年代
老年代空间分配担保机制
垃圾回收器
Serial收集器
- -XX:+UseSerialGC -XX:+UseSerialOldGC
- 全程单线程,新生代采用了复制算法,老年代采用了标记-整理算法
Parallel Scavenge收集器
- -XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代)
- JDK1.8默认收集器,即PS组合
- Parallel收集器其实就是Serial收集器的多线程版本,吞吐量优先,STW时间较长
- 新生代采用复制算法,老年代采用标记-整理算法
ParNew收集器
- -XX:+UseParNewGC
- ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用
- 新生代收集器,采用复制算法
CMS收集器
简介
- -XX:+UseConcMarkSweepGC(old)
- 第一款并发收集器,实现垃圾收集线程和用户线程同时工作,响应时间优先
- 老年代收集器,采用标记清除算法
工作流程
工作过程如下:
- 初始标记:标记一下GC Roots能直接关联的对象,时间短,STW
- 并发标记:进行GC Roots跟踪的过程,时间长,并发
- 重新标记:上一步中标记产生变动的那一部分对象,三色标记的增量更新算法,时间短,STW
- 并发清除:清除GC Roots不可达对象,新增对象标记为黑色不作处理,时间最长,并发
- 并发重置
CMS存在的问题:
- CPU敏感:上下文切换耗费资源,核心数少的CPU遇上过多线程会导致吞吐量降低
- 浮动垃圾:在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了
- 内存碎片:标记清除算法导致
- concurrent mode failure:并发标记和并发清理阶段,没有回收完再次触发full gc,会用serialold垃圾收集器替代,造成长时间STW
CMS处理内存碎片问题:
当内存碎片导致分配不了对象,采用Serial Old(标记整理)替代CMS,造成长时间STW
为了避免替换成Serial Old,因此可以定时重启(游戏服务器)
CMS核心参数
- -XX:+UseConcMarkSweepGC:启用cms
- -XX:ConcGCThreads:并发的GC线程数
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一
次 - -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
- -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设
定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整 - -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
- -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
- -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
三色标记
并发标记过程中,对象状态可能会变更,有两种情况
- 多标:已经被标记为非垃圾的对象变为垃圾对象,影响不大,下次gc回收即可
- 漏标:产生的浮动垃圾,靠三色标记+读写屏障解决
三种颜色如下:
黑色:代表所有对象已经被垃圾回收器访问过,且这个对象的所有引用都已经扫描过了
灰色:代表对象已经被垃圾收集器访问过,但这个对象至少有一个引用没有被扫描过
白色:尚未被垃圾收集器访问过。刚开始时,所有对象都为白色,若结束仍未白色代表不可达
写屏障一般有两种实现方式:(参考AOP的概念)
- 增量更新(Incremental Update)
黑色对象一旦插入了只想白色对象的引用之后,它就变成了灰色对象 - 原始快照(Snapshot At The Beginning,SATB)
将删除的引用记录下来,在重新扫描时,将白色对象标记为黑色。待下一轮gc处理
以Java HotSpot为例,其并发标记时漏标的处理方案如下:
- CMS:写屏障+增量更新
- G1,Shenandoah:写屏障+SATB
- ZGC:读屏障
G1回收器
G1介绍
- G1将Java堆划分成了大小相等的独立区域(Region)
- 一般Region大小等于堆大小处于2048,比如堆大小4096M,则Region大小为2M
- G1保留了年轻代和老年代的概念,但是不再物理隔阂了
- 年轻代默认占比5%,运行过程中会不断增加更多Region,最多60%
- Eden与Survivor区比例仍然为8:1:1
- Humongous区专门保存大对象,可以横跨多个Region,Full GC时,一并回收
G1流程
- 初始标记:同CMS
- 并发标记:同CMS
- 最终标记:同CMS
- 筛选回收:根据回收价值和成本进行排序,根据用户期望GC停顿时间制定回收计划
G1垃圾收集分类
- YoungGC
G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC - MixedGC
老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的 Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区 - FullGC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这 个过程是非常耗时的
高吞吐量系统如何优化
类似于kafka几十万并发场景,调整-XX:MaxGCPauseMills小一点可以减少卡顿
如何选择垃圾回收器
4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC
为什么G1用SATB?CMS用增量更新
- SATB相对增量更新效率更高(当然SATB会造成更多的浮动垃圾),G1不怕浮动垃圾
- 增量更新需要进行深度扫描,G1分区太多,扫描时间过长
记忆集与卡表
主要解决老年代引用新生代对象的问题,卡表是记忆集在hotspot中的具体实现
卡表为一个字节数组,每个元素对应着一小块内存区域(起始值),称为“卡页”,写屏障维护
安全点
安全点就是指代码中一些特定的位置,当线程运行到这些位置它的状态是确定的,这样JVM可以安全进行一些操作,比如GC等,所以GC不是什么时候做就立即触发的,是需要等待所有线程运行到安全点后才触发:
- 方法返回之前
- 调用某个方法之后
- 抛出异常的位置
- 循环的末尾
安全区域
Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的,比如线程因为sleep中断。