垃圾收集算法
分代收集算法
基于不同的“代”的特点来选择不同的内存清理算法
标记-复制算法
内存分为2块,每次只用其中一块。GC时把非垃圾对象挪动到另一块后清空当前区块
缺点:空间占用量大,所以一般用在年轻代(空间比较少)
标记-清除算法
标记存活对象,标记结束后统一回收所有未标记区域
缺点:
效率问题:需要标记的对象可能有很多
空间问题:标记后产生大量碎片
标记-整理算法
标记存活对象,标记结束后把存活对象向一段移动,之后清理掉存活对象以外的内存区域
垃圾收集器
垃圾收集算法的实现
Serial
-XX:+UseSerialGC(年轻代) -XX:+UseSerialOldGc(老年代)
串行收集:年轻代:标记-复制算法 老年代:标记整理算法
单线程缺点效率太低
Parallel
-XX:+UseParallelGC(年轻代) -XX:+UseParallelOldGc(老年代)
-XX:ParallelGCThreads(设置线程,默认CPU核数)
串行收集:年轻代:标记-复制算法 老年代:标记整理算法
JDK1.8 默认年轻代、老年代收集器
ParNew
-XX:+UseParNewGC(年轻代)
历史问题元Parallel的年轻代收集器无法和CMS(只有老年代版本)配合使用所以提供了优化过后的 ParNew纯年轻代收集器
CMS
-XX:+UseConcMarkSweepGC(老年代)
Parallel收集器在大的内存4G+ 会有问题:STW的时间比长
所以出现优化的CMS收集器(Concurrent Mark Sweep)使用标记-清除算法
- 初始标记 : STW
GC-Root出发找到直接引用对象(不再往下找) - 并发标记 : 不STW 大概占用整个收集过程80%的时间
从直接引用开始 遍历整个对象图标记 - 重新标记 : STW
三色标记增量更新算法做重新标记
GC-ROOt 出发处理白色对象 - 并发清理 : 不STW
到这里之前的类图已经标记完了没有漏的 所以只会出现本来有用现在没用了 本来不做处理
执行未标记区域清理
期间新增所有对象标记为不做处理(三色标记黑色) - 并发重置 : 不STW
重置之前标记数据
梳理CMS的三色标记流程
三色标记简要描述:
没有被标记的都是白色的
所有字段被都标记完了的是黑色的
还有字段没有被标记完的就是灰色的
这里的黑色和灰色不需要考虑的那么复杂
如下伪代码:
//假设遍历是一轮一轮的 只要被别的对象引用就初始为灰色grey
objMarkState = 'grey'
for (filed in objFiledList) {
mark(filed);
}
objMarkState = 'black'
来分析一下标记过程
- 初始标记:GC Root 都是黑色的,所有被CG Root直接引用的对象直接标记为灰色
- 并发标记:一直遍历灰色对象,对象字段直接标记为灰色,自己变成黑色
- 重新标记:对漏标的对象进行重新遍历标记
- 并发清理:所有白色对象都被清理掉
重新标记时漏标的对象是从哪里来的?
就是增量标记时标记的对象
CMS细节
和Parallel收集器的区别
- CMS把本来很长的STW时间分隔成了很多个步骤从而使每次STW时间很短
用户体验更好
CMS收集器有哪些缺点
- 和用户线程争抢资源 浮动垃圾无法清理
- 浮动垃圾:并发标记和并发清理阶段产生的垃圾本次GC无法处理
- 标记清除算法造成大量碎片(可以设置清除后整理-XX:+UseCMSCompactAtFullCollection)
- 并发失败 收集过程中因为用户线程还在继续执行 有可能重新触发GC 此时会直接进入STW 并使用 serial old垃圾收集器来收集清理垃圾 一般需要通过核心参数设置避免这个情况
并发标记不STW会引起主要的2个问题
- 本来不是垃 现在是垃圾了
- 本来是垃圾 然后复活了
使用三色标记法处理(下边详细分析)
对象引用跨代
- 年轻代引用老年代对象
- 老年代引用年轻代对象
在做GC的时候因为已经分代了 那如果做GC的时候GC root在另一代内存中
首先跨代引用现象很少出现
然后jvm在年轻代设置了一块区域 存放 老年代 卡页(固定大小)的映射
映射内容包含:页的起始地址 是否存在引用的标志位
如果存在老年代 引用年轻代 则 使用写屏障 设置映射内容的标志位为 1
young GC的时候就要扫描 老年代的卡页
进入并发清理阶段时新进的对象如何处理
直接标记为黑色 下一轮GC再处理
CMS核心参数
参数的xx越多代表 高版本有可能不支持的概率越大
TODO 详细分析
三色标记算法
并发标记和并发清理流程因为用户线程还在跑
会出现
- 多标记 :本来不是垃圾的 现在是垃圾了 影响不大 下一轮GC再处理
- 少标记 :本来是垃圾的 现在不是垃圾了
对对象进行可达分析的时候 标记对象有不同的颜色(三种)
- 黑色 :对象引用的所有对象都被扫描过了 和 没有引用其他对象的对象 (分析完了)
- 灰色 : 至少还有一个引用对象还没有扫描过 扫描过程中 扫描了一个对象还没有确定是否扫完的状态(分析中)
- 白色 : 还没有扫描到的标记为白色(没分析:默认)
对象状态变更
在并发标记做完后会清理所有白色标记的对象 但是存在如下情况
在并发标记阶段 如果黑色对象 引用了白色对象 则白色对象就不再是垃圾了
CMS使用CPU读写屏障来避免少标记
如图中的D对象的两步操作
Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
简单来说就是:
一个灰色对象对一个白色对象的引用断掉的同时一个黑色对象对这个白色对象引用
2个过程记录,选一个过程中记录下这个白色对象,在重新标记阶段把这些对象当作灰色对象重新处理即可
要破坏这两种情况中的一种需要用到JVM C++写屏障
写屏障
C++给对象复制的时候 前后都有扩展的方法
也就是 写前屏障 写后屏障 JVM代码级别的屏障(类似于AOP)
两种算法
用来保障当时扫描的对象图不会出现漏标
-
增量更新 Incremental Update -CMS使用
写前屏障
黑色对象要引用白色对象则 把黑色对象和引用关系记录下来 之后重新从黑色对象出发重新扫描
等于黑色对象变成灰色对象 -
原始快照 SATB (Snapshot-At-The-Beginning)-G1使用
写后屏障
灰色对象要删除对白色对象引用时 把这个灰色对象和引用关系记录下来 之后从灰色对象出发重新扫描
也就是说不管删除与否都按当时的快照扫描
垃圾收集器:G1
8G以上堆内存推荐使用
JVM内存区域划分为2048个Region
分代改为逻辑上的区分了 Region角色变更 边用边改
默认年轻代 5%(设置默认不能超过60 % E:s0:s1 = 8:1:1)
超过 e区 50% 为大对象 直接放入 H区
G1清理流程
筛选回收: STW 并且可以根据停顿时间分为多次 停顿的时候交给用户线程使用(默认分8次)
Safepoint : 安全点 *
做回收的时候根据用户设置的最大停顿时间来预估回收区域(Collection Set),只处理估算时间内按优先级排序能处理完的内存区域 其他区域下次CG再处理
清理使用 标记-复制算法 把非垃圾对象复制到空白区域,原区域清空并变为空白角色
虽然G1使用复制算法 但是因为对G1来说一个Region是最小的单位 所以内存区域是连续的
安全点
在STW的时候是不能影响原子性操作 需要等他们执行完 如:CAS、执行完程序更新程序计数器操作等 不能被暂停的操作 所以JVM再底层设置了很多安全点 当需要STW的时候 更改标识
所有线程到安全点 检查标志位如果需要暂停 则暂停等待
安全点不仅仅是对操作安全 也是对GC操作安全 不能放在很难到达的地方
安全区域
如果某段代码不会引起 引用关系的变化 则GC认为这个代码区域就是安全区域可以随时开始STW
排序优先级
更短的时间回收更大的区域(存活对象越多越慢)
复制会消耗时间
G1清理分类
-
Young GC
再原有E区放满了 之后计算清理预估时间 如果小于配置最大停顿 则扩展 e区大小
直到接近最大停顿时间发生Young GC
增长也不是无限的 不会超过阈值(默认60%) -
Mixed GC
老年代使用整堆的阈值值就会触发 会回收 young 和 部分old区 H区也会回收 -
Full GC
没有最够的region区域存放 Mixed GC 存活对象的时候触发
STW 清理所有区域
G1主要可配参数
G1 用SATB的推测
因为G1分块比较多 并且相对来说对象比较多
SATB 是对灰色对象扫描的补充 不需要重新扫黑色对象了
G1详细分析:https://zhuanlan.zhihu.com/p/517403816?utm_id=0