史上最全GC原理
什么是垃圾
定义
- 释放已占用的内存,防止内存泄露
- 清除已经死亡或者长时间未使用的对象内存
语言特性
-
c++手动回收垃圾
- 忘记回收
- 回收多次
-
java 自动回收
如何定位垃圾
引用计数法
- 对象头中分配一片空间用于存储对象引用次数
- 程序执行过程中完成,非STW
- 注意:Recycler 算法可解决循环引用,但在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法
根可达性分析算法
-
GC Root
-
虚拟机栈中引用的对象
public static void testGC(){
StackLocalParameter s = new StackLocalParameter(“localParameter”);
s = null;
} -
方法区中类静态属性引用的变量
-
方法区中常量引用的对象
-
本地方法栈JNI中引用的对象
任何 native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
-
-
通过GC roots对象作为起点开始向下搜索引用的对象,找到的对象都为存活对象即可达,其他没有标记的对象都为垃圾
怎么回收垃圾
有哪些回收方法
-
标记清除 mark sweep
- 1、从GC Root遍历对象图,标记出垃圾对象;2、再次遍历清除
- 产生碎片,内存不连续,效率偏低(两遍扫描)
-
复制copying
- 1、内存分为两块;2、第一块使用完成后将存活的对象复制到第二块;3、清除第一块内存;
- 没有碎片,效率高,但浪费空间,大对象时复制成本较高
-
标记整理 mark compact
- 1、标记出所有存活对象;2、对存活对象按照整理顺序(Compaction Order)整理到内存的一端;3、清理端以外的内存
- 没有碎片、无浪费空间,但效率偏低(两遍扫描,引用指针需要调整,内存变动频繁)
-
分代算法Generational Collection
-
java堆空间
-
新生代1/3
-
Eden区8/10
- 98%的对象朝生夕死
-
From区1/10
-
To区1/10
-
-
老年代2/3
-
哪些对象会进入
-
大对象
- 需要大量连续内存空间的对象,避免在新生代产生大量复制
-
长期存活对象
-
对象头中存放对象年龄,每经过一次minorgc年龄增加一次,默认到15时会进入
- 可配置:MaxTenuringThreshold
-
-
动态对象年龄
-
年龄1的占用了33%,年龄2的占用了33%,累加和超过默认的TargetSurvivorRatio(50%),年龄2和年龄3的对象都要晋升
- 有点负载均衡感觉
-
-
-
常用算法
-
-
-
是以上三种回收算法的组合算法
-
jvm中有哪些收集器
-
Serial old
-
分代收集器
-
ParNew
- 采用复制算法的多线程收集器
- 主要工作在 Young 区,可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。
-
CMS
-
目标:获取最短回收停顿时间
-
算法:三色标记+标记清除算法+增量更新算法
-
步骤
- 1、初始标记:STW,标记GC Root直接引用的对象;
2、并发标记:从GC Root引用对象开始遍历对象图,标记出可达对象;;
3、重新标记:STW,采用增量更新算法重新标记2步因用户线程增加引用的对象;
4、并发清理:清理未标记的垃圾对象;
5、并发重置:重置本次GC过程中的标记数据;
- 1、初始标记:STW,标记GC Root直接引用的对象;
-
问题
-
并发
-
抢占用户线程cpu资源
-
CMS默认回收线程数是(CPU个数+3)/4
这个公式的意思是当CPU大于4个时,保证回收线程占用至少25%的CPU资源,这样用户线程占用75%的CPU,这是可以接受的。
但是,如果CPU资源很少,比如只有两个的时候怎么办?按照上面的公式,CMS会启动1个GC线程。相当于GC线程占用了50%的CPU资源,这就可能导致用户程序的执行速度忽然降低了50%,50%已经是很明显的降低了。
-
解决办法:incremental mode(增量模式),执行过程中GC线程和用户线程交替执行
-
-
浮动垃圾
-
并发清理过程中产生浮动垃圾,可以忽略,下次清理
-
解决办法
-
提前回收机制:CMSInitiatingOccupancyFraction参数默认是内存占用92%时启动GC
- 如果设置99%,这是需要内存分配1%时会Concurrent Mode Failure错误,这是CMS默认启动Serial Old收集器,效率更慢
-
动态检查机制:UseCMSInitiatingOccupancyOnlyCMS参数设置CMS会根据历史记录,预测老年代还需要多久填满及进行一次回收所需要的时间。在老年代空间用完之前,CMS可以根据自己的预测自动执行垃圾回收。
-
-
-
GC执行过程不确定
- 在并发标记或者清理阶段会出现还没有回收完成又一次触发fullgc,这时会出现concurrent mode failure错误,此时会STW,用户serioal old处理
-
-
标记清除算法
-
产生碎片化内存,分配效率慢
-
解决办法
- UseCMSCompactAtFullCollection参数(默认开启),在Full GC后开启内存碎片整理,但是STW
- XX:CMSFullGCsBeforeCompaction,参数表示经历多少次fullgc后对内存空间压缩整理,默认为0,每次fullgc会压缩
-
-
-
-
最佳实践配置
- -XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=80 //回收内存占比
-XX:+UseCMSInitiatingOccupancyOnly //启动动态检查机制
-XX:CMSFullGCsBeforeCompaction=5//设置经历多少次fc后开始压缩整理碎片
- -XX:+UseConcMarkSweepGC
-
应用场景
- 多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除
-
-
-
分区收集器
-
G1
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。==这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率
-
目标:针对大内存、达到实时高效、高吞吐量
-
算法:三色标记+复制+标记压缩+STAB
-
基本概念
-
分区region
-
物理分区,逻辑分代,内存区域分为E O H S等,每个分代内存可以不连续
E代表是Eden区,S代表Survivor,O代表Old区,H代表humongous表示巨型对象(大小大小Region空间一半的对象)
-
单个分区取值1-32M,必须是2的幂次,-XX:G1HeapRegionSize
-
最多有2048个分区
-
-
SATB
-
Snapshot-At-The-Beginning,GC初始标记阶段对堆内存活的对象做一次快照,作用是维持并发GC的正确性
-
如何保证正确性
-
Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS(上一次标记的位置)和nextTAMS(下次标记的位置)。在TAMS以上的对象是新分配的,这是一种隐式的标记
- 解决了并发期间新对象分配
-
对象的引用被替换时,通过write barrier对引用字段复制进行环切AOP, 将旧引用记录下来,所以效率会低些,然后在最终标记阶段只扫描出有write barrier记录的对象
- 解决了灰色对象到白色对象的引用断开
-
-
问题:如果被替换的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,这就是float garbage,STAB精度偏低
-
-
写屏障
这块涉及到SATB标记算法的原理,SATB是指start at the beginning,即在并发收集周期的第一个阶段(初始标记)是STW的,会给所有的分区做个快照,后面的扫描都是按照这个快照进行;在并发标记周期的第二个阶段,并发标记,这是收集线程和应用线程同时进行的,这时候应用线程就可能修改了某些引用的值,导致上面那个快照不是完整的,因此G1就想了个办法,我把在这个期间对对象引用的修改都记录动作都记录下来,有点像mysql的操作日志。
-
RSet
-
Remember Set,每个分区中维护一个RSet,主要记录其他分区引用本分区对象的关系,谁引用了我的对象
-
如何辅助GC
- YGC时,选定Y区的RSet作为根集,里面记录old->young的跨带引用,避免扫描整个old代区
- mixed gc时,old代中每个分区记录old->old,young->old的RSet,不用扫描整个old分代区
-
-
-
CSet
- Collection Set,GC要收集的Region的集合(任意分代),跨分区的扫描RSet
-
停顿预测模型
- 通过模型统计计算出的历史数据来预测本次回收需要选择的Region数量,尽量满足设置的目标
- 通过XX:MaxGCPauseMillis参数设置用户期望的停顿时间,默认200ms
- 衰减标准偏差为理论基础
-
-
GC模式
-
Young GC
-
E区无法分配内存(达到阈值)时启动,即E区和S区复制到Old区(MaxTenuringThreshold参数配置)或者另外一个S区,多线程并行执行
YoungGC的回收过程如下:
根扫描,跟CMS类似,Stop the world,扫描GC Roots对象。
处理Dirty card,更新RSet.
扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。
拷贝扫描出的存活的对象到survivor2/old区
处理引用队列,软引用,弱引用,虚引用(下一篇优化中会再讲一下这三种引用对gc的影响)
-
-
Mixed GC
-
只回收老年代部分region,一般发生在YGC之后,目的是复用YGC扫描的GC Root,减少stw
-
发生时机
- G1MixedGCLiveThresholdPercent参数控制老年代分区中的存活对象比例,达到阈值这个分区会放在RSet中,默认45
- G1HeapWastePercent参数控制,在一次younggc之后,可以允许的堆垃圾百占比,超过这个值就会触发mixedGC
-
步骤
-
1、初始标记:标记GC Roots,会STW,复用YoungGC的暂停时间,设置好所有分区的NTAMS值
-
2、根分区扫描(RootRegionScan)
- 和java程序并行执行,基于标记算法,对Survivor对象全部扫描标记为gcroot
-
3、并发标记:从GC Root引用对象开始遍历对象树,标记出存活对象
-
4、最终标记:会STW,标记出在3阶段发生变化的对象,同时处理STAB缓冲区;
-
5、清除:STW,清除标记的垃圾对象,清理之后,将存活对象复制到其他可用分区,主要解决内存碎片问题
-
1、对各个Region的回收价值和成本进行排序,根据用户设置的停顿时间执行清除计划
比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内
-
2、采用复制算法,将一个region中的存活对象复制到另一个空的region,这样好处在于不会产生碎片
-
-
-
-
-
使用场景
-
服务端垃圾收集器
-
多处理器,内存偏大,一般大于6G以上
-
需要低延迟的响应(停顿时间可控)
-
存在以下情况可以尝试使用G1
- Full GC 次数太频繁或者消耗时间太长
- 对象分配的频率或代数提升(promotion)显著变化
- 受够了太长的垃圾回收或内存整理时间(超过0.5~1秒)
-
-
和CMS的区别
- 停顿时间可控
- 最终标记效率更高,G1只标记写屏障记录的对象,CMS Remark阶段扫描所有对象,STW时间更长
- CMS清除阶段是并发的,G1是STW
-
最佳实践配置
-
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 //设置停顿时间,默认200
-XX:INitiatingHeapOccupancyPercent=45 //设置整个堆使用率超过设置的值时启动Mix GC,默认45 -
不要设置年轻代的大小
通过-Xmn显式设置年轻代的大小,会干扰G1收集器的默认行为:
G1不再以设定的暂停时间为目标,换句话说,如果设置了年轻代的大小,就无法实现自适应的调整来达到指定的暂停时间这个目标
G1不能按需扩大或缩小年轻代的大小 -
响应时间度量
不要根据平均响应时间(ART)来设置-XX:MaxGCPauseMillis=n这个参数,应该设置希望90%的GC都可以达到的暂停时间。这意味着90%的用户请求不会超过这个响应时间,记住,这个值是一个目标,但是G1并不保证100%的GC暂停时间都可以达到这个目标
-
-XX:ParallelGCThreads=n //垃圾收集器的并行阶段的垃圾收集线程数
-
-XX:ConcGCThreads=n //垃圾收集器并发执行GC的线程数
-
-
-
ZGC
-
Shenandoah
-
-
三色标记法
-
含义
- 黑:对象和属性引用的对象已完成标记
- 灰:对象被标记,但属性资源引用的对象没有标记完成
- 白:对象没有被标记,回收对象
-
问题
-
漏标(两者缺一不可)
- Mutator将黑对象引用指向白对象
- Mutator删除灰对象到白对象的直接或者间接引用
-
解决办法
-
核心是解决其中一步即可
- CMS 增量更新+写屏障
黑对象新增白对象引用时通过写屏障记录下来,在重新标记阶段对记录的从新标记,即黑色对象变为灰色对象 - G1 Shenandoah STAB+写屏障
灰色对象删除了白色对象引用时,通过写屏障记录下来,然后重新标记阶段再次标记 - ZGC 读屏障(待学习和补充)
- CMS 增量更新+写屏障
-
-
-
XMind - Trial Version
来源:oschina
链接:https://my.oschina.net/u/4350719/blog/4926552