目录
0.基本概念
- 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
- 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
- 吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
- Stop The World:通过GC Roots进行引用检查,必须保证在一个能确保一致性的快照中进行,具体指的是在整个分析期间整个系统看起来就像冻结在某个节点,不可以出现分析过程中对象引用关系还在不断变化的情况,否则就无法保证准确性
- 安全点:在STW时,并非在所有地方都能停顿下来开始GC,只有在到达安全点时时参能暂停,例如方法的调用,循环跳转,异常跳转等功能指令才会产生安全点。
1. 垃圾回收器分类与概要描述
1.1 简单分类
总体上可以把Java的垃圾回收器分为3类:
- 串行垃圾回收器(Serial Garbage Collector)
- 并行垃圾回收器(Parallel Garbage Collector)
- 并发标记扫描垃圾回收器(CMS Garbage Collector)
1.2 组合使用关系
Java垃圾回收器主要如下10种,各自优缺点以及组合关系如下(此图是马士兵老师课堂笔记):
上图中的且其中的蓝色连线表示可以搭配使用,红色方框代表此垃圾回收器可使用的堆内存区域
1.3 概要描述
GC收集器的不断优化,是为了消除或者减少工作线程因内存回收而导致的停顿。
2. Serial 收集器
2.1 Serial 收集器描述
Serial 是一款用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial 进行垃圾收集时,只用一条线程执行垃圾收集工作,并且在收集的同时,所有的用户线程必须暂停(Stop The World)。
2.2 Serial 收集器运行示意图
Serial / Serial Old收集器运行示意图
2.3 特点与适用场景
特点:因为简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,没有线程交互的开销,专心做GC,自然可以获得最高的单线程效率。Serial收集器对于运行在client模式下的应用是一个很好的选择(到目前为止,它依然是虚拟机运行在client模式下的默认新生代收集器)
2.4 参数设置
-XX:+UseSerialGC
3 ParNew收集器
3.1 ParNew描述
ParNew收集器其实就是Serial收集器的多线程版本,也是采用复制算法,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样,也共用了相当多的代码。
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
CMS收集器是一个被认为具有划时代意义的并发收集器,因此如果有一个垃圾收集器能和它一起搭配使用让其更加完美,那这个收集器必然也是一个不可或缺的部分了。
3.2 运行示意图
收集器的运行过程如下图所示:
3.3 特点与适用场景
特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题,在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
3.4 参数设置
指定使用CMS后,会默认使用ParNew作为新生代收集:
"-XX:+UseConcMarkSweepGC"
强制指定使用ParNew:
"-XX:+UseParNewGC"
指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相:
"-XX:ParallelGCThreads"
3.5 为什么只有ParNew能与CMS收集器配合
- CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作
- CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作
- 因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码
4. Parallel Scavenge 收集器
4.1 描述
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。Parallel Scavenge收集器关注点是吞吐量(如何高效率的利用CPU)
4.2 运行示意图
Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,不进行手工优化,可以选择把内存管理优化交给虚拟机去完成。
4.3 特点与适用场景
多线程收集,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput)
使用:
- 高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;
- 当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;
- 例如,那些执行批量处理、订单处理(对账等)、工资支付、科学计算的应用程序;
4.4 参数设置
#使用此GC
-XX:+UseParallelGC
#最大垃圾收集停顿时间:
-XX:MaxGCPauseMillis
#垃圾收集时间占总时间的比率
-XX:GCTimeRatio
GC自适应的调节策略(GC Ergonomics)
-XX:+UseAdaptiveSizePolicy
控制最大垃圾收集停顿时间:"-XX:MaxGCPauseMillis,"参数允许的值是一个大于0的毫秒数,MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;因为可能导致垃圾收集发生得更频繁;
设置垃圾收集时间占总时间的比率:"-XX:GCTimeRatio",设置垃圾收集时间占总时间的比率,0 < n < 100的整数,也就是程序运行时间占总时间的比率,默认值是99,即垃圾收集运行最大1%(1/(1+99))的垃圾收集时间
GC自适应的调节策略(GC Ergonomics)
参数:-XX:+UseAdaptiveSizePolicy,这是个开关参数,打开之后就不需要手动指定新生代大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、新生代晋升年老代对象年龄(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以达到最大吞吐量,这种方式称为GC自适应调节策略,只需设置好内存数据大小(如"-Xmx"设置最大堆),然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标,自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。
5 Serial Old收集器
5.1 描述
Serial Old是Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的java虚拟机默认的年老代垃圾收集器。
5.2 运行示意图
新生代Serial与年老代Serial Old搭配垃圾收集过程图:
新生代Parallel Scavenge/ParNew与年老代Serial Old搭配垃圾收集过程图:
5.3 特点与适用场景
使用:在Server模式下,主要有两个用途:
a.在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。
b.作为老年代中使用CMS收集器的后备垃圾收集方案,在并发收集发生Concurrent Mode Failure时使用
5.4 参数设置
#设置Serial时默认的老年代回收器
-XX:+UseSerialGC
#设置ParNew时默认的老年代回收器
-XX:+UseParNewGC
6 Parallel Old收集器
6.1 描述
Parallel old是 Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器,并且是在JDK1.6才有此收集器的。
6.2 运行示意图
6.3 特点和适用场景
使用:多线程,采用标记-整理算法。注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
6.4 参数设置
#年轻代GC设置为Paralle时默认老年代GC为ParallelOld
-XX:+UseParallelGC
#强制指定老年代GC
-XX:+UseParallelOldGC
7 CMS收集器
7.1 描述
Concurrent mark sweep(CMS)收集器是一种老年代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。在JDK1.5出现。
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验,CMS收集器是Sun HotSpot虚拟机中第一款真正意义上并发垃圾收集器,它第一次实现了让垃圾收集线程和用户线程同时工作。
7.2 运行示意图
CMS收集器工作过程:
工作机制:
CMS工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下4个阶段:
- a.初始标记:只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
- b.并发标记:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
- c.重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
- d.并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
7.3 特点和适用场景
并发收集、最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验
7.4 参数设置
#使用CMS GC回收老年代,年轻代默认为ParNew收集器
-XX:+UseConcMarkSweepGC
#CMS启动的内存阀值 1.5默认为68,1.6默认为92
-XX:CMSInitiatingOccupancyFraction=68
#开启内存碎片整理
-XX:+UseCMSCompactAtFullCollection
#在执行n次不压缩后,进行一次内存碎片整理,默认为0
-XX:CMSFullGCsBeforeCompaction=0
7.5 三个缺点
缺点1. CPU资源占用
CMS收集器对CPU资源非常敏感,其默认启动的收集线程数=(CPU数量+3)/4,在用户程序本来CPU负荷已经比较高的情况下,如果还要分出CPU资源用来运行垃圾收集器线程,会使得CPU负载加重。
例如:当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。(比如 CPU=2时,那么就启动一个线程回收,占了50%的CPU资源。)(一个回收线程会在回收期间一直占用CPU资源)
CPU资源解决办法:
针对这种情况,曾出现了"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS);类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间;但效果并不理想,JDK1.6后就官方不再提倡用户使用。
缺点2.浮动垃圾
无法处理浮动垃圾,由于CMS并发清除阶段用户线程还在运行,伴随着程序还在产生新的垃圾,这一部分垃圾出现在标记之后,CMS无法在当次收集中处理掉它们,只能留到下次再清理,这一部分垃圾称为“浮动垃圾”。
可能出现"Concurrent Mode Failure"失败而导致另一次Full GC产生。也正是由于在垃圾收集阶段用户线程还在运行,那么也就需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等待老年代填满之后再进行收集,需要预留一部分空间给并发收集时用户程序使用。可以通过“-XX:CMSInitiatingOccupancyFraction”参数设置老年代内存使用达到多少时启动收集。在JDK1.5默认为68%,在JDK1.6默认启动阀值为92%。
在Concurrent Mode Failure失败,虚拟机将启动后备预案:临时启用Serial Old收集器重新进行老年代的垃圾收集,停顿时间更长,因此并不是CMSInitiatingOccupancyFraction参数设置的越大越好。
缺点3. 产生大量内存碎片
由于CMS是基于“标记+清除”算法来回收老年代对象的,因此长时间运行后会产生大量的空间碎片问题,可能导致新生代对象晋升到老生代失败。
由于碎片过多,将会给大对象的分配带来麻烦。因此会出现这样的情况,老年代还有很多剩余的空间,但是找不到连续的空间来分配当前对象,这样不得不提前触发一次Full GC。
内存碎片解决办法:
使用"-XX:+UseCMSCompactAtFullCollection"和"-XX:+CMSFullGCsBeforeCompaction",需要结合使用。
为了解决空间碎片问题,CMS收集器提供−XX:+UseCMSCompactAlFullCollection标志,使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;
CMSFullGCsBeforeCompaction
由于合并整理是无法并发执行的,空间碎片问题没有了,但是有导致了连续的停顿。因此,可以使用另一个参数−XX:CMSFullGCsBeforeCompaction,表示在多少次不压缩的Full GC之后,对空间碎片进行压缩整理。可以减少合并整理过程的停顿时间;默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
7.6 CMS & Parallel Old的对比
CMS与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;
但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;
原因:CMS不进行内存空间整理节省了时间,但是可用空间不再是连续的了,垃圾收集也不能简单的使用指针指向下一次可用来为对象分配内存的地址了。相反,这种情况下,需要使用可用空间列表。即,会创建一个指向未分配区域的列表,每次为对象分配内存时,会从列表中找到一个合适大小的内存区域来为新对象分配内存。这样做的结果是,老年代上的内存的分配比简单实用碰撞指针分配内存消耗大。这也会增加年轻代垃圾收集的额外负担,因为老年代中的大部分对象是在新生代垃圾收集的时候从新生代提升为老年代的。
由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大;
8 G1 收集器
8.1 描述
G1(Garbage-First)是JDK7-u4才推出商用的收集器;G1是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。G1的使命是在未来替换CMS,并且在JDK1.9已经成为默认的收集器。
上一代的垃圾收集器(串行serial, 并行parallel, 以及并发CMS)都把堆内存划分为固定大小的三个部分: 年轻代(young generation), 年老代(old generation), 以及持久代(permanent generation)。
G1区域划分:
G1垃圾收集器采用的是区域化,分布式的垃圾收集器。其核心思想为将整个堆内存区域划分成大小相同的子区域(Region),在JVM启动时会自动设置这些区域的大小(区域大小范围“1MB~32MB”,默认设置2048个区域,即此时支持的最大内存“32MB*2048=65536M”,64G内存),这样Eden,Survivor,Tenured(Tenured还有一种细分 humongous,用来存放大小超过 region 50%以上的巨型对象)就变为了一系列不连续的内存区域,也就避免了全内存区GC的操作。
8.2 运行示意图
如果不计算维护 Remembered Set 的操作,G1收集器大致可分为如下步骤:
初始标记:
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
并发标记:
从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)
最终标记:
修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录。虚拟机将并发标记时间段对象变化记录在Remembered Set Log中,最终标记阶段需要把Remembered Set Log合并到Remembered Set中;需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
筛选回收:
对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。首先排序各个Region的回收价值和成本;然后根据用户期望的GC停顿时间来制定回收计划;最后按计划回收一些价值高的Region中垃圾对象;回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;可以并发进行,降低停顿时间,并增加吞吐量;
8.3 特点和使用场景
4个特点如下:
特点1:并行与并发
G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
特点2:分代收集
G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
- 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
- 能够采用不同方式处理不同时期的对象;
- 虽然保留分代概念,但Java堆的内存布局有很大差别;
- 将整个堆划分为多个大小相等的独立区域(Region);
- 新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;
特点3:空间整合
G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。
与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。这是一种类似火车算法的实现
特点4:可预测的停顿
G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。
8.4 G1为什么能建立可预测的停顿时间模型
因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆价值的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这样就保证了在有限的时间内可以获取尽可能高的收集效率。
8.5 G1与其他收集器的区别
其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。
8.6 G1收集器存在的问题
Region不可能是孤立的,分配在Region中的对象可以与Java堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。其他收集器也存在这种问题(G1更加突出而已)。会导致Minor GC效率下降。
G1收集器是如何解决避免扫描整个堆?
G1与其他垃圾收集器都是采用Remembered Set来避免整堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。
8.7 参数设置
指定使用G1收集器:
"-XX:+UseG1GC"
当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45:
"-XX:InitiatingHeapOccupancyPercent"
为G1设置暂停时间目标,默认值为200毫秒:
"-XX:MaxGCPauseMillis"
设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region:
"-XX:G1HeapRegionSize"
新生代最小值,默认值5%:
"-XX:G1NewSizePercent"
新生代最大值,默认值60%:
"-XX:G1MaxNewSizePercent"
设置STW期间,并行GC线程数,最大值为8,如果CPU数量超过8,则为CPU数量的5/8
"-XX:ParallelGCThreads"
设置并发标记阶段,并行执行的线程数:
"-XX:ConcGCThreads"
设置量清代与老年代的比率(Yong/Tenured),默认为2
“-XX:NewRatio”
设置Eden与survivor的比率(Eden/survivor),默认为8
“-XX:SurvivorRatio”
新生代保存到老生代的岁数
“-XX:MaxTenuringThreshold”
设置预留空间的空间百分比,以降低目标空间的溢出风险,默认为10%
“-XX:G1ReservePercent”
本章节是《深入理解java虚拟机》G1垃圾收集器的笔记,在下一章节 6java虚拟机-深入G1垃圾收集器 中,进行G1详细的讲解
9 ZGC 收集器
本章节引用博客:深入理解JVM - ZGC垃圾收集器,理解JVM垃圾收集器-ZGC,Java——七种垃圾收集器+JDK11最新ZGC
9.1 ZGC描述与特点
ZGC(Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器。它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器。在JDK 11新加入,还在实验阶段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms。
ZGC的使用:
-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC
9.2 动态Region
ZGC的Region可以具有如图所示的大、中、小三类容量:
- 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
- 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。·
- 大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,最小容量可低至4MB,所有大型Region可能小于中型Region。大型Region在ZGC的实现中是不会被重分配的,因为复制一个大对象的代价非常高昂。
9.3 染色指针技术
9.3.1 HotSpot虚拟机的标记三种方案
实现方案有如下三种:
- 把标记直接记录在对象头上(如Serial收集器);
- 把标记记录在与对象相互独立的数据结构上(如G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息);
- 直接把标记信息记在引用对象的指针上(如ZGC)
9.3.2 染色指针
为什么放到指针上:
染色指针是一种直接将少量额外的信息存储在指针上的技术。目前在Linux下64位的操作系统中高18位是不能用来寻址的,但是剩余的46为却可以支持64T的空间,到目前为止我们几乎还用不到这么多内存。于是ZGC将46位中的高4位取出,用来存储4个标志位,剩余的42位可以支持4T的内存,如图所示:
- Linux下64位指针的高18位不能用来寻址,所有不能使用;
- Finalizable:表示是否只能通过finalize()方法才能被访问到,其他途径不行;
- Remapped:表示是否进入了重分配集(即被移动过);
- Marked1、Marked0:表示对象的三色标记状态;
- 最后42用来存对象地址,最大支持4T;
9.3.3 染色指针的三大优势
- 一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。而Shenandoah需要等到更新阶段结束才能释放回收集中的Region,如果Region里面对象都存活的时候,需要1:1的空间才能完成收集。
- 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC只使用了读屏障。
- 染色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。
9.4 三色标记
9.4.1 三色标记
在并发的可达性分析算法中我们使用三色标记(Tri-color Marking)来标记对象是否被收集器访问过:
- 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
可达性分析的扫描过程,其实就是一股以灰色为波峰的波纹从黑向白推进的过程,但是在并发的推进过程中会产生“对象消失”的问题,如图:
9.4.1 对象消失(错标,漏标问题)
对象消失理论,只有同时满足才会发生对象消失:
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用;
要解决对象消失问题只需要破坏其中一条就行了,目前常用有两种方案:
- 增量更新(Incremental Update):增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
- 原始快照(Snapshot At TheBeginning,SATB):原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。
9.5 内存多重映射
ZGC使用了内存多重映射(Multi-Mapping)将多个不同的虚拟内存地址映射到同一个物理内存地址上,这是一种多对一映射,意味着ZGC在虚拟内存中看到的地址空间要比实际的堆内存容量来得更大。
因为染色指针只是重新定义内存中某些指针的其中几位,OS又不支持,OS只会把整个指针当做一个内存地址来对待,只是它自己瞎想,为了解决这个问题,使用了现代处理器的虚拟内存映射技术。
现代处理器一般使用请求分页机制+虚拟内存映射技术:请求分页机制把线性地址空间和物理地址空间分别划分为大小相等的块。这样的块称为页。通过在线性虚拟空间的页和物理地址空间的页建立映射表,分页机制会进行线性地址到物理地址的映射,完成线性地址到物理地址的转换。
把染色指针中的标志位看作是地址的分段符,那只要将这些不同的地址段都映射到同一个物理内存空间,经过多重映射转换后,就可以使用染色指针正常进行寻址了,效果如图:
ZGC的多重映射只是它采用染色指针技术的伴生产物
9.6 读屏障Load Barrier
当对象从堆中加载的时候,就会使用到读屏障(Load Barrier)。这里使用读屏障的主要作用就是检查指针上的三色标记位,根据标记位判断出对象是否被移动过,如果没有可以直接访问,如果移动过就需要进行“自愈”(对象访问会变慢,但也只会有一次变慢),当“自愈”完成后,后续访问就不会变慢了。
读写屏障可以理解成对象访问的“AOP”操作
9.7 ZGC运作过程
ZGC的运作过程大致可划分为以下四个大的阶段:
9.7.1 并发标记
并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记和最终标记(ZGC中就是名字不同而已)的短暂的停顿,整个标记阶段只会更新染色指针中的Marked 0、Marked 1标志位。
- 停顿时间和堆大小无关,只和GC Roots数量有关。
- 总结就是:并发标记阶段会有两个短暂STW。
- ZGC只有短暂的STW,大部分的过程都是和应用线程并发执行,比如最耗时的并发标记和并发移动过程。
9.7.2 并发预备重分配
并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
- ZGC的重分配集只是决定里面的存活对象会被复制到其他的Region。不是为了效益回收。
- JDK12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段完成的。
9.7.3 并发重分配
并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。
- ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。
- ZGC的染色指针因为“自愈”(Self-Healing)能力,所以只有第一次访问旧对象会变慢,而Shenandoah的Brooks转发指针是每次都会变慢。 一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配,但是转发表还得留着不能释放掉,因为可能还有访问在使用这个转发表。
- 举例如:因为在标记和移动过程中,GC线程和应用线程是并发执行的,所以存在这种情况:对象A内部的引用所指的对象B在标记或者移动状态,为了保证应用线程拿到的B对象是对的,那么在读取B的指针时会经过一个 “load barriers” 读屏障,这个屏障可以保证在执行GC时,数据读取的正确性。
9.7.4 并发重映射
并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。
9.8 ZGC优点
1. 低停顿,高吞吐量,ZGC收集过程中额外耗费的内存小。
- 低停顿,几乎所有过程都是并发的,只有短暂的STW。
- 内存小,ZGC没有写屏障,卡表之类的。
- 吞吐量方面,在ZGC的‘弱项’吞吐量方面,因为和用户线程并发,还是有影响的。但是!但是!,以低延迟为首要目标的ZGC已经达到了以高吞吐量为目标Parallel Scavenge的99%,直接超越了G1。
2 .G1通过写屏障维护记忆集,才能处理跨代指针,得以实现增量回收。记忆集占用大量内存,写屏障对正常程序造成额外负担。
3. 在多核处理器的某种架构下,ZGC优先在线程当前所处的处理器的本地内存上分配对象,以保证内存高效访问。
4. 并发停顿方面:ZGC只有短暂的STW,大部分的过程都是和应用线程并发执行,比如最耗时的并发标记和并发移动过程。
5. ZGC中没有引入分代,也就没有新生代和老年代的概念,只有一块一块的内存区域page,以page单位进行对象的分配和回收。
6. 并发的标记-整理算法。没有内存碎片。
9.9 ZGC的缺点
1. 承受的对象分配速率不会太高,因为浮动垃圾。
- ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。
- 造成回收到的内存空间小于期间并发产生的浮动垃圾所占的空间。
ZGC没有分代概念,每次都需要进行全堆扫描,导致一些“朝生夕死”的对象没能及时的被回收。所以就不存在Young GC、Old GC,所有的GC行为都是Full GC。
2. ZGC目前只在Linux/x64上可用。
10 Shenandoah垃圾收集器
Shenandoah是一款只有OpenJDK才会包含的收集器,最开始由RedHat公司独立发展后来贡献给了OpenJDK,相比G1主要改进点在于:
- 支持并发的整理算法,Shenandoah的回收阶段可以和用户线程并发执行;
- Shenandoah 目前不使用分代收集,也就是没有年轻代老年代的概念在里面了;
- Shenandoah 摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。
10.1 Shenandoah收集器的工作过程
Shenandoah收集器的工作过程一共有九个阶段,下图只画了最核心的三个阶段并发标记、并发回收、并发引用更新。
- 初始标记(Initial Marking):与G1一样,只标记与GC Roots直接关联的对象,这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。
- 并发标记(Concurrent Marking) :与G1一样,从GC Root开始对堆中对象进行可达性分析,找出存活的对象,可与用户线程并发执行,不会造成停顿,时间的长度取决于堆中存活对象的数量和对象图的结构复杂度。
- 最终标记(Final Marking):与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(Collection Set),会有一小段短暂的停顿。
- 并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region)。
- 并发回收(Concurrent Evacuation) :首先把回收集里面的存活对象先复制一份到其他未被使用的Region之中,然后通过读屏障和Brooks Pointers转发指针技术来解决在垃圾回收期间用户线程继续读写被移动对象的问题,并发回收阶段运行的时间长短取决于回收集的大小。
- 初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的停顿。
- 并发引用更新(Concurrent Update Reference) :真正开始进行引用更新操作,这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。
- 最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于GCRoots中的引用。会产生一个非常短暂的停顿,停顿时间只与GC Roots的数量相关。
- 并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,所以最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。
10.2 连接矩阵
连接矩阵可以简单理解为一张二维表格,如果Region N有对象指向RegionM,就在表格的N行M列中打上一个标记,如图所示,如果Region 5中的对象Baz引用了Region 3的Foo,Foo又引用了Region 1的Bar,那连接矩阵中的5行3列、3行1列就应该被打上标记。在回收时通过这张表格就可以得出哪些Region之间产生了跨代引用。
10.3 Brooks Pointer 转发指针技术
复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进行的话,就变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。Brooks Pointer 转发指针技术是来实现对象移动与用户程序并发的一种解决方案。
Brooks 在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己(类似句柄,一个是放在句柄池中,一个是放在对象头前面),如图:
在对象移动的时候我们只需要将Brooks Pointer 指向新对象,在对象访问过程中,只通一条mov指令就可以完成对新对象的访问了,如图:
当写操作发生时,Shenandoah收集器是通过CAS(Compare And Swap)操作,来保证收集器线程或者用户线程只有其中之一可以进行修改操作,以此来保证并发时对象访问的正确性。
优缺点
优点:延迟低
缺点:高运行负担使得吞吐量下降;使用大量的读写屏障,尤其是读屏障,增大了系统的性能开销;
11 Epsilon 垃圾收集器
使用:
-XX:+UnlockExperimentalVMOptions
-XX:+UseEpsilonGC
特点:
一个处理内存分配但不实现任何实际内存回收机制的GC,当堆内存空间就不够时,自动触发与OutOfMemoryError
相关的处理。一个不回收垃圾的垃圾回收器看起来很蠢,实际上在不少情况下都有着它的妙用
11.1 性能调优
在进行性能调优时,通常的做法是以当前的应用作为基础,运行性能测试之后得到相关的性能数据,以这个数据作为基准(baseline)。接着尝试修改某个参数,再运行同样的性能测试,得到新的性能数据,再与之前得到的基准值进行比较,从而判断该参数对性能的影响。这样做有一个重要的前提,那就是两次测试的结果应该只与这个参数的调整有关。如果有其他变量的存在,就无法建立性能差异与参数值之间的关联关系。而Java应用中的GC操作是不可控的。两次测试中GC操作造成的暂停时间,都会导致结果发生变化,会造成错误的判断。
这就体现出Epsilon的价值。因为Epsilon没有任何GC的动作,就排除了GC操作的干扰。这对性能测试的准确性是大有好处的。
11.2 测试内存分配
由于垃圾回收器的存在,大多数时候我们并不关注内存的分配,只是不断的创建对象,而依赖垃圾回收器完成对不使用的对象的回收。所以我们对于应用所分配的内存的上限并没有一个准确的认知。
如果你的应用需要对内存的分配进行限制,比如运行在资源受限的系统上,那么可以使用Epsilon并设置堆内存的最大值。这样可以测试应用是否满足这样的限制。如果不满足,那么应用会直接退出。
11.3 耗时短暂的任务
有些应用在设计时就只存在极短的时间。比如,一些定期运行的任务在启动之后只是执行一些简单的操作,然后就马上退出。对于这样的应用,进行GC操作是没有意义的。因为在JVM进程退出之后,它所占用的内存会自动被操作系统回收。可以通过Epsilon来在运行时停止GC操作。这样可以进一步降低任务的执行时间。
11.4 要求极低延迟的任务
有些应用对处理延迟有苛刻的要求。由于GC操作带来的暂停,对于这样的应用来说是不能接受的。通过Epsilon可以避免GC操作带来的暂停。这相当于用内存换取执行时间。
3. 总结
收集器名称 | 工作区域 | 算法 | 开启参数 | 配合对象 | 线程 | 并行 | 并发 | 适用场合 | 优缺点 | 版本 |
---|---|---|---|---|---|---|---|---|---|---|
Serial | 新生代 | 复制算法 | -XX:+UseSerialGC | CMS;Serial Old | 单 | 否 | 否 | 单CPU;Client模式下 | 缺:stop the world;优:简单高效,没有线程交互开销,专注于GC; | jdk1 |
ParNew | 新生代 | 复制算法 | -XX:+UseParNewGC或-XX:+UseConcMarkSweepGC | CMS;Serial Old | 多 | 是 | 否 | server首选、jdk6之前,老年代用CMS,新生代只能用Serial或ParNew | 缺:stop the world 优:并行GC | |
Parallel Scavenge | 新生代 | 复制算法 | -XX:+UseParallelGC或-XX:+UseParallelOldGC | Parallel Old;Serial Old | 多 | 是 | 否 | 关注高吞吐量并且响应性要求不高的应用 | 搭配ParNew使用,或者在jdk6之前搭配Parallel Scavenge使用,也可在server模式下作为CMS的备胎(CMS可能会因为浮动垃圾而发生concurrent mode failure的错误,此时需要serial old上位) | JDK1.4 |
Serial Old | 老年代 | 标记-整理 | -XX:+UseSerialGC或-XX:+UseParNewGC | Serial;Parallel Scavenge | 单 | 否 | 否 | Serial Old是Serial垃圾收集器年老代版本,运行在Client默认的java虚拟机默认的年老代垃圾收集器 | 搭配ParNew使用,或者在jdk6之前搭配Parallel Scavenge使用,也可在server模式下作为CMS的备胎(CMS可能会因为浮动垃圾而发生concurrent mode failure的错误,此时需要serial old上位) | jdk1 |
Parallel Old | 老年代 | 标记-整理 | -XX:+UseParallelGC或-XX:+UseParallelOldGC | Serial;Parallel Scavenge | 多 | 是 | 否 | 注重吞吐量以及CPU资源的场合 | server模式默认选项。搭配Parallel Scavenge,关注吞吐量及cpu资源敏感的场合 | JDK1.6(1.8默认) |
CMS | 老年代 | 标记-清除 | -XX:+UseConcMarkSweepGC | Serial;ParNew | 多 | 是 | 是 | 最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验 | 优点:要求高响应性的互联网站或BS服务端。缺点:对cpu资源非常敏感;无法处理浮动垃圾,可能会出现Concurrent mode failure;标记清除算法容易产生垃圾碎片 | JDK1.5 |
G1 | 老年代,新生代 | 整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的 | -XX:+UseG1GC | G1管理整个堆空间 | 多 | 是 | 是 | G1是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。 | 并行与并发,分代收集,空间整合,可预测的停顿 | JDK7-u4。java9时作为默认GC |
ZGC | all | 动态Region内存布局,不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器 | -XX:+UnlockExperimentalVMOptions-XX:+UseZGC | 自己管理全部堆空间 | 多 | 是 | 是 | 回收TB级内存(最大4T),停顿时间不超过10ms | ZGC最大的问题是浮动垃圾;回收TB级内存,短暂停顿 | JDK 11 |
Shenandoah | all | 不使用分代收集,整理算法 | 自己管理全部堆空间 | 多 | 是 | 是 | 连接矩阵 | OpenJDK | ||
Epsilon | 一个处理内存分配但不实现任何实际内存回收机制的GC | -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC | 性能调优,测试内存分配,耗时短暂的任务,要求极低延迟的任务 | Release 11 |
内容参考:
《深入理解java虚拟机》
《https://blog.csdn.net/CrankZ/article/details/86009279》
《https://xiaolyuh.blog.csdn.net/article/details/103935465》
《https://segmentfault.com/a/1190000021711902》
《https://blog.csdn.net/ld3205/article/details/90640967》
《https://zhuanlan.zhihu.com/p/100856595》
《https://www.cnblogs.com/fx-blog/p/11776415.html》
......