文章目录
一、垃圾回收器分类
JVM规范没有明确给出太多垃圾回收器的规范,让厂商自行发挥。
分类标准及类别:
- 按一次执行GC线程数:
分为串行和并行【注意,是GC线程且并行,GC线程与用户线程是没法并行的,只能并发】
串行常用于单处理:同一个时刻只有一个GC线程执行;
并行:用于多处理器,每个CPU核心被一个GC线程占用,同时执行GC,比较高效。
对于32位操作系统,可以使用jdk的Client模式,这个模式下默认使用串行GC
- 按照与用户进程的工作模式:
分为并发与独占式:
【上面的串行、并行图示其实都属于独占式】
并发:可以让用户线程与GC线程在同一时间段交替的执行,不会出现用户线程长时间等待GC结束的情况,适用于高交互的场合。
独占:GC开始时,STW,直到一个GC任务结束,CPU的控制器完全交由GC线程。GC效率高,吞吐量高,适用于交互要求不高而对执行效率要求高的场合【经典:后台运算】
- 按照内存的碎片化整理来分:
压缩式:进行清理的同时,会将存活对象存放到起始的连续空间,之后不需要通过空闲列表分配,而采用指针碰撞。
非压缩式:只会将程序标记为清除,不会进行移动,容易出现空间碎片,但是总体较高效。
二、垃圾回收器的性能指标
- 吞吐量:实际进行用户线程的时间 / 总运行时间。【因此,GC相关耗时间越长,吞吐量越低】
- 暂停时间:为了进行GC,一次STW所耗时间。
由于最重要的三项指标不可能同时达到,因此称为不可能三角,最多可以达到两项【吞吐量与暂停时间是老死不相往来的关系】
不同类型的垃圾收集器会根据倾向与吞吐量还是暂停时间有不同的特点,因此不能说哪种最好,只能根据不同的应用场景选用不同的垃圾处理器。
如下图:
- 上面是注重吞吐量的场合,因此垃圾处理的总时间较短,但是一次执行垃圾清理的时间却很长
- 下面是注重暂停时间的场合,每次暂停都很短,但是总GC时间耗费就长了,导致吞吐量降低。
当前GC收集器的设计目标,总体概括起来就一句话:
在所要求【可控】的暂停时间以内,尽量提高吞吐量
三、垃圾收集器的版本迭代
存在一个历史分水岭:2018来了JDK11发布,引入ZGC【ZGC的Z是26英文字母的最后一位,暗示Oracle不会再推出新的垃圾处理器,而是不断对ZGC做优化】
Shenandoah大致性能与ZGC相差不远,但是还是有一点差距,且不是Oracle产品,不收待见【RedHat产品】
分水岭之前的七种垃圾回收器及新老组合关系:
黑色实线是当前可用的组合【当前:JDK14】
红色虚线表示在JDK9就已经移除的场合【即使是JDK8,也不推荐使用,因为后期肯定会被移除】
绿色虚线和蓝色框框表示JDK14弃用的组合和垃圾收集器。
可见,当前推荐使用的老版本垃圾收集器只有三种组合:
- 单处理器场景下的: Serial + SerialOld
- Parallel Scavenge + ParallelOld【JDK8默认组合】
- 最强大老版本GC:G1【G first】
注意点:
-
CMS和parallel的GC框架不一样,不能兼容,因此不能组合;
-
Serial是CMS的后备处理【cms失败,进行Serial,因为Serial在多处理器场景下的低效,间接导致了CMS被淘汰】
还是那句话:
没有最好的,根据应用场景选择合适的
查看当前系统使用的GC版本:【两种方式】
①通过程序参数查看:
-XX:+PrintCommandLineFlags
表示打印一下通过命令行的xxFlags设置的参数,其中就包括GC版本
②通过命令行查看
通过
jinfo -flag UseXXXGC 进程号
如:jinfo -flag UseParalellGC 进程号
我的版本是JDK15,默认使用G1收集器:
四、串行回收器:Serial与SerialOld
Serial是一种串行垃圾回收器,适用于单处理器场合,常用在Client模式下。
当新生代使用Serial时,老年代会被默认使用功能SerialOld【注,就没有对SerialOld进行配置的命令,这点和Parallel不一样,Parallel是老少都能配,配了一个另一个就顺带配上了】
Serial在多线程模式下异常低效,但在单线程场景下确实最快的,因此到现在也还有它的身影。
从之前的图示也可以看出,SerialOld某种意义上来说也是“最受欢迎的”,因此他身上的连线最多。
Serial优缺点:
- 优点:实现简单,单处理机下最高效
- 缺点:多处理下最低效
在交互性强的场合基本上完全不能使用,因为它的等待时间最长,比其他高吞吐量的还要长【就因为这点,推动了CMS的淘汰】
五、并行垃圾回收器:ParNew
总体概念:
很尴尬的收集器,高不成低不就。
一款并行处理的垃圾回收器。
是Serial的并行版本,代码互相借鉴,导致重合度很高【我抄我自己】
看之前的图:
与ParNew相连的只有CMS与SerialOld,但是由于CMS当前已经被移除,SerialOld与ParNew的组和当前也被废除,相当于间接意义上ParNew也被删除了。
ParNew的单处理器环境执行效率不如Serial,多处理下也不是很好。
ParNew可以自定义GC执行的线程数量
设置线程数的注意点:
- 不得超过CPU核心数【防止多线程一直争抢CPU造成额外开销,性能不增反减】
- 尽量接近CPU核心数【利用多处理器资源】
综上,保持默认和CPU核心数相等就行
若需要设置老年代GC:
-XX:+UseConcMarkSweepGC
若超过其版本弃用,是不让我们使用的。像我就用不了
六、吞吐量优先垃圾回收器:Parallel Scavenge
Parallel回收器至今仍在使用。
他在后台运算场景保持非常高的效率。
Parallel的设计初衷是,保持一个可接受的等待时间限制下,尽可能的使得程序高吞吐量【固定死一个,另一个尽可能最好】
应用场合:后台运算,不需太多交互的场合【高吞吐量收集器的套话】
与ParallelOld是一个不错的组合【jdk8的默认配置】
参数配置:
和Serail一样,开启Parallel或者ParallelOld任一个都会默认开启他的好基友
若需要设置最大停顿时间,最好开启自适应策略,让GC程序可以根据当时的内存等场景自动调整各项配置
Parallel在JDK15.0还在使用,但是ParallelOld已经被移除。
七、低延迟垃圾回收器:CMS
设计CMS是为了尽可能减少每次STW的时间。
真正意义上的并发,并不是ParNew那样半吊子并发。
CMS是老年区的垃圾回收器。
CMS将垃圾收集工作分为几个阶段:
用户线程执行一段时间之后,开始初始标记【耗时极短,需要STW】
随后,与用户线程并发执行,进行并发标记
再次开启STW,修补之前标记并对这段时间的新垃圾坐标记
清理线程、重置线程与用户并发执行
并发在处理器不足的情况下并不是同一时刻两者同时执行的,而是交替执行,只能说对于两者漫长的执行时间来说,小小的切换看上去就是同时执行的【你中有我,我中有你】
低延迟的实现:
STW只有短暂的初始标记与重新标记环节
CMS将耗时巨大的工作都与用户进程同步执行,而将小规模短耗时的工作采取STW的形式执行。
整个流程中STW的时间极短,因此用户几乎感受不到GC的过程。
优缺点分析:
- 优点:
- 暂停时间短,交互性强
- 并发度高
- 缺点:
- 采取标记-清除算法,导致大量内存碎片
- 执行效率低
- 剽窃用户进程的CPU,导致用户进程执行较为缓慢
- 致命问题:空间碎片大量充斥,无法分配会采取SerialOldGC,导致多线程的GC达到最慢,停顿时间也达到最长。
采用标记清除算法
若分配失败,使用SerialOld回收器
为什么不使用标记清除算法?
用户程序正在并发执行,不能进行对象地址的移动
浮动垃圾:前面提过,标记垃圾的过程有和用户线程同步进行的时候。这段时间内,若用户线程产生新的垃圾,CMS将很可能不能标记
不稳定:失败时使用性能最差的SerialOld
参数设置:
若在老年代使用CMS,会在新生代默认使用ParNew
还是有ParNew的立足之处的,因为CMS作为OldGC的场合,会默认使用ParNew作为新生代GC。
八、区域化分代垃圾回收器:G1
目标:设定一个目标暂停时间限制的情况下,1尽可能地达到最高的性能,并且兼顾各种回收的任务的较全能的回收器。
名称的含义:Garbage First
G1会优先收集垃圾较多的区域(Region)【价值高】,而暂时忽略垃圾较少的区域
8.2 G1的优势、特点
四个字概括:分区算法
-
并发与并行
执行YoungGc时,进行高效并行的垃圾处理【STW】
进行OldGC时,为了追求更短的停顿时间,采取与用户线程并发执行的方式。 -
分代收集
区分年轻代和老年代分别收集的收集器
不要求各个区的空间连续,而是按类型化为一个个小区域,离散分布。
-
空间整合
每次回收之后,非空闲部分会连续的占满区域的最前方,处理了垃圾碎片的问题。
适合长时间运行
-
可预测的停顿时间
因为是分代的,每个区域都很小,暂停时间就比较好控制【选择适量的区进行收集即可。选择区域的标准就是价值】
软实时:即尽最大努力将延迟控制在设定范围内,若是不能也别怪我【大概率可以,约90%可以】
缺点:
若执行GC的堆区内存不大,对比收集的垃圾数量价值,收集垃圾所耗费的资源更高【及入不敷出】
这样的场景,他的性能不及之前的CMS。
常用于服务器的应用。
8.3 参数设置
区域常常设置为2048个, 每个region的大小取2m的整数幂。
最大停顿时间不应该太小,回大幅度降低吞吐量,甚至引发FULL GC。
8.4 应用场景
用于服务器应用比较合适,只有堆区较大的场合才能发挥出优势。
亮点:使用用户线程协助进行垃圾回收,可提高垃圾回收的速度。
8.5 分区思想:化整为零
区域更像是新声代老年代这样的“分区”而不是小块。其内部可以存储多个对象,且由于采用复制算法和标记压缩算法,每次整理都会将对象移到区域最开头的连续空间,这时候可以使用指针碰撞来进行。
而记录哪些处于空闲,哪些区域存放,用的就是空闲列表,G1是两者结合的思想。
由于此时只是单纯单纯空闲区域就能复制,因此无需向以前那样特定分为Survivor1与Survivor2,
空闲的都可作为新的Survivor
大对象不再存放在老年区,避免低生命大对象也存储到老边区,年龄不匹配。
开发了新的大对象区:Humgonous,总体来说和Old区等级别
8.6 垃圾回收过程
若延迟设置过小,长时间的来不及处理的垃圾累计最终会导致FullGC,作为保护手段保留
但是其造成的大停顿,仍然需要尽量避免FullGC
老年代只有所占空间超过区域的45%才会开始并发标记。
RememberedSet 记忆集
Why?
当对象出现跨区域引用时,查找这个对象的引用难免会涉及遍历其他区域。
这样一次标记的延迟可就高了。c
采取空间换时间的策略:用一个表:记忆集来表示这个对象被其他区域引用的记录。
写屏障(WriteBarrier):
引用对象进行写操作时,会被暂时中断,若发现是跨区域的引用,就将其记录到记忆集中。这个中断过程就是写屏障。
Region2的对象被Region1和Region3的对象引用,会被记录在Region2的Remember表中。
8.6.1 年轻代回收
年轻代的回收使用复制算法
这是一个独占式、GC并行的回收过程,需要STW。
Eden区和当前的S区都会被复制到新的S区
其中,也有一些S区达到年限,晋升到O区
DirtyCardQueue【辅助RememberSet使用,处理并发问题】
当内存中出现引用变更时,会在DirtyCardQueue中入队一个记录,之后会根据这个区域的Cart进行这个区域的记忆集更新【为什么不直接记忆集更新,而要多此一举呢?是因为记忆集的更新涉及到同步,更加消耗资源,还不如多此一举先记录一下,资源耗费反而更低】
8.6.2 并发标记过程
并发标记主要是指老年代区域的标记及回收过程
Survivor区的扫描必须在young GC之前完成:因为young gc时就会执行Survivor区的移动,到时候就来不及查看Survivor区的剩余标记了。
一个区域被标记全部是垃圾,立即全部回收这个region
8.6.3 混合回收
混合回收是为了收集之前的一些优先度、价值比较低的区域【老年代和新生代都有】。
是一个未雨绸缪式的回收。
前两个过程中,大部分垃圾很多的区已经被搞完了。现在还有一些杂鱼活着。
混合回收会查找这些杂鱼中垃圾比较多的【超过65%的垃圾】进行回收。
混合回收要经历八次。
8.6.4 Full GC
本来G1就是为了尽量避免FullGC的出现,但是由于错误的设置或者代码原因、配置原因等,导致上面的算法失效,最终会引发full gc
其出现的场合只有两种:
- 内存太小,没有空间防止复制的存活对象;
- 设置的暂停时间上限太小,还没能清理结束就必须停止,长时间这样最终导致内存耗尽
8.7 G1设置的建议
- 不要设置新生代大小和比例,防止覆盖暂停时间的上限设置;
- 暂停时间不要设置太短,影响系统吞吐量
九、七个经典垃圾收集器的比较
面试必背
CMS看来是英年早逝,Serial确是老骥伏枥。
九、日志信息
十、新时代的垃圾回收器
10.1 Shenandoah
Shenandoah:追求短停顿,号称停顿时间与堆大小无关
【如何理解这句话?
意思是说,他已经基本做到了和用户线程的并发执行,因此可以在用户线程执行的过程中GC,自反正都在用户执行的时候执行,堆大小对回收自然没多大影响。
其实还是有的,初始标记时,堆太大、对象太多’还是会影响标记速度,其他并发过程确实没什么影响了】
从论文数据可以看到:
他的运行效率最低,但是停顿时间很短,比以往的低了一个数量级。
10.2 ZGC
ZGC只在初始标记时非常短的时间内发生了STW,其他绝大部分时间都是并发工作的
延迟之低,恐怖如斯。
基本上达到了延迟低于10ms的任务,且吞吐量能力基本与Paralell持平。
延迟对比其他垃圾收集器可以说是降维打击。