huan30

JVM 的垃圾回收器

经典垃圾收集器

 如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。
 这些经典的收集器尽管已经算不上是最先进的技术,但他们曾在实践中千锤百炼,足够成熟。

HotSpot虚拟机的垃圾收集器
HotSpot虚拟机的垃圾收集器

 图中展示了其中作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。
 ps:虽然垃圾收集器的技术在不断进步,但直到现在还没有最好的收集器出现,更加不存在"万能"的收集器,所以我们选择的只是对具体应用最合适的收集器。如果有一种放之四海而皆准、任何场景下都适用的完美收集器存在。HotSpot虚拟机完全没必要实现那么多种不同的收集器了

Serial 收集器

       Serial 收集器是最基础、历史最悠久的收集器,曾经(JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。从名字就可以猜出,这个收集器是一个**单线程**工作的收集器。 但它的 "单线程"的意义并不仅仅是说明它只会使用一个处理器
 或一条收集线程去完成垃圾收集工作,更重要的是强调它在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。"Stop The World" 这个词语也许听起来很酷,但这项工作是由*虚拟机在后台自动发起和自动完成的*,这对很多应用来说都是不能接受
 的,不妨试想一下,要是你的电脑每运行一小时就会暂停五分钟,你会有什么心情?

Serial/Serial Old 收集器运行示意图
Serial/Serial Old 收集器运行示意图

       虽然 Serial 收集器最早出现,但目前已经老而无用,食之无味,弃之可惜的"鸡肋",但事实上,它仍然时HotSpot虚拟机运行在客户端模式下的默认新生代收集器有着优于其他收集器的地方,那就是简单而高效,对于内存资源受限的环境,他是所有收集器里
 额外内存消耗最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集器自然可以获得最高的单线程收集效率。在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说
 并不会特别大,收集几十兆甚至一二百兆的新生代,垃圾收集器的停顿时间完全可以控制在十几,几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说完全是可以接受的。

ParNew 收集器

       ParNew 收集器实质上时Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括 Serial收集器可用的所有控制参数(例如 -XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法
 Stop The World、对象分配规则、回收策略等都完全一致。

ParNew/Serial Old收集器运行示意图
ParNew/Serial Old收集器运行示意图

       ParNew 收集器除了支持多线程并行收集之外,其他于Serial收集器相比并没有太多创新之处,但它确实不少运行在服务端模式下的HotSpot虚拟机,时JDK 7 之前遗留系统首选的新生代收集器,其中有一个与功能、性能无关但其实
 很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作
       ps:JDK 9 开始,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方细外它能够完全被 G1 所取代甚至还取消了ParNew 加 Serial Old 以及 Serial 加 CMS 这两组收集器组合的支持。
 也可以理解为ParNew 合并入CMS成为它专门处理新生代的组成部分。
       -XX:ParallelGCThreads 参数来限制垃圾收集的线程数

Parallel Scavenge 收集器

       Parallel Scavenge 收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。
       Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程地停顿时间,而PS 收集器地目标则时达到一个可控地吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时
 间与处理器总消耗时间地比值  即 吞吐量 = (运行用户代码时间)/(运行用户代码时间 + 运行垃圾收集时间) 
       如果虚拟机完成某个任务,用户代码加垃圾收集总耗费了100分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99% 。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提高用户体验;而高吞吐量则可以最高效率
 地利用处理器资源,尽快完成程序地运算任务。主要适合在后台运算而不需要太多交互地分析任务。
       Paraller Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio参数
             1.-XX:MaxGCPauseMillis 参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设置值。不过不要异想天开的认为如果把这个参数值设置的更小就可以使得系统垃圾收集速度变得更快,垃圾收集停顿时间
 是以牺牲吞吐量和新生代空间为代价换取的。
             2.-XX:GCTimeRatio 参数的值应时一个大于0 小于100 的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为 19 ,那允许的最大垃圾收集时间就占总时间的 5 % (即 1/(1+19)),默认值为99。
 即允许最大 1%(1/(1+99)) 的垃圾收集时间。
             3.Parallel Scavenge 收集器也经常被称作"吞吐量优先收集器",还有一个参数 -XX:UseAdaptiveSizePolicy 这是一个开关参数,激活之后就不需要人工指定新生代的大小(-Xmn)、Eden 与Survivor区的比例(-XX:SurvivorRatio)、
 晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息、动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾手机的自适应调节策略(GC Ergonomics)。
       只需要把基本的内存数据设置号(-Xmx最大堆)然后使用1,2 参数给虚拟机设置一个优化目标,那具体细节参数调节工作就有虚拟机完成。自适应调节也是 Parallel Scavenge收集器区别于 ParNew 收集器的一个重要特征。

Serial Old 收集器

       Serial Old 时Serial收集器的老年代版本,它同样是一个单线程收集器,<font  color="red">使用标记-整理算法。</font> 
 这个收集器的主要意义也是共客户端模式下的HotSpot虚拟机使用,
 如果在服务端模式下,它也可能有两种用途:
       1.在JDK 5 以及以前的版本种与 Parallel Scavenge收集器搭配使用。
       2.作为 CMS 收集器发生失败时的后备预案,在并发收集发生 Concurrent Mode Failure时使用。

Parallel Old 收集器

       Parallel Old 时Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于 **标记-整理算法** 实现。这个收集器时直到 JDK 6 时才开始提供的,"吞吐量优先"收集器终于有了比较名副其实的搭配组合。

CMS 收集器

简述及运行过程

       CMS (Concurrent Mark Sweep)收集器是一种 **以获取最短回收停顿时间** 为目标的收集器。非常适用于关注服务的响应时间,细外系统停顿时间尽可能短,以给用户带来很好的交互。
 基于 **标记-清除** 算法实现 整体过程分为四个步骤包括
       1.初始标记(CMS initial mark)   (STW) 仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
       2.并发标记(CMS concurrent mark)  从直接关联对象开始遍历整个对象图的过程,耗时较长但是不需要停顿用户线程
       3.重新标记(CMS remark)            (STW)是为了修正并发标记期间,因用户线程继续运行而导致标记产生的那一部分对象的标记记录
       4.并发清除(CMS concurrent sweep) 清理删除标记阶段判断的已经死亡的对象

CMS 收集器运行示意图
CMS 收集器运行示意图

CMS 优点及其缺点

优点
       CMS 是一款优秀的收集器,它最主要的优点在名字上已经体现出来:**并发收集、低停顿**
缺点
       CMS 收集器时HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的缺点:
CMS 对处理器资源非常敏感
       事实上,面向并发设计的程序都对处理器资源比较敏感,在并发阶段它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量。CMS 默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数
 在四个或以上,并发回收时垃圾收集线程只不过占用不超过25% 的处理器运算资源,并且会随处理器核心数量的增加而下降。但当处理器核心数量不足四个时,CMS 对用户程序的影响就肯恩变得很大。未来缓解这种情况,虚拟机提供了一种称为“增量式并
 发收集器”的CMS收集器变种,在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的时间。JDK 7 i-CMS模式已经被声明为"deprecated" JDK 9 被完全废弃
CMS 无法处理"浮动垃圾" 及 需要注意 CMS触发时间以及发生"并发失败"
       在CMS 的并发标记和并发清理阶段,用户线程还在继续运行的,程序在运行过程自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS 无法在当此收集种处理掉它们,只要留待下一次垃圾收集时再清理掉。
 这一部分垃圾就称为"浮动垃圾"。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此 CMS 收集器不能像其他收集器那样等待到老年代几乎完全被填满在进行收集,必须预留一部分空间供并发
 收集时的程序运作使用。
       JDK 5 的默认设置下,当老年代使用 68% 的空间后就会被激活。可以适当调用参数 -XX:CMSInitiatingOccu-pancyFraction 的值提高 CMS 出发百分比降低内存回收频率,获取更好的性能,到JDK 6时 CMS 启动阈值已经默认提升至 92% ,但
 这又会更容易面临另一种风险,要是 CMS 运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次 "并发失败"(Concurrent Mode Failure)这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old 收集器来重新进行
 老年代的垃圾收集,但这样停顿时间就很长了,所有上述参数设置太大将会很容易导致大量的并发失败产生,性能反而降低。
采用"标记-清除"算法造成空间碎片过多 造成大对象分配问题
       使用该算法意味着手机结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full FC 的情况。为了解决
 这个问题,CMS 收集器提供了一个 -XX:UseCMSCompactAtFullCollection 开关参数(默认开始 JDK9 开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于内存整理必须移动存活对象,是无法并发的。这样空间碎片的问
 题是解决了但停顿时间又会变长,因此还有一个参数 -XX:CMSFullGCsBeforeCompaction(默认值为0,每次进入Full GC 都进行碎片整理 ,JDK9 开始废弃),参数要求CMS收集器在执行若干次不整理空间的 Full GC后,下一次进入FullGC就会先进行碎
 片整理。

Garbage First 收集器

简述

       Garbage First(简称G1)收集器时垃圾收集器技术发展历史上里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形态,JDK 8 Update 40 的时候,G1提供并发的类卸载的支持,补全了其计划功能的最后一块拼图。
 这个版本的 G1 收集器才被Oracle 官方称为"全功能的垃圾收集器"(Full-Featured Garbage Collector)。
       G1 是一款主要面向**服务端应用**的垃圾收集器。JDK 9 发布之日,G1宣告取代Parallel Scavenge加Parallel Old 组合,成为服务端模式下的默认垃圾收集器,而CMS 则沦落至被声明伟不推荐使用(Deprecate)的收集器。

运行过程

       1.初始标记: 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可以用Region中分配新对象。这一阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,
 所有G1 收集器在这个阶段实际并没有额外的停顿
       2.并发标记: 从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找到要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
       3.最终标记: 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
       4.筛选回收: 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来指定回收计划,可以*选择任意多个Region构成会收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再
 清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,时必须暂停用户线程,由多条收集器线程并行完成的。

G1 收集器运行示意图

停顿时间模型

       作为CMS收集器的替代者和继承人,设计者们希望做出一款能够建立起"停顿时间模型"(Pause Prediction Mdel)的收集器,停顿时间模型的意思时能够支持指定在一个长度为 M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样
 的目标。那具体如何实现这个目标呢?首先要有一个思想上的改变,在G1 收集器出现之前的所有收集器,包括CMS在内,垃圾收集器的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),在要么就是整个Java堆(Full GC)。而G1跳出
 这个樊笼,它可以面向堆内存任何部分来组成会收集(Collection Set 一般简称CSet)进行回收。衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
Region
       G1 开创了基于Region 的堆内存布局是它能够实现这一目标的关键。虽然G1也仍遵循分代收集理论设计的。但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续额Java堆划分为多个大小
 相等的独立区域(Region),每个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是以及存活了一段时间、熬过多次收集的就对象>
 都能获取很好的收集效果。
Humongous(存储大对象)
       Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过一个Region容量的一半的对象即可判断为大对象。每个Region的大小可以通过参数 -XX:G1HeapRegionSize设置,取值范围为1MB~32MB,且应为2的N次幂。
 而面对那些超过整个Region容量的超级大对象,将会被存放在N个连续的HumongousRegion之中,G1中大多数行为都把Humongous Region作为老年代的一部分看待。

G1收集器Region示意图
G1收集器Region示意图

-XX:MaxGcPauseMillis
       G1虽然任然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的
 整数倍,这样可以有计划地避免在整个Java堆中尽心全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的"价值"大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据
 用户设置的允许的收集停顿时间(参数-XX:MaxGCPauseMillis指定,默认200毫秒),优先处理回收价值收益最大的那些Region,也就是"Garbage First"名字的由来。---这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在
 有限时间内获取尽可能高的收集效率。

G1 潜在问题

如何解决多Region存在的跨Region引用
       解决思路使用  [记忆集](https://www.cnblogs.com/huan30/p/14323091.html)  避免全堆作为 GC Roots扫描,在G1收集器上记忆集的应用其实要复杂很多,它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的
 指针并标记这些指针分别在那些卡页的范围之内。
       G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,理论存储的元素是卡表的索引号。这种"双向"的卡表结构(卡表是"我指向谁",这种结构还记录着"谁指向我")比原来的卡表实现起来更复杂,同时由于Region
 数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他传统垃圾收集器有着更高的内存占用负担。根据经验,G1至少要消耗大约相当于Java堆容量10%至20%的额外内存来维持收集器工作
并发标记阶段如何保证收集线程与用户线程互不干扰
       这里首先要解决的是用户线程改变对象引用关系时,必须保证其不能打破原本的对象图结构:CMS 收集器次啊用增量更新算法实现,而G1收集器则是通过原始快照(STAB)算法来实现。此外,垃圾收集堆用户线程的影响还体现在回收过程中新创建对象的
 内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region涉及了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置
 以上。G1收集器默认在这个地址上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。与CMS中"Concurrent Mode Failure"失败会导致Full GC 类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要*冻结用户线程执行,导致
 Full GC而产生长时间"Stop The World"
怎么建立起可靠的停顿预测模型
       用户通过-XX:MaxGCPauseMillis参数指定的停顿时间只意味着垃圾收集发生之前的期望值,但G1收集器要怎么做才能满足用户的期望呢?G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器
 会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。话句话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,有哪些
 Region组成的回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

G1 对比 CMS

优点
       指定最大停顿时间、分Region的内存布局、按收益动态确定回收集
       G1 整体采用 "标记-整理" 算法实现,运行期间不会产生内存空间碎片。垃圾收集完成之后能提供规整的可用内存。有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前出发下一次收集
缺点
       G1无论是为了垃圾收集产生的内存占用还是程序运行的额外执行负载都要比CMS要高
内存占用
       G1 和 CMS 都是用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个Region,无论扮演老年代还是新生代,都必须有一份卡表,这导致G1的记忆集可能会占用整个堆容量的20%乃至更多的内存空间;相比起来CMS的卡表就相当简单,只有唯一一份。
 而且只需要处理老年代到新生代的引用,反过来则不需要。
执行负载
       由于两个收集器各自的细节实现特点导致了用户程序运行时的负载会有不同,譬如:
 它们都是用到了写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样的卡表维护操作外,为了实现原始快照搜索算法,还需要使用写前屏障来跟踪并发时的指针变化情况。

低延迟垃圾收集器

       衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),三者最多可以同时达成其中的两项。

Shenandoah 收集器

相比G1 的改进

       1.支持并发的整理算法
       2.没有使用分代收集
       3.使用"连接矩阵"来记录跨Region的引用关系
运行过程九阶段
       初始标记: 首先标记与GC Root是直接关联的对象
       并发标记: 遍历对象图,标记出全部可达的对象
       最终标记: 处理剩余SATB扫描,并在这个阶段统计回收价值最高的Region,构成一组回收集
       并发清理: 用于清理那些整个区域内连一个存活对象都没有找到的Region
       并发回收: 将回收集里面的存活对象复制一份到其他未被使用的Region中。使用读屏障和转发指针应对并发问题
       初始引用更新: 并发回收结束后把堆中所有指向旧对象的引用修正到复制后的新地址。实际并未做什么,只是为了建立一个线程集合点确保上步任务完成
       并发引用更新: 真正开始进行引用更新操作。
       最终引用更新: 修正GC Roots中的引用
       并发清理: 整个回收集中所有Region再无存活对象,直接清空以便之后使用

*s Pointer 实现对象移动与用户程序并发的一种解决方案

ZGC 收集器

特点

内存布局
       ZGC的Region具有动态性---动态创建和销毁,以及动态的区域容量大小。
       小型Region: 容量固定为2MB,用于放置小于256KB的小对象
       中型Region: 容量固定32MB,用于放置大于等于256KB但小于4MB的对象
       大型Region: 容量不固定,可以动态变化,但必须是2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象。因为复制一个大对象的代价非常高昂,所有不会被重分配,
并发整理算法的实现
染色指针

染色指针

运行过程

       并发标记: ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1 标志位。
       并发预备重分配: 这个阶段需要根据特定的查询条件统计得出本次收集过程中要清理那些Region将这些Region组成重分配集。
       并发重分配: 把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从就对象到新对象的转向关系。
       并发重映射: 修正整个堆中指向重分配集中旧对象的所有引用。

分类:

JVM

技术点:

相关文章: