JVM垃圾收集器(二)

目录

1、Serial

2、ParNew

3、Parallel Scavenge

4、Serial Old

5、Parallel Old

6、CMS

1、为什么需要两次“stop the world”

2、CMS的并发带来的问题

3、CMS的触发时机

4、CMS的缺陷

5、为什么CMS用清除算法

7、G1

1、Region

2、设计Region的意义

3、G1的三种模式

4、Mixed GC的运行过程

5、Card Table

6、三色标记法

7、STAB

8、G1的特点

8、JDK 默认垃圾收集器


规范中并未规定垃圾收集器如何实现,所以不同的虚拟机都提供了各种收集器,让用户根据需求组合使用。

HotSpot的各种收集器:

两个收集器之间存在连线,表示它们可以搭配使用。提供这么多种组合,还是因为每款收集器各有优劣,至今没有十全十美的收集器。

1、Serial

这是最基础的收集器,用于新生代,名字的意思是“串行”。

这是一个单线程工作的收集器,单线程指的是它只使用一条线程来进行垃圾收集。

标记-复制算法。

它的原理和流程比较简单,但由于停止线程这个动作是虚拟机主动发起的,用户线程不可知,突然就被停掉,不是很好。

后续垃圾收集器的一大目标,就是缩短用户线程的停顿时间,但始终没有办法不停顿。

2、ParNew

这是Serial的多线程并行版本,默认开启的收集线程数和cpu数量一样,可以同时使用多条线程进行并行的垃圾收集。

除了Serial外,只有ParNew能与CMS配合工作。CMS具有划时代意义,后来ParNew可以视为合并成了CMS的专用新生代收集器。

标记-复制算法。

3、Parallel Scavenge

也是针对新生代,支持多线程并行收集,特点是关注点不同。

  • CMS等收集器的关注点是,尽可能缩短垃圾回收时用户线程的停顿时间

  • 而Parallel Scavenge的关注点是,保证一个可控制的吞吐量,保证用户体验。

    (这里的吞吐量指的是,单位时间内(运行用户代码+进行垃圾收集),用户代码执行时间的占比)

由于关注吞吐量,所以Parallel Scavenge也被称为“吞吐量优先收集器”。        

适合注重吞吐量,或者处理器资源稀缺的场景。

标记-复制算法。

4、Serial Old

这是Serial的老年代版本,也是单线程的,使用标记-整理算法。

5、Parallel Old

这是Parallel Scavenge的老年代版本,支持多线程并发收集,使用标记-整理算法。

6、CMS

CMS(Concurrent Mark Sweep)是一种以达到“最短回收停顿时间”为目标的收集器。适合关注响应速度的服务器。

它比一般的标记-清除算法要复杂一些,分为以下4个阶段:

  • 初始标记:Stop The World,标记GC Roots能直接关联到的对象,比较快。
  • 并发标记:执行GC Roots跟踪标记过程,可以和用户线程并发执行,不需要暂停用户线程。
  • 重新标记:Stop The World,对标记期间产生的对象存活性的再次判断,修正对这些对象的标记,执行时间相对并发标记短。
  • 并发清除:清除对象,可以和用户线程并发执行。

CMS是清理老年代的

1、每阶段做的事情

初始标记

工作模式:JDK7之前单线程,JDK8之后多线程

目的:标记存活对象

包括两部分:

  • 标记老年代中GC Roots能直接关联到的对象
  • 标记新生代中还存活的,包含指向老年代的引用的对象,即额外考虑新生代对老年代的跨代引用

此阶段会stw,为了缩短停顿时间,可以开启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled,同时调大并发标记的线程数,不过不要超过CPU的核数

并发标记

目的:顺着初始标记阶段标记出的存活对象,找出所有的存活对象

因为是和用户线程并发执行,所以期间可能发生这些动作:

  • 新生代对象晋升到老年代
  • 直接在老年代分配大对象
  • 老年代对象之间的引用关系发生变化

对于这些对象,都需要重新标记当前的最新状态,否则就可能漏标存活对象。

为了提高重新标记的效率,避免重新扫描整个老年代,此阶段在发现上述行为后,会把该对象所在的card标记为脏卡,后续只需要扫描所有脏卡来处理。

因此,并发标记阶段完成后,老年代的所有存活对象并不会都被标记,还有一部分以脏卡的形式被记录,等待后续处理。

预清理阶段

目的:扫描所有脏卡,检查脏卡内所有对象的引用关系,标记存活对象

可终止的预处理

目的:这个阶段去尝试进行重新标记阶段的工作,因为重新标记阶段会stw,这样能减少一些停顿时间

此阶段最大持续时间为5秒,因为它期待这5秒内能发生一次young gc,清理新生代,进而减少下阶段扫描跨代引用的时间

重新标记

目的:完成对整个老年代的所有存活对象的标记,stw

用到三色标记里的增量更新算法做重新标记

调优:

这个阶段虽然目的是标记老年代,但是需要扫描整个堆,因为新生代可能存在对老年代的跨代引用。

