经典垃圾回收器
Posted 烧脑IT
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了经典垃圾回收器相关的知识,希望对你有一定的参考价值。
常见的垃圾回收器种类:
新生代垃圾回收器
Serial
ParNew
Parallel Scavenge
老年代垃圾回收器
Serial Old
Parallel Old
CMS
新生代和老年代垃圾回收器
G1
上图展示了七种作用于不同分代的收集器, 如果两个收集器之间存在连线, 就说明它们可以搭配使用。
前置名称规范:
并发:并行描述的是多条垃圾收集器线程之间的关系。
并行:并发描述的是垃圾收集器线程与用户线程之间的关系。
Serial收集器
Serial收集器运行示意图,如下:
如上所示,当用户线程都执行到了安全点时,暂停用户线程,并且会唤醒GC线程进行垃圾回收,收集完成后,停止GC 线程,开启用户线程。
Serial 收集器是一款适用于新生代的单线程垃圾回收器,采用了标记-复制算法。
这里的单线程的含义:
1. 并不是意味这当前GC 线程的工作线程只有一个,而是强调当GC 线程工作的时候,用户线程必须停止,也就是我们常说的STW(stop the world)
适用场景:
Client模式
系统资源少的的场景
优点:
简单高效
占用资源少
单核处理器回收效率高
缺点:
需要stop the world,且时间相对较长。
可以用 -XX:+UserSerialGC 来选择 Serial 作为新生代收集器
ParNew收集器
ParNew收集器的运行示意图如下:
ParNew收集器可以理解是Serial收集器的多线程版本,其实和Serial并没有很多的区别,如上所示,ParNew收集器运行示意图中,当用户线程到达安全点的时候,会暂停所有的用户线程,并且启动GC 线程,默认情况下,GC 线程数和CPU数量一致。
1. 可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
2. 由于上下文切换开销,ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果。
ParNew是一款新生代的多线程垃圾回收器,采用了标记-复制算法
使用场景:
服务端应用
与CMS垃圾回收器搭配使用
Parallel Scavenge收集器
Parallel Scavenge收集器的运行示意图如下:
Parallel Scavenge收集器在运行效果上类似于ParNew收集器。
Parallel Scavenge是一款新生代的垃圾回收器,同样也是采用了标记-复制算法。
那么和ParNew 收集器的区别是什么呢?
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,如:
CMS等收集器关注用户线程的停顿时间
Parallel Scavenge收集器关注可控制的吞吐量
所谓吞吐量,即:
吞吐量 = 运行用户代代码时间/(运行用户代码时间+垃圾收集时间)
比如:虚拟完成某个任务花费了100分钟(用户代码时间+垃圾收集时间),其中垃圾收集花费了1分钟,那么吞吐量为99%。
与考虑用户线程停顿时间的收集器的区别是什么呢?
CMS等垃圾回收器:停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验。
Parallel Scavenge收集器:高吞吐量则可以最高效率地利用处理器资源, 尽快完成程序的运算任务。
所以说,Parallel Scavenge收集器适合的场景为:
注重吞吐量
高效利用CPU
高效运算且无需交互
那么如何控制吞吐量呢?
可以使用参数来精确控制吞吐量:
-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间, 一个大于0的毫秒数
-XX:GCTimeRatio:直接设置吞吐量大小, 一个大于0小于100的整数
备注:
JDK8的默认新生代垃圾回收器:
Serial Old收集器
Serial Old垃圾收集器的运行示意图如下:
Serial Old是Serial收集器的老年代版本, 它同样是一个单线程收集器, 使用标记-整理算法。
使用场景:
客户端模式下,HotSpot虚拟机使用
服务端模式下, 有两种用途:
与Parallel Scavenge收集器搭配使用
作为CMS 收集器发生失败时的后备预案, 在并发收集发生Concurrent Mode Failure时使用。
Parallel Old收集器
Parallel Old的运行示意图如下:
Parallel Old是Parallel Scavenge收集器的老年代版本, 支持多线程并发收集, 基于标记-整理算法。
Parallel Old收集器出现后, “吞吐量优先”收集器终于有了比较名副其实的搭配组合(在此之前只能搭配Serial old 使用), 在注重吞吐量或者处理器资源较为稀缺的场合, 都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
CMS收集器
下图是CMS垃圾回收器的运行示意图:
CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。
CMS收集器是基于标记-清除算法实现,主要的流程分为:
初始标记:标记出GC root 直接关联的对象,速度很快,需要Stop the world。
并发标记:根据第一步标记出的直接关联对象进行搜素标记其他对象,这个阶段耗时较长,但是可以与用户线程并发执行。
重新标记:修正并发标记过程中,由于用户线程修改导致的的一部分对象(采用增量更新的方式)。
并发清除:清理删除掉标记阶段判断的已经死亡的对象,由于是采用标记-清除算法,所以不需要移动对象,即可以与用户线程并发执行。
从上面的步奏来看,最耗时的阶段是并发标记和并发清除,但是可以与用户线程并发执行,所以说达到低停顿的目的。
那么是不是就没缺点了呢?
CMS至少有三个缺点:
对处理器资源非常敏感,并发阶段抢占用户线程资源,导致总吞吐量下降, 默认启动的回收线程数是总线程数的3/4。
无法处理“浮动垃圾”可能导致 Concurrent Mode Failure异常,进而Full gc
浮动垃圾产生:由于并发阶段(并发标记和并发清除),用户线程还在执行,即垃圾还在产生。
Concurrent Mode Failure异常原因:并发阶段垃圾,用户线程还在执行,需要为其预留空间,浮动垃圾产生可能填满用户运行空间,导致执行失败,进而使用Serial Old进行垃圾收集,进入Full gc。
空间碎片化, 基于“标记-清除”算法固有问题,导致给大对象分配内存较难,进而进入Full GC
解决办法: CMS收集器在执行过若干次不整理空间的Full GC之后, 下一次进入Full GC前会先进行碎片整理。使用参数:-XX:CMSFullGCsBeforeCompaction 设置
其他缺点:
需要全量扫描对象:CMS虽然是老年代的gc,但仍要扫描新生代来确认老年代存活的对象。
重点:全量扫描年轻代和老年代岂不是很慢?那我们如何解决呢?
解决办法:
新生代:Minor GC,新生代垃圾回收之后,存活的对象很少,如果在扫描新生代之前进行一次minor GC,那么整个情况就会好很多。
老年代:卡表,新生代指向了老年代那些地方存在跨带引用,我们只需要扫描卡表即可得出老年代引用新生代。
Garbage First(G1)
G1垃圾回收器的运行示意图如下:
如上所示, G1收集器的运作过程大致可划分为以下四个步骤:
初始标记:
标记一下GC Roots能直接关联到的对象。
修改指针(指针的意义,下面描述)的值, 让下一阶段用户线程并发运行时, 能正确地在可用的Region中分配新对象。
并发标记:
从GC Root开始对堆中对象进行可达性分析, 可与用户程序并发执行。
处理SATB记录下的在并发时有引用变动的对象
最终标记:
处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
筛选回收:
更新Region的统计数据, 对各个Region的回收价值和成本进行排序。
制定回收计划。
决定回收的那一部分Region的存活对象复制到空的Region。
理掉整个旧Region的全部空间。
上述过程直接中,只有并发标记可以通用户线程一起执行,其他的都需要停止用户线程。所以说G1的目的并不是一味的追求低延迟,而是在保证延迟可控的情况下,能带来最大的吞吐量。
G1的意义
Garbage First(简称G1) 收集器开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
那什么是面向局部收集呢?
其他收集器, 包括CMS在内, 垃圾收集的目标范围要么是整个新生代(Minor GC) , 要么就是整个老年代(Major GC) , 再要么就是整个Java堆(Full GC)。 而G1跳出了这个樊笼, 它可以面向堆内存任何部分来组成回收集 (Collection Set, 一般简称CSet) 进行回收,衡量标准变为了 不再是它属于哪个分代, 而是哪块内存中存放的垃圾数量最多, 回收收益最大。
其中基于Region的内存布局是实现面向局部收集的关键。
那什么是基于Region的内存布局呢?
G1不再坚持固定大小以及固定数量的分代区域划分, 而是把连续的Java堆划分为多个大小相等的独立区域(Region)。
每个Region的大小可以通过参数-XX:G1HeapRegionSize设定, 取值范围为1MB~32MB, 且应为2的N次幂。
每一个Region都可以根据需要, 扮演新生代的Eden空间、 Survivor空间, 或者老年代空间, 对扮演不同角色的Region采用不同的策略去处理。
Region中还有一类特殊的Humongous区域, 专门用来存储大对象
G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。
所以最终G1面向局部收集的Region内存布局如下图所示:
那么这样设计的目的是什么呢?
设计者希望建立一款能够建立起“停顿时间模型”(PausePrediction Model) 的收集器, 停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内, 消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。
为什么这样的内存设计可以建立停顿时间模型呢?
主要有以下原因:
将Region作为单次回收的最小单元, 即每次收集到的内存空间都是Region大小的整数倍, 这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小, 价值即回收所获得的空间大小以及回收所需时间的经验值, 然后在后台维护一个优先级列表, 每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定, 默认值是200毫秒) , 优先处理回收价值收益最大的那些Region。
G1需要解决的问题主要有:
Region里面存在的跨Region引用对象如何解决?
解决思路:使用记忆集避免全堆作为GC Roots扫描,每个Region都维护有自己的记忆集, 这些记忆集会记录下别的Region指向自己的指针。
结果:
由于Region数量比传统收集器的分代数量明显要多得多, 因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。
并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
主要问题及解决办法:
并发标记,导致的引用关系被破坏。CMS收集器采用增量更新算法实现, 而G1收集器则是通过原始快照(SATB) 算法来实现的。
回收过程中新创建对象的内存分配。 G1为每一个Region设计了两个指针, 把Region中的一部分空间划分出来用于并发回收过程中的新对象分配。 如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行, 导致Full GC而产生长时间“Stop The World”。
怎样建立起可靠的停顿预测模型?
用户通过-XX:MaxGCPauseMillis参数指定的停顿时间
只意味着垃圾收集发生之前的期望值, 但G1收集器要怎么做才能满足用户的期望呢?
G1收集器会记录每个Region的回收耗时、 每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,然后维护一个优先级列表,根据停顿时间来决定回收的区域。
注意;
不能将停顿时间设置太小,否则可能导致平凡的Full gc。通常情况下100-200ms是比较合适的。
G1的GC 模式:
Young GC:回收eden区域以及Survivor区域,并将活的对象复制到Old区域和其他的Survivor区域。
Mix GC:回收整个Young 区域+一部分Old区域
G1没有Full GC 的概念,需要Full gc 则调用Serial Old进行全堆扫描。
适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。可以用 -XX:+UseG1GC 使用 G1 收集器,jdk9 默认使用 G1 收集器。
其他:
为什么新生代不使用:标记-整理算法:
https://blog.csdn.net/weixin_41539756/article/details/95797617
— 完 —
原创内容,未经账号授权,禁止随意转载。
点这里 以上是关于经典垃圾回收器的主要内容,如果未能解决你的问题,请参考以下文章