JVM垃圾回收器 Posted 2021-03-04 yelao
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM垃圾回收器相关的知识,希望对你有一定的参考价值。
七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。
并行(Parallel): 并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态 。
并发(Concurrent): 并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行 。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响 。
低延迟、高吞吐量:
停顿时间越短 就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度 能提升用户体验;
高吞吐量 则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务 。
1. Serial收集器
这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
目前已经老而无用,食之无味,弃之可惜的“鸡肋”了,但事实上,迄今为止,它依然是HotSpot虚拟机运行在客户端模式 下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境 ,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销 ,专心做垃圾收集自然可以获得最高的单线程收集效率。 在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大 ,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
2. ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本 ,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。
它却是不少运行在服务端模式下 的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器 ,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作 。
所以自JDK 9开始 ,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代,甚至还取消了ParNew加Serial Old以及Serial加CMS这两组收集器组合的支持(其实原本也很少人这样使用),并直接取消了-XX:+UseParNewGC参数 ,这意味着ParNew和CMS从此只能互相搭配使用,再也没有其他收集器能够和它们配合了。读者也可以理解为从此以后,ParNew合并入CMS,成为它专门处理新生代的组成部
分。ParNew可以说是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器 。
ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果 ,甚至由于存在线程交互的开销,该收集器在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分之百保证超越Serial收集器。当然,随着可以被使用的处理器核心数量的增加,ParNew对于垃圾收集时系统资源的高效利用还是很有好处的 。它默认开启的收集线程数与处理器核心数量相同 ,在处理器核心非常多(譬如32个,现在CPU都是多核加超线程设计,服务器达到或超过32个逻辑核心的情况非常普遍)的环境中,可以使用-XX:ParallelGCThreads 参数来限制垃圾收集的线程数。
3. Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器……Parallel Scavenge的诸多特性从表面上看和ParNew非常相似 ,那它有什么特别之处呢?
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间 ,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量 (Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值:
如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量 ,分别是控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小 的-XX:GCTimeRatio 参数。-XX:MaxGCPauseMillis 参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价 换取的:系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
-XX:GCTimeRatio 参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。除上述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy 值得我们关注。这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小 (-Xmn )、Eden与Survivor区
的比例(-XX:SurvivorRatio )、晋升老年代对象大小(-XX:PretenureSizeThreshold )等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics) 。如果读者对于收集器运作不太了解,手工优化存在困难的话,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择。只需要把基本的内存数据设置好(如-Xmx 设置最大堆),然后使用-XX:MaxGCPauseMillis 参数(更关注最大停顿时间)或-XX:GCTimeRatio (更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。
4. Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式 下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用 ,另外一种就是作为CMS收集器发生失败时的后备预案 。
Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,并非直接调用Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现几乎是一样的 ,所以在官方的许多资料中都是直接以Serial Old代替PS MarkSweep进行讲解。
5. Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集 ,基于标记-整理 算法实现。这个收集器是直到JDK 6 时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial old(PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。由于老年代Serial Old收集器在服务端应用性能上的“拖累” ,使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一定比ParNew加CMS的组合来得优秀。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合 ,在注重吞吐量或者处理器资源较为稀缺的场合 ,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
6. CMS收集器
在JDK 5发布时,HotSpot推出了一款在强交互应用 中几乎可称为具有划时代意义的垃圾收集器——CMS收集器。这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了 让垃圾收集线程与用户线程(基本上)同时工作。
CMS收集器是
HotSpot虚拟机追求低停顿 的第一次成功尝试。但是它还远达不到完美的程度,至少有以下三个明显的缺点:
首先, CMS收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量 。CMS默认启动的回收线程数是(处理器核心数量+3)/4 ,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。
然后, 在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉 。这一部分垃圾就称为“浮动垃圾 ”。同样也是由于在垃圾收集阶段用户线程还需要持续运行 ,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集。如果在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccu-pancyFraction 的值来降低内存回收频率。到了JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure) ,这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集。
最后, 空间碎片过多,无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC 的情况。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection 开关参数(默认是开启的,此参数从JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理 过程,由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发 的。另外一个参数-XX:CMSFullGCsBefore-Compaction (此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后 ,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)。
7. Garbage First收集器
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部 收集的设计思路和基于Region 的内存布局形式。早在JDK 7刚刚确立项目目标、Oracle公司制定的JDK 7 RoadMap里面,G1收集器就被视作JDK 7 中HotSpot虚拟机的一项重要进化特征。到了JDK 8 Update 40的时候,G1提供并发的类卸载的支持,补全了其计划功能的最后一块拼图。
G1是一款主要面向服务端应用的垃圾收集器 。HotSpot开发团队最初赋予它的期望是(在比较长期的)未来可以替换掉JDK 5中发布的CMS收集器。现在这个期望目标已经实现过半了,JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合 ,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。如果对JDK 9及以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC 来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃。规划JDK 10功能目标时,HotSpot虚拟机提出了“统一垃圾收集器接口”,将内存回收的“行为”与“实现”进行分离 ,CMS以及其他收集器都重构成基于这套接口的一种实现。以此为基础,日后要移除或者加入某一款收集器,都会变得容易许多,风险也可以控制,这算是在为CMS退出历史舞台铺下最后的道路了。
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region) ,每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region中还有一类特殊的Humongous 区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象 即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为1MB~32MB,且应为2的N次幂 。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中 ,G1的大多数行为都把Humongous Region作为老年代 的一部分来进行看待。
G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍 ,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集 。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值 ,然后在后台维护一个优先级列表 ,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis 指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region ,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
G1收集器至少有(不限于)以下这些关键的细节问题 :
跨Region引用对象如何解决?每个Region都维护有自己的记忆集 ,数量大,占据10-20%的堆内存空间 。
收集线程与用户线程互不干扰地运行?通过原始快照 (SATB)算法来实现。G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针 ,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上 。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的 ,不纳入回收范围。如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。
怎样建立起可靠的停顿预测模型?用户通过-XX:MaxGCPauseMillis参数指定的停顿时间 。G1收集器的停顿预测模型是以衰减均值(Decaying Average) 为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。
运作过程大致可划分为以下四个步骤:
初始标记(Initial Marking): 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,这个阶段需要停顿线程,但耗时很短。
并发标记(Concurrent Marking): 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图 ,找出要回收的对象,与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录 下的在并发时有引用变动的对象。
最终标记(Final Marking): 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录 。
筛选回收(Live Data Counting and Evacuation): 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程 ,由多条收集器线程并行完成的。
G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量 ,所以才能担当起“全功能收集器”的重任与期望。
以上是关于JVM垃圾回收器的主要内容,如果未能解决你的问题,请参考以下文章
JVM垃圾回收器
JVM垃圾回收机制 (垃圾判断,垃圾回收算法,垃圾回收器,五种引用)jvm
JVM系列 - JVM垃圾回收器
JVM的垃圾回收机制 总结(垃圾收集回收算法垃圾回收器)
JVM垃圾回收优化实战-G1垃圾回收器
垃圾收集JVM的垃圾回收器有哪些特点?