为了以高效率,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前进行一次young gc。

这样,只需要扫描较小的新生代,大大提高效率。

并发清理

1、为什么需要两次“stop the world”

在“初始标记”阶段,CMS会快速扫描一下能和GC Roots直接关联到的对象,之后就会解除暂停。

之后和用户线程并发执行,进行对象的可达性分析。

但是,在用户线程执行的过程中,引用关系很可能产生了变化,于是就再次stop the world,修改这些引用发生变化的对象的标记。

比如,一个对象在第一次被判断为了“死亡”,之后用户线程又重新与它建立了引用关系,那么第二次会将它修改为“存活”。

注意:未被“初始标记”阶段标记的对象,在“重新标记”阶段不会被标记为垃圾对象。

这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的“并发标记”和“并发清除”过程,收集器线程都可以与用户线程一起工作。所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

优点:并发收集、低停顿

2、CMS的并发带来的问题

在并发阶段,它虽然不会导致用户线程停顿,但是占用了一部分CPU的运行资源,导致应用程序变慢了,降低总吞吐量。

CMS默认启动的回收线程数是(处理器核心数 + 3)/4。当处理器核心不足4个时,CMS对用户程序的影响较大。

3、CMS的触发时机

CMS收集器不能像其他收集器那样,等待老年代几乎满了才进行收集。这是因为CMS需要预留足够的内存给用户线程使用

默认的策略是,老年代使用了68%的空间后,就会开始回收。

  • 如果策略的阈值设置得较低,那么垃圾回收就会更频繁,影响性能
  • 如果策略的阈值设置得过高,如果CMS预留的内存无法满足程序分配新对象的需要,JVM就会启动应急预案:冻结用户线程的执行,临时启用Serial Old来重新对老年代进行垃圾回收,这样停顿时间就变得很长。

4、CMS的缺陷

  • CMS收集器对处理器资源非常敏感,因为虽然不会停止用户线程,但是会和用户线程一起竞争处理器资源,导致用户线程执行变慢
  • CMS无法处理“浮动垃圾”,可能导致这次GC没有产生足够的空间,不得不触发一次Full GC
  • CMS是基于标记-清除算法,会产生大量的空间碎片。如果影响到对象分配,就不得不触发一次Full GC来整理内存。
  • 如果在CMS运行过程中,用户线程突然产生了大量的垃圾,JVM就会紧急暂停用户线程,使用Serial Old来重新对老年代进行垃圾回收

什么是浮动垃圾

在“初始标记”第一次判断时,该对象不是垃圾。但到“重新标记”第二次判断的期间,这个对象变为了垃圾,那么本次垃圾回收就无法处理它,只能等到下一次GC时才有机会将它回收,这种对象就是浮动垃圾。

5、为什么CMS用清除算法

因为CMS考虑的是,尽量减少垃圾收集让用户线程停顿的时间,但是工作量无法减少,那么就考虑在某些阶段,让用户线程和垃圾收集并发执行。

正因如此,在用户线程正常执行时,CMS不能去擅自修改任何对象的地址,否则会导致用户线程无法定位到对象。

复制算法和整理算法都需要改变对象的内存地址,所以不适合。

7、G1

G1是一款面向服务器的高性能垃圾收集器,主要针对具有多核处理器和大内存的机器。

JDK 1.9时,G1作为了默认的垃圾收集器,同时CMS被标记为“不推荐使用”。

G1在以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。

1、Region

G1堆内存的布局和其他收集器不同。

G1不再进行固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分成多个大小相等的独立区域(Region)

region 的大小是一致的,数值是在 1M 到 32M 字节之间的一个 2 的幂值数,JVM 会尽量划分 2048 个左右的 region。

每个Region都可以根据需要,扮演Eden、Survivor或者老年代空间。

G1可以对扮演不同角色的Region采用不同的收集策略,这样无论是新创建的对象,还是年龄较大的对象,都能获得很好的收集效果。

Region中还有一类特殊的Humongous区域,专门存储大对象。

如果一个对象超过Region容量的50%,它就会被存放在N个连续的Humongous Region中,G1的大多数行为都会把它当做老年代的一部分来看待。

由于堆内存被零散拆分了,所以需要维护一个空闲列表,来记录所有可用的region。

2、设计Region的意义

G1中依然存在新生代、老年代的概念,但它们不是连续的,而是一系列Region的动态集合。

G1能建立起可预测的停顿时间模型的基础,就是选择将Region作为单次回收的最小单元。

具体的做法是,它会根据各个Region里面的垃圾堆积的“价值”,维护一个优先级列表。价值包括两个方面:

  • 回收后能获得的空间大小
  • 回收所需的时间

每次垃圾回收时,根据用户指定的允许停顿时间,优先回收那些价值大的Region,保证了有限时间内的较高效率

相当于垃圾回收的思路转变了:

  • 之前是优先回收新生代,因为新生代往往能获得较大的内存,新生代回收完还不够才会去全堆收集
  • 现在是,优先回收那些回收价值大的内存,这是一个主动的行为,所以效率就比之前高很多,因为更有针对性。

