上一篇我们讲解了一些垃圾回收的理论和一些基础的算法和思想,这一篇主要是jvm从古至今垃圾收集器的实现。
注:有连线的代表他们可以互相配合使用。
最早的一款收集器,看名字就知道该收集器是一个单线程工作的收集器,单线程强调在它进行垃圾收集时必须暂停其他所有工作线程,直到它收集结束,这对很多应用来说是很不友好的。
特点:简单而高效(与其他收集器的单线程相比)、占用内存小
Serial对于运行在客户端模式下的虚拟机来说是个很好的选择。
-XX:+UseSerialGC=Serial+SerialOld 打开串行垃圾回收器
是Serial收集器的多线程并行版本,除了多线程之外,其他的和Serial一模一样
ParNew收集器主要 是使用CMS(+UseConcMarkSweepGC)的默认新生代收集器,看图一就知道了在JDK9之后,CMS收集器只能配合ParNew使用。CMS(后面会讲)是JDK5发布的真正意义上支持并发的垃圾收集器,它收集实现了让垃圾收集器与用户线程同时工作。但随着垃圾收集器的不断改进,CMS被G1所替代。
ParNew主要运行在多线程的环境,默认开启的收集器线程和处理器核心相同,可以使用-XX:ParallelGCThreads参数改变线程数。
垃圾回收器的并发和并行概念如下
并行(Parallel):描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,也就是多个垃圾收集线程一起多线程工作,用户线程等待,如ParNew。
并发(Concurrent):描述的是垃圾收集器线程和用户线程之间的关系,说明同一时间垃圾收集器线程和用户线程都在运行,只不过垃圾收集线程占用了一部分的系统资源,用户线程应用程序的处理可能会变慢(吞吐量受影响)。
也是一种并行的,基于标记复制的收集器。该收集器的关注点是吞吐量优先*,像CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间。
所谓吞吐量即处理器用于运行用户代码的时间与处理器总消耗时间的比值
吞吐量=运行用户代码的时间/(代码运行时间+运行垃圾收集时间)
像CMS这种关注停顿时间的更适合与用户的及时交互,就比如我宁愿多做几次垃圾回收,使得我每次垃圾回收的时间比较少,而Parallel Scavenge是我每次垃圾回收时间可以长一点,使得我尽量少做垃圾回收这一动作这种高吞吐量的更适合后台运算较多,不需要太多交互的任务。
使用-XX:+UseParallelGC -XX:+UseParallelOldGC 参数开启这对cp,只需开启一个另一个自动开启。
-XX:ParallelGCThreads=n :线程数
-XX:+UseAdaptiveSizePolicy:自动调整伊甸园区和幸存区的比例
-XX:GCTimeRatio=ratio :调整总时间和垃圾回收时间的占比,默认ratio为99
-XX:MaxGCPauseMillis=ms :默认200ms,最大暂停时间
Concurrent Mark Sweep,关注响应速度,低延迟,老年代,看名字就知道基于标记清除算法。
前面介绍的收集器GC线程运行时都是需要停顿用户线程,好点的也是用并行的垃圾收集缩短时间,而CMS是并发的,用户线程和GC线程可以一起工作,感觉是不是进步了不少?但它的工作过程相对复杂了不少,包括:
初始标记和重新标记任然需要Stop the World。
-XX:+UseConcMarkSweepGC :使用并发标记清除,基于标记清除的并发策略。
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld 是一对,老年代使用标记清除,新生代使用复制的垃圾回收器,当老年代发生并发失败的问题时,又会退化到SerialOld单线程的垃圾回收器。
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent :当达到percent时,会进行垃圾回收,以预留空间给浮动垃圾(因为CMS给予清除算法,难免会产生浮动垃圾),不能等到堆内存不足再清理,应该预留一点空间保留这些浮动垃圾
-XX:+CMSScavengeBeforeRemark :重新标记前先对新生代进行垃圾回收,以此减轻重新标记的压力,因为重新标记会检测老年带中的对象是否有被新生代的所引用,为了避免扫描新生代的这些垃圾,所以先清理掉,再进行可达性分析算法。
缺点:老年代容易产生碎片,当碎片过多时,可能会产生并发失败,所以需要退化到串行的垃圾回收执行一次,此时需要的时间较久。
现代收集器*:
Garbage First(简称G1),垃圾收集器发展历史的里程碑,JDK9默认收集器,取代了cms。
它开创了面向局部收集和基于Region的内存布局,他们是什么呢?
以前的垃圾收集器都是要么收集新生代,要么收集老年代,要么我就全收集,而G1不同,它可以面向堆内存任何地方来组成回收集(Collection Set)进行回收,衡量标准不再是以前的属于哪块分代了(依然遵循分代收集),而是哪块内存的垃圾数量多,回收的收益最大,这就是G1的MixedGC。
其中的Humongous区域专门用来存储超过一块Region容量一半的大对象;如果对象超过了单个Region的大小,将会用多个连续的Region存放,Region差不多可以看成是老年代。
jdk对这种大对象进行了优化,大对象会优先被回收,并且大对象不会被拷贝,代价大。
通过-XX:G1HeapRegionSize可以调整Region的大小,应为2的N此幂
用户可以设定允许的停顿时间(-XX:MaxGCPauseMillis=ms ,默认200ms)。
G1收集器每次都会按Region为单位,根据用户设定的允许的停顿时间,去寻找那些回收价值比较高的对象(这样可以避免在整个堆中进行收集),后台维护一个优先级列表,按需回收,这也是G1名字的由来,保证了G1在有限的时间获取尽可能高的收集效率。
也就是前面我们讲过的老年代可能会引用新生代的对象的问题,推出了记忆集的解决方案,在G1中,因为划分了Region,所以更复杂了,每个Region都维护了自己的记忆集,记录其他Region指向自己的指针,并标记指向的人在哪些范围之内,用Hash表实现的(Key存放跨带引用本Region的其他Region的起始地址,value存放卡表索引号)。可以看出G1的记忆集占用较多,大概占整堆的10%-20%。
G1的并发问题,也有不同,G1是通过原始快照(SATB)来解决的,G1为每一个Region设计了两个TAMS的指针,把Region的一部分空间划分出来用于并发回收过程中的新对象的分配,新对象地址必须在TAMS指针以上,默认他们是存活的,不纳入回收范围。
停顿时间?
通过-XX:MaxGCPauseMillis=ms参数设置了的停顿预测模型,怎么实现的?G1收集时会记录每个Region的回收耗时,Region记忆集里的脏卡数据等各个可测量的步骤花费的成本,求出平均值等信息,从而在不超过期望停顿时间的约束下获得最高的回收收益。而且如果把停顿时间设置的太小,每次只能回收一点垃圾,时间长了,就会OOM。我们需要适中的选择停顿时间。
除了并发标记外,都是需要停顿用户线程的,G1并不是一味地追求低延迟,只是在可控的延迟内获得更高的吞吐量。
优点:
整体上是标记整理算法,两个区域之间是复制算法
缺点:
为了减少FullGC的发生,我么可以提前一点进行垃圾收集。
jdk9增加了:-XX:InitiatingHeapOccupancyPercent调整,默认45%,即垃圾超过45%时就开始垃圾回收了。并且该比率可以动态调整
最后的两款收集器是Shenandoah和ZGC,被官方称为低延迟垃圾收集器,尚处于实验状态,但肯定是以后的主流收集器。
它们几乎整个工作过程都是并发的,只有初始标记和最终标记这些阶段有短暂的延迟。
内存占用、吞吐量、延迟是衡量垃圾收集器的三项重要指标,它们也被称为不可能三角
Shenandoah也是基于G1的Region的内存布局形式,也是优先处理价值大的垃圾……;
不同之处:
连接矩阵类似数据结构中图的邻接矩阵表示方法,假如有n个顶点,就设置一个形如int[n][n]的二维数组,如果第2个顶点指向了第4个顶点,则[4][2]=1,进行标记;把对象理解成图的顶点即可。
工作流程:
可以总结为:并发标记、并发回收、并发引用更新。
转发指针(Brooks Pointer)
在对象的结构布局上动手脚。通常一个对象都有一个对象头,现在在所有对象头前面统一加一个新的引用字段:
在正常情况下,该引用指向对象自己。如果该对象因为垃圾回收被复制到其他地方了,那么该指针就会指向那个复制的新的对象,并且这一过程在清理旧对象之前。想一想,如果在并发中,有其他引用找该旧对象,但旧对象的引用指向的不是本身,而是那个新对象,就不会发生啥问题了。但是会出现多线程竞争问题,这里就不再多说。
Z Garbage Collector,JDK11 新加入的具有实验性质的低延迟垃圾收集器。oracle研发。
ZGC 是一款基于 Region 内存布局,(暂时)不设分代,使用读屏障、染色指针和内存多重映射等技术来实现的可并发的标记-整理算法的垃圾收集器。
ZGC的Region布局不太一样,ZGC的Region其实被称为ZPage,具有动态性——动态创建和销毁,以及动态的区域容量大小,ZPage可分为:
颜色指针可以说是ZGC的核心概念。因为他在指针中借了几个位出来做事情,所以它必须要求在64位的机器上才可以工作。并且因为要求64位的指针,也就不能支持压缩指针。
在64位系统,目前Linux操作系统也就能支持46位(win 44位),当然高18位不能用来寻址,但它所支持的46位指针也能支持64TB的内存,对于现在已经能给充分满足服务器的需求。ZGC的染色指针就是将这46位的指针宽度拿出4位充当标志位,弊端就是ZGC管理的内存不能超过4TB。
为什么最高位16个不能用?
由于X86_64处理器硬件的限制,目前X86_64处理器地址线只有48条,也就是说64位系统支持的地址空间为256TB。为什么处理器的指令集是64位的,但是硬件仅支持48位的地址?最主要的原因是成本问题,即便到目前为止由48位地址访问的256TB的内存空间也是非常巨大的,也没有多少系统有这么大的内存,所以在设计CPU时仅仅支持48位地址,可以少用很多硬件
ZGC 的运行过程大致可划分为以下四个大的阶段,都是可以并发执行的,仅是两个阶段中间会存在短暂的停顿小阶段:
GC调优官方文档:
Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide, Release 16
本文部分摘自《深入理解 Java 虚拟机第三版》