我们可以把垃圾回收器分为3类
串行、吞吐量优先、响应时间优先
其中,串行是单线程,吞吐量优先和响应时间优先是多线程
本文重点讨论的是分代收集器,ZGC会另外做一个文章专门讲解
1 串行(单线程)
Serial收集器(复制算法)——新生代
Serial Old收集器(标记-整理算法)——老年代
- 单线程
- 对内存较小,适合个人电脑
- 采用复制算法-新生代,标记整理算法-老年代
-XX:+UseSerialGC=Serial+SerialOld
——开启串行GC
回收过程
安全点:
GC时,所有用户线程都会在一个地方停止下来,这个地方就是安全点,目的是防止用户线程引用的内存地址因垃圾回收而混乱
2 吞吐量优先(多线程)
Parallel Scavenge收集器(标记-复制算法)——新生代
Parallel Old收集器(标记-复制算法)——老年代
追求高吞吐量,高效利用CPU(垃圾回收时CPU占用会到100%),吞吐量一般为99%,吞吐量=用户线程时间/(用户线程时间+GC线程时间)
- 多线程
- 堆内存较大,多核cpu
- 让单位时间内,STW的时间最短 0.2 0.2 = 0.4
- 采用复制算法-新生代,标记整理算法-老年代
- 垃圾回收时间对总程序的时间占比越低,则吞吐量越高
开启使用
-XX:+UseParallelGC — -XX:+UseParallelOldGC
——开启吞吐量优先GC,开启其中一个,另一个自动开启(JDK1.8默认开启)
-XX:+UseAdaptiveSizePolicy
——采用自适应的新生代大小调整
-XX:GCTimeRatio=ratio
——调整吞吐量的目标,垃圾回收时间不能超过总时间的(1/(1+ratio)),ratio默认为99,一般设置为19,如果达不到设定的吞吐量,就会把堆内存设置得更大,这样垃圾回收的次数就不频繁,但单次GC暂停时间就会越长
-XX:MaxGCPauseMillis=ms
——调整最大暂停时间,默认200ms,与GCTimeRatio冲突
-XX:ParallelGCThreads=n
——设置线程数
回收过程
3 响应时间优先(多线程)
ParNew收集器(标记-复制算法)——新生代,Serial收集器的多线程版本
CMS(Concurrent Mark Sweep)收集器(标记-清除算法)——老年代
用户线程与GC线程并发进行,用户线程与GC线程会竞争抢占CPU
- 多线程
- 堆内存较大,多核cpu
- 尽可能让单次STW的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
- 在垃圾回收的过程中可能会产生新垃圾,被称为浮动垃圾
- 采用标记清除算法-老年代,复制算法-新生代
开启使用
-XX:+UseConcMarkSweepGC — -XX:+UseParNewGC — SerialOld
——可以与用户线程并发执行,用于老年代,在并发开启失败后(由于内存碎片过多),会退化为串行
-XX:ParallelGCThreads=n — -XX:ConcGCThreads=threads
——ParallelGCThreads是并行的线程数,ConcGCThreads是并发的线程数,一般设置为ParallelGCThreads的1/4
-XX:CMSInitiatingOccupancyFraction=percent
——为了应对浮动垃圾,预先设置触发GC的内存阈值,一般设置为80,早期JDK默认设置为65
-XX:+CMSScavengeBeforeRemark
——在重新标记之前,要对新生代进行垃圾回收
回收过程
初始标记: 仅仅标记GC ROOTS的直接关联对象,即只标记根对象,世界暂停
并发标记: 使用GC ROOTS TRACING算法,进行跟踪标记,世界不暂停
重新标记: 因为之前并发标记,其他用户线程不暂停,可能产生了新垃圾,所以重新标记,世界暂停,且只追踪在并发标记过程中产生变动的对象
问题
- 会产生浮动垃圾
- 会产生过多的内存碎片,导致CMS退化为串行,响应时间增加
4 G1
概述
定义: Garbage First(Garbage One),一种新型垃圾收集器,相比于CMS收集器,G1收集器有两个最突出的改进
- 基于 标记-整理 算法,不会产生内存碎片
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收
发展历史
- 2004 论文发布
- 2009 JDK6 体验
- 2012 JDK7 官方支持
- 2017 JDK9 默认
适用场景
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
- 超大堆内存,堆内存越大,G1对比CMS的优势就越大,会将堆划分为多个大小相等的 Region(每个区域1,2,4,8…,每个区域都可以作为Eden、Servivor、old…)
- 整体上是 标记+整理 算法(解决了CMS的内存碎片问题),两个区域之间是 复制 算法
相关JVM参数
-XX:+useG1GC
——JDK9以前
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
G1 垃圾回收阶段
新生代垃圾收集、新生代垃圾收集+并发标记、混合收集三个阶段,循环发生
第一个阶段——Young Collection(新生代)
每个region可以作为任意区域,新生成的对象会任选一个region放入,该region就会变成Eden区(简称E区)
E区资源紧张,会触发GC,把依然存活的对象放入一个新的region,该region就会变成Survivor区(简称S区),该过程会STW
如果S区资源紧张,会触发GC,把可以放入老年代的对象
放入一个新的region,该region就会变成Old区(简称O区),该过程会STW
第二阶段——Young Collection+CM(新生代垃圾回收+并发标记)
-
在Young GC时会进行GC Root的初始标记
-
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),该过程与CMS的并发标记类似,阈值由下面的JVM参数决定
-XX:InitiatingHeapOccupancyPercent=percent
——默认45
第三阶段——Mixed Collection(混合收集)
会对E、S、O区进行全面垃圾回收
-
最终标记(Remark)会STW,主要标记之前并发标记时漏掉的对象,这个地方也与CMS的重新标记类似
-
拷贝存活(Evacuation)会STW,对老年代来说,G1 会根据最大暂停时间,部分回收O区的垃圾,只回收垃圾最多的区域,这也是为什么叫Garbage First的原因
-XX:MaxGCPauseMillis=ms
——最大停顿时间
图中,橘色虚线边框的O区并没有发生GC,就是因为G1会采取Garbage First的策略
所以,G1之所以能做到既保证高吞吐量,又保证低响应时间,就是因为它并不会对老年代中所有的对象进行回收,而是选择性地进行回收,G1之所以能这样做,是基于其分区的策略,即把整个堆划分为了一个一个region。
每种垃圾回收器发生Full GC的时机
-
SerialGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
-
ParallelGC
-
新生代内存不足发生的垃圾收集 - minor gc
-
老年代内存不足发生的垃圾收集 - full gc
-
-
CMS
-
新生代内存不足发生的垃圾收集 - minor gc
-
老年代内存不足,并发失败 - full gc
-
-
G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足(老年代占整个堆内存的45%时),且垃圾回收速度跟不上用户产生垃圾速度 - full gc
Young Collection 跨代引用
新生代的根对象查找,有一部分来自老年代,老年代存活对象很多,如果挨个遍历,效率会很低,这个时候我们就会采用一种卡表技术(card table)
我们会把O区细分为很多个card(512k),如果老年代中有一个对象引用了新生代的对象,我们就把这个对象对应的card标记为脏卡,这样,新生代的根对象查找时,我们只用遍历脏卡,大大缩小了遍历区域
- 卡表与 Remembered Set
- 在引用变更时,是异步进行的,通过 post-write barrier+dirty card queue 技术(这个地方笔者精力有限,没有深入研究,感兴趣的读者可以自行查阅别的资料),将待更改的card放入一个card queue中进行更新
- concurrent refinement threads 更新 Remembered Set
Remark
pre-write barrier——写屏障技术,在对象引用改变前,将对象让如队列satb_mark_queue
黑色的是已经处理完成的且有引用的对象,灰色的是在处理中的对象,白色的是还没有处理的对象
其它版本的优化
JDK 8u20 字符串去重
在新生代回收时检查是否有重复字符串,如果有,则让他们指向同一个对象
优点:节省大量内存
缺点:略微增加新生代回收时间
-XX:+UseStringDeduplication
默认开启
JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark
默认开启
JDK 8u60 回收巨型对象
- 一个对象大于 region 的一半时,称之为巨型对象
- G1 不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉
JDK 9 并发标记起始时间的调整
-
并发标记必须在堆空间占满前完成,否则退化为 FullGC
-
JDK 9 之前需要使用
-XX:InitiatingHeapOccupancyPercent
-
JDK 9 可以动态调整
-
-XX:InitiatingHeapOccupancyPercent
用来设置初始值 -
进行数据采样并动态调整
-
总会添加一个安全的空档空间
-
JKD 9 更高效的回收
…
之后的优化这里就不一一列出了,感兴趣的读者可以去查阅Oracle官方文档:https://docs.oracle.com/en/java/javase/12/gctuning
以上便是JVM中垃圾收集器的全部内容