概述
并发与并行
并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于 垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。
1. 按线程数分 按照线程数(用于垃圾回收的)可以分为串行垃圾回收器 和并行垃圾回收器 。
串行垃圾回收器:在同一时间段内只允许有一个cPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。 并行垃圾回收器:可以运用多个CPU同时执行垃圾回收。 2. 按工作模式分 按照按工作模式可以分为并发式垃圾回收器 和独占式垃圾回收器 。 并发式垃圾回收器:在同一时间段内只允许有一个cPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。 独占式垃圾回收器:可以运用多个CPU同时执行垃圾回收。
3. 按碎片处理方式分 按照按工作模式可以分为压缩式垃圾回收器 和非压缩式垃圾回收器 。
压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
7种经典的垃圾回收器
串行回收器: serial 、serial old 并行回收器: ParNew 、Parallel scavenge 、Parallel old 并发回收器: CMS 、G1
新生代收集器: serial、ParNew、Parallel scavenge; 老年代收集器: Serial old、Parallel old、CMS; 整堆收集器: G1;
垃圾回收器
Serial与Serial Old 回收器
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机 新生代收集器的唯一选择。Serial收集器是一个单线程 工作的收集器,在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。 Serial Old是Serial收集器的老年代版本。
Serial回收器采用复制算法、串行回收和“Stop The World” 机制 执行垃圾回收。 Serial Old回收器标记—压缩算法、串行回收和“Stop The World” 机制 执行垃圾回收。
ParNew 回收器
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。
Parallel 与Parallel Old 回收器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是 能够并行收集的多线程收集器。 和ParNew收集器不同,Parallel scavenge收集器的目标则是达到一个可控制的吞吐量 ,它也被称为吞吐量优先的垃圾收集器。
吞吐量:处理器用于运行用户代码的时间与处理器总消耗时间的比值。
高吞吐量可以最高效率地利用处理器资源,尽快完成程序的运算 任务,主要适合在后台运算而不需要太多交互的分析任务。
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实 现。
CMS回收器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
CMS收集器是基于标记-清除 算法实现的,它的运作可以分为四个步骤,包括:
初始标记 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快; 并发标记 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对 象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行; 重新标记 重 新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的 标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一 些,但也远比并发标记阶段的时间短; 并发清除 清理删除掉标记阶段判断的已经死亡的 对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
CMS收集器无法处理“浮动垃圾 ”,有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。 在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分 垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集 时再清理掉。这一部分垃圾就称为“浮动垃圾”。
同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待 到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
CMS是基于“标记-清除”算法实现的收集器,意味着收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦。
为什么不使用标记—压缩算法避免碎片?
因为当并发清除时,用标记—压缩整理内存的话,原来的用户线程使用的内存就无法使用。要保证用户线程继续执行,前提是它运行的资源不受影响。标记—压缩更适合“Stop The World”场景下使用。
G1(Garbage First)回收器
Garbage First开创了收集 器面向局部收集的设计思路和基于Region的内存布局形式,它是一款主要面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
G1收集器出现之前的所有其他收集器,垃圾收集的目标范围要么是整个新生代,要么就是整个老年代,再要么就是整个Java堆。而G1可以面向堆内存任何部分来组成回收集(Collection Set)进行回收,衡量标准不再是它属于哪个分代,而 是哪块内存中存放的垃圾数量最多,回收收益最大 。
G1回收器的特点
1. 并行与并发
并行性:G1在回收期间,可以有多个GC线程同时工作,此时用户线程Stop The World。 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。
2. 分代收集
G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。 收集器能够对扮演不同角色的 Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的 旧对象都能获取很好的收集效果。 Region中还有一类特殊的Humongous 区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。
3. 空间整合
G1在内存回收时以region作为基本单位,region之间使用复制算法,但整体上可看做是标记—压缩算法。
4. 可预测的停顿时间模型
G1收集器能建立可预测的停顿时间模型,它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免 在整个Java堆中进行全区域的垃圾收集。 G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值 ,然后在后台维护一 个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region。 这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获 取尽可能高的收集效率。 停顿预测模型是以衰减均值为理论基础来实现的,在垃圾收集过程中,G1收集器会记 录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得 出平均值、标准偏差、置信度等统计信息。然后通过这些信息预测现在开始回收的话,由 哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。
Region里面存在的跨Region引用对象如何解决?
使用记忆集避免全堆作为GC Roots扫描,它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一 种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。
G1收集器的运作过程
初始标记 (Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要 停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。并发标记 (Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。最终标记 (Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。筛选回收 (Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。
7种经典垃圾回收器的比较
垃圾回收器组合
低延迟垃圾收集器
Shenandoah收集器
Shenandoah也是使用基于Region的堆内存布局,同样 有着用于存放大对象的Humongous Region,默认的回收策略也同样是优先处理回收价值最大的 Region……但在管理堆内存方面,它与G1至少有三个明显的不同之处。
G1的回收阶段是可以多线程并行的,但却不能与用户线程并发,Shenandoah支持并发的整理算法。 默认不使用分代收集的,换言之,不会有 专门的新生代Region或者老年代Region的存在,没有实现分代,并不是说分代对Shenandoah没有价值, 这更多是出于性价比的权衡,基于工作量上的考虑而将其放到优先级较低的位置上。 Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降 低了伪共享问题的发生概率。连接矩阵可以简单理解为一张二维表格,如果Region N有 对象指向Region M,就在表格的N行M列中打上一个标记,在回收时通过这张表格就可以得出哪些Region之间产生了跨代引用。
Shenandoah收集器的工作过程大致可以划分为以下九个阶段:
初始标记 :与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍 是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。并发标记 :与G1一样,遍历对象图,标记出全部可达的对象,这个阶段 是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。最终标记 :与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值 最高的Region,将这些Region构成一组回收集。最终标记阶段也会有一小段短暂的停顿。并发清理 :这个阶段用于清理那些整个区域内连一个存活对象都没有找到 的Region。并发回收 :在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之 中。并发回收阶段运行的时间长短取决于回收集的大小。初始引用更新 :并发回收阶段复制对象结束后,还需要把堆中所有指 向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未 做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收 集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的 停顿。并发引用更新 :真正开始进行引用更新操作,这个阶段是与用户 线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它 不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为 新值即可。最终引用更新 :解决了堆中的引用更新后,还要修正存在于GC Roots 中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。并发清理 :经过并发回收和引用更新之后,整个回收集中所有的Region已 再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收 这些Region的内存空间,供以后新对象分配使用。
ZGC收集器
ZGC和Shenandoah的目标是高度相似的,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
ZGC收集器是一款基于Region内存布局的,(暂时) 不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低 延迟为首要目标的一款垃圾收集器。
ZGC也采用基于Region的堆内存布局,但 与它们不同的是,ZGC的Region具有动态性——动态创建和销毁,以及动态的区域容量大小。
ZGC的运作过程可分为四个阶段:
并发标记 (:与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的 阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的 短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、Shenandoah不同的是,ZGC 的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位。并发预备重分配 :这个阶段需要根据特定的查询条件统计得出 本次收集过程要清理哪些Region,将这些Region组成重分配集。并发重分配 :重分配是ZGC执行过程中的核心阶段,这个过程要把重分 配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。并发重映射 :重映射所做的就是修正整个堆中指向重分配集中旧对象的所 有引用。ZGC很巧妙地把并发重映射 阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所 有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后,原来记录新旧 对象关系的转发表就可以释放掉了。
更多详情,请参考《深入理解Java虚拟机第3版》一书。
选择合适的垃圾收集器
考虑以下三个问题:
应用程序的主要关注点是什么?
如果是数据分析、科学计算类的任务,目标是能尽快算出结果, 那吞吐量就是主要关注点; 如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务 超时,这样延迟就是主要关注点; 而如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是 不可忽视的。
运行应用的基础设施如何?
硬件规格,要涉及的系统架构是x86-32/64、SPARC还是 ARM/Aarch64; 处理器的数量多少,分配内存的大小; 选择的操作系统是Linux、Solaris还是Windows 等。
使用JDK的发行商是什么?版本号是多少?
是ZingJDK/Zulu、OracleJDK、Open-JDK、OpenJ9抑 或是其他公司的发行版?该JDK对应了《Java虚拟机规范》的哪个版本?
怎样选择垃圾回收器
优先调整堆的大小让JVM自适应完成。 如果内存小于100M,使用串行收集器 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒互联网应用),使用并发收集器