3、G1的三种模式

Young GC

当所有eden region被耗尽无法申请内存时,就会触发一次young gc。会暂停用户线程,发起多个垃圾回收线程。

存活的对象会被拷贝到survivor region,或者晋升到old region中。被清理的region会被放入空闲列表中,等待下次被使用

Mixed GC

之前的所有垃圾收集器,都是要么针对新生代,要么针对老年代,要么针对整堆进行垃圾回收的。

当越来越多的对象晋升到old region中,达到设定的阈值后,就会触发一次Mixed GC,回收掉高价值的目标

Full GC

如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc

G1的full gc算法就是单线程执行的serial old gc,会导致用户线程被长时间暂停,所以需要避免Full GC。

4、Mixed GC的运行过程

  • 初始标记:

    • stop the world,标记一下GC Roots能直接关联到的对象
    • 并且修改TAMS指针的值,让下一阶段的用户线程能正确在可用的空间分配新对象
  • 并发标记:从GC Roots出发,对堆中对象进行可达性分析。和用户线程一起并发执行(只有并发标记阶段能和用户线程并发执行)

  • 最终标记:stop the world,处理并发阶段结束后遗留的STAB记录

  • 筛选回收

    • 根据用户设定的时间停顿用户线程,因为涉及到对象地址的修改。
    • 对Region的价值进行排序,根据用户期望的停顿时间制定回收计划,构成回收集
    • 然后把决定回收的那一部分Region的存活对象复制到空的Region中,清理掉旧Region的全部空间。

5、Card Table

在传统垃圾回收中遇到的跨代引用问题,G1中也同样存在。

由于G1把整个堆拆成了很多个region,所以每个region的不同对象之间是有互相引用的依赖关系的,而且同代引用也会发生在不同region之间。

如果进行回收之前需要遍历所有region来做到准确的垃圾回收,效率极低。

G1也是采用了Remember Set记忆集的思路,做了一个Card Table。

卡表中存放了各个Region之间的引用关系,这样就可以只去扫描相关的Region,不需要全体扫描。

6、三色标记法

G1和CMS一样,在并发标记阶段使用了三色标记法:

漏标问题

正常的引用关系是:

  • 一个对象扫描完成,作为灰色节点
  • 扫描该对象的成员变量,也就是和它有引用关系的其他对象,扫描完成后,之前的对象变为黑色节点
  • 还没有被扫描的对象,作为白色节点
  • 如果在并发标记阶段,用户线程把灰色节点和白色节点之间的引用删除,此时扫描灰色节点的成员变量时就不会扫描到该白色节点,它会被视为垃圾
  • 但是白色节点和黑色节点产生了引用,那么这个引用是无法被现有的扫描方式所察觉的,就会造成本来存在引用关系的节点被漏标的问题。

CMS和G1如何解决漏标问题

产生漏标问题的条件有两个:

  • 黑色对象指向了白色对象 (关注引用的增加)
  • 灰色对象指向白色对象的引用消失 (关注引用的删除)

所以要解决漏标问题,打破两个条件之一即可。

  • CMS的做法是:
    • 并发标记过程中,使用“增量更新”机制,如果产生了新的引用,就把该黑色节点标记为灰色,之后还会重新扫描,获得最新的引用关系
    • 并发标记完成后,要回收的对象就不会再增加了
    • 并发标记完成后触发二次停顿,把这些重新建立引用关系的对象移出回收范围
    • 并发标记过程中产生的新垃圾不会被回收。
  • G1的做法是:
    • 会记录在并发标记阶段产生的新垃圾,然后在最终标记阶段,把这些垃圾也计入回收范围。
    • 当灰–>白消失时,要把这个 引用 推到GC的堆栈,保证白还能被GC扫描到。

为什么G1不使用增量更新机制

因为如果把黑色节点标记为灰色节点,之后还要二次查找,效率低。

G1保存了卡表,里面存放了各个Region之间的引用关系。

7、STAB

G1使用原始快照(STAB)算法来解决,Snapshot-At-The-Beginning。

具体做法是:

  • 在GC开始之前,创建一个对象快照。
  • 在并发标记时所有快照中当时的存活对象就认为是存活的,标记过程中新分配的对象也会被标记为存活对象,不会被回收。

要达到GC与用户线程并发运行,必须要解决回收过程中新对象的分配,所以G1为每一个Region区域设计了两个名为TAMS(Top at Mark Start)的指针,从Region区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围。

8、G1的特点

  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,所以不会产生内存碎片
  • 可预测的停顿:这是 G1 相对于 CMS 的另⼀个优势,降低停顿时间是 G1 和 CMS 共同 的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定允许的停顿时间。

8、JDK 默认垃圾收集器

jdk1.7 默认垃圾收集器:Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.8 默认垃圾收集器:Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 默认垃圾收集器:G1

猜你喜欢

转载自blog.csdn.net/m0_62609939/article/details/130659505
今日推荐