Step 3.2:垃圾收集器与内存分配策略

本文深入探讨了Java HotSpot虚拟机中的垃圾收集器和内存分配策略。主要内容包括根节点枚举、安全点、安全区域、记忆集与卡表、写屏障以及并发可达性分析。文章详细阐述了各种机制如何协同工作以确保高效、低延迟的垃圾收集,同时也介绍了经典的垃圾收集器,如Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS和G1收集器的特点与应用场景。
摘要由CSDN通过智能技术生成

1. HotSpot算法细节实现

Step 3.1 中从原理上介绍了常见的对象存活判定算法(技术器计数法和GC Roots)和垃圾收集算法,Java虚拟机实现这些算法时,必须对算法的执行效率有严格的考量,才能保证虚拟机的高效运行。

1.1 根节点枚举

这里从可达性算法中从GC Roots集合找引用链这个操作作为介绍虚拟机高效实现的第一个例子。GC Roots的节点包括全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧的本地变量表)。我们知道Java引用越庞大,光是方法区大小就有上千兆,里面的类、常量更时恒河沙数,要一一检查要消耗大量时间。

目前所有的收集器在根节点枚举这一步骤都需要暂停用户线程,现在可达性分析算法查找引用链的过程都可以做到和用户线程并发执行,但根节点枚举始终还是必须在一个能保障一执行快照中才能进行—这里“一致性”的意思是整个枚举期间执行子系统看起来像冻结在某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化的情况,这也是导致垃圾收集过程中必须停顿所有用户线程的重要原因。由于目的前主流Java虚拟机使用的都是准确式垃圾收集,所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机有办法直接得到哪些地方存放着对象的引用。在HotSpot的解决方案里,是使用一组OopMap的数据结构来达到这个目的。一旦一个加载动作完成时,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译的过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样子,收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找。

准确式垃圾收集是Java虚拟机(JVM)中的一种内存管理方法,它通过精确追踪对象之间的引用关系,识别和回收不再被程序使用的内存对象,以防止内存泄漏并提高程序性能。这确保了只有不再可达的对象才会被回收,而不会误判仍在使用的对象为垃圾。

1.2 安全点

在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举,但如果导致OopMap内容变化的指针特别多,如果为每一条指令都生成对应的的OopMap,将会需要大量的额外空间,这样垃圾收集的成本是很高的。实际上HotSpot也没有为每条指令生成OopMap,前面提到,只是在“特定的位置”记录了这些信息,这些位置被称为安全点。有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到安全点后才能暂停。安全点的位置的选取是以“是否具有让程序长时间执行的特征”为标准进行选定的,因为每条指令执行的时间都是很短的,程序不太可能因为指令流长度太长的原因长时间执行,“长时间执行”的最明显的特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等都属于对指令序列的复用,所以只有具有这些功能的指令才会产生安全点。

虚拟机会选择在程序中那些包含了需要长时间执行的指令序列的地方设置安全点。这是因为在这些指令序列中,对象的使用和引用发生得比较频繁,这样的情况通常会导致垃圾收集器找到更多需要回收的对象。这就是为什么这些特定的指令序列会成为执行垃圾收集操作的好时机。

对于安全点,另外一个问题就是,如何在垃圾收集发生时让所有线程都跑到最近的安全点,然后停顿下来。这里有两种方法:

  • 抢先式中断
  • 主动式中断

前者不需要线程的执行代码主动去配置,在垃圾收集发生时,系统首先把所有的用户线程全部中断,如果发现有用户的线程中断点不在安全点上,就恢复这条线程的执行,让其一会再中断,直到跑到安全点上。(这种方法用的很少)。后者的思想是当垃圾收集需要中断线程时,不直接对线程操作,仅仅设置一个标志位,各个线程执行过程时会不停地主动轮询这个标记,一旦发现中断标志位为真时,就在自己最近的安全点中断挂起。(轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象)轮询操作在代码中会频繁出现,所以要求它很高效,HotSpot使用内存保护陷阱的方式,把轮询操作在代码精简到只有一条汇编指令。

如下面代码,test指令就是轮询指令,当需要暂停用户线程时,虚拟机将0x160100的内存页面谁在为不可读,那线程执行到test指令时就会产生一个自陷异常信号,然后在预先注册的异常处理器中挂起线程等待执行,这样仅通过一条汇编指令便完成安全点轮询和触发线程中断了。

在这里插入图片描述

1.3 安全区域

安全点似乎完美的决定如何停顿用户线程了,但还是会有特殊情况。安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点。但如果程序没有分配处理器时间,线程处于睡眠或阻塞状态呢?这时候线程无法响应虚拟机的中断请求,不能再走到安全点去中断挂起自己,虚拟机也不能等待线程重新被激活分配处理时间。对于这种情况HotSpot引入了安全区域的概念。

安全区域是指能够确保在某一段代码片段中,引用关系不会发生变化,因此在这个区域中任何地方发生垃圾收集都是安全的。(可以理解为扩展了的安全点)当用户线程执行到安全区域里面的代码时,首先会标识自己进入了安全区域,那样当这段时间虚拟机要发起垃圾收集时就不必去管这些声明自己在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当做没事发生过,继续执行。否则它将一直等待,知道收到可以离开安全区域的信号为止。

1. 4 记忆集与卡表

在Step 3.2中,我们介绍了记忆集的概念,它的作用时当存在跨代引用时新生代会建立一个记忆集把老年代划分为若干小块,标识出老年代哪块内存存在跨代引用。但其实不止新生代和老年代之间会存在跨代引用问题,所有涉及部分区域收集(Partial GC)行为的垃圾收集器,如G1、ZGC和Shenandoah收集器,都会面临相同有问题,所以有必要进一步解释记忆集的原理和实现方式。

记忆集是一种用于记录从非收集区域到收集区域的指针集合的数据结构。最简单的实现方式是用非收集区域中所有含跨代引用的对象数组来实现这个数据结构。但这种方式存在着很大的空间维护成本,而收集器其实并不需要知道这些跨代指针的具体细节,只需要知道某一块非收集区域是否存在有着指向收集区域的指针即可。所以有了记忆集更加合理的实现方式:

  • 字长精度:每个记录精确到一个机器字长,该字包含跨代指针
  • 对象精度:每个记录精确到一个对象,该对象里有字段含跨代指针
  • 卡精度:每个记录精确到一块内存区域,该区域内含有跨代指针

第三种“卡精度”所指的是用一块称为“卡表”的方式去实现记忆集的,这也是最常用的记忆集的实现方式。HotSpot虚拟机的卡表实现方式就是用一个字节数组。

CARD_TABLE[this address >> 9]=0

字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域一块特定大小的内存块,这个内存被称为卡页。卡页的大小一般是2的N次幂,HotSpot种N是9,即512字节。

在这里插入图片描述
一个卡页的内存中通常包含不止一个对象,只要卡页中有一个(或更多)对象的字段存在跨代指针,那就将对应卡表的数组元素标识为1,称这个元素变脏,没有则为0。在实际垃圾回收时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一起扫描。

1. 5 写屏障

前面介绍了如何使用记忆集来缩减GC Roots扫描范围的问题,但还没有解决卡表如何维护的问题。

卡表何时变脏?
答:在有其他分代区域中对象引用了本区域的对象时,其对应的卡表就会变脏,变脏时间点为引用类型字段赋值的那一刻。

卡表如何变脏?
在HotSpot虚拟机里是通过写屏障维护卡表状态的。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形通知,供程序执行额外的动作,也就是在赋值前后都在写屏障的覆盖范畴内。赋值前部分写屏障称为写前屏障,赋值后部分屏障称为写后屏障。HotSpot很多地方都会用到写屏障,但G1出现前都是用写后屏障。

所以有了写屏障后,虚拟机会为所有赋值操作都生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外开销,不过这个开销与Minor GC时扫描整个老年代的代价相比是低很多的。

转移到多线程场景下,我们会发现新的问题(一旦涉及到更新就应该想到多线程场景下涉及的问题)。高并发场景卡表的更新会出现“伪共享”问题。现代CPU缓存系统中时以缓存行为单位存储的,当多线程修改相互独立的变量时,如果这些变量共享一个缓存行是,就会彼此影响。索引为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当卡表元素没有被标记才能更新。HotSpot中增加了-XX:+UseCondCardMark来决定是否开启卡表的更新操作的条件判定。

1. 6 并发的可达性

前面提到目前主流的垃圾收集器都依靠可达性算法来判断对象是否存活。可达性分析算法理论上要求全过程都基于一个能保障一执行的快照中才能进行分析,这意味着必须要冻结用户线程的运行。 GC Roots相比Java堆中的对象是很少的,所以根节点的扫描相对时间比较固定,当开始从GC Roots继续向下遍历对象图时,这一过程的时间和堆容量直接成正比例关系了,堆越大存储的对象越多,对象图结构也越复杂,要标记更多对象而产生的停顿时间自然更长。要优化这一部分,就需要搞清楚为什么必须在一个能保障一致性的快照上进行对象图的遍历?为了解释这个问题,我们这里引入了三色标记作为推导辅助工具,将便利对象图过程中遇到的对象,按照是否访问过这个条件标记成下面三种颜色:

  • 白色:表示没有被垃圾收集器访问过
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都被扫描过
  • 灰色:表示对象已经被垃圾收集器访问过,当这个对象至少还有一个引用没有被扫描过

此时假说用户线程与收集器时并发工作的,收集器在对象图上标记颜色,同时用户线程在修改引用关系,此时会出现两种结果:

  • 将原来已经消亡的对象标记为存活(该问题不太严重,下一次垃圾回收还是可以将这对象收集掉)
  • 把原来存活的对象标记为死亡(程序会发生错误,问题很严重)

初始状态,只有根被访问过,为黑色(引用是有向的,只有被黑色的引用过才能存活)

在这里插入图片描述

扫描过程中,以灰色为波峰从黑向白推进。灰色是黑白的分界线

在这里插入图片描述

扫描顺利完毕,此时黑色就是存活对象,白色就是消亡可回收的对象

在这里插入图片描述

但如果用户线程在标记过程中并发修改了引用关系,扫描就不会那么顺利了,例如下图正在扫描一个灰色对象,此时该灰色对象的一个引用被切断了,同时原来引用的对象又与已扫描过的黑色对象建立了引用关系

在这里插入图片描述

又如,这种切断后 重新被黑色对象引用对象是原来引用链的一部分。由于黑色对象不会重新扫描,所以扫描结束后被黑色引用的对象还是白色,这样原来不应该消亡的对象最后被垃圾收集器回收了,这样问题就很大了

在这里插入图片描述

所以当且仅当下面两个条件满足时,会产生“对象消失”问题:

  • 赋值器插入了一条或多条从黑色对象到白色对象的引用
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用

所以为了解决上面出现的并发问题,我们只需要破坏这两个条件即可。由此产生了两个解决方案:

  • 增量更新
  • 原始快照

增量更新破坏第一个条件,当黑色对象插入新的指向白色对象引用关系时,就会将这个插入引用记录保存下来,等扫描结束后,再将这些记录过的引用关系中的黑色为根,重新扫描一次。

原始快照破坏第二个条件,当灰色对象要删除指向白色的引用时,就要将这个删除记录保存下来,在扫描结束后,再将这些引用关系中的灰色对象为根,重新扫描一次。

以上无论时对引用关系记录插入还是删除,虚拟机的记录操作都是通过写屏障实现的。

2. 经典垃圾收集器

下面介绍HotSpot虚拟机中出现过的经典垃圾收集器。如果说收集算法时内存回收的方法论 ,那垃圾收集器就是内存回收的实践者。

在这里插入图片描述

上图显示了七种不同分代的垃圾收集器,如果两个垃圾收集器存在连线就,就说明它们可以搭配使用。虽然我们下面会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来,虽然垃圾收集器的技术在不断进步,但直到现在还没有一个最好的垃圾收集器出来。

2.1 Serial收集器

  • 简介

Serial收集器时最基础,历史最悠远的收集器,在(JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。

  • 特点
  1. Serial收集器是一个单线程收集器,它进行垃圾收集时只会使用一个处理器或一条收集线程去完成垃圾收集过程,而且它进行垃圾收集时,必须暂停其他所有工作线程(你的电脑在垃圾收集过程中处于无法响应状态),直到它收集完成。

在这里插入图片描述
2. 迄今为止,它依然时HotSpot虚拟机运行在客户端默认新生代收集器,有着优于其他收集器的地方,那就是简单高效,对于内存资源受限的环境,它就是所有收集器里额外消耗内存空间最小的。

2.2 ParNew收集器

  • 简介

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

-XX:SurvivorTatio,-XX:PretenureSizeThreshold,-XX:HandlePromotionFailure

在这里插入图片描述

  • 特点

1.ParNew相比Serial除了多线程外没有什么其它创新点,但它时不少运行在服务端模式下HotSpot虚拟机,尤其是JDK7之前的遗留系统中首选的新生代收集器,其中有一个功能、性能无关但其实很重要的原因就是:除了Serial收集器外,目前只有它能与CMS收集器配置工作(也正是CMS收集器的存在才巩固了ParNeW的地位)

2.ParNew收集器在单核心的环境中不会比Serial有更好的效果,甚至因为存在线程交互的开销,该收集器在通过超线程技术实现伪双核处理器环境中都不能百分百超越Serial收集器。当然随着可以被使用的处理器核心数量增加,ParNew对于垃圾收集时系统资源的高效利用还是很有好处的。

3.ParNew默认开启的线程数量和处理器核心数相同,处理器非常多的环境下可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数

并发是指两个或多个事件在同一时间间隔发生,任务在不同的时间点交给处理器进行处理。并行是指两个或多个事件在同一时刻发生,任务可以分配给每个处理器独立完成。并发侧重于在同一实体上,而并行侧重于在不同实体上。并发在同一台处理器上同时处理多个任务,而并行在多台处理器上同时处理多个任务。

2.3 Parallel Scavenge 收集器

  • 简介

Parallel Scavenge收集器是一款新生代收集器,它和上面一样使用标记复制算法实现的收集器,也是能够并行收集的多线程收集器。(看起来和ParNew很类似)

在这里插入图片描述

  • 特点

1.它的关注点和其他收集器不同,CMS等收集器关注点时尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(用于运行用户代码的时间/运行用户代码时间+运行垃圾收集的时间)

2.Parallel Scavenge收集器提供两个参数拥有精确控制吞吐量,分别是-XX:GCTimeRatio直接设置吞吐量大小和-XX:MAXGCPauseMillis控制最大垃圾收集停顿时间。前者的值区间为(0,100),后者的值是一个大于0的毫秒数。

3.由于与吞吐量关系密切,Parallel Scavenge收集器也被称为“吞吐量优先收集器”

4.除了关注上面两个参数以外,还有一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,当这个参数被激活后,就不需要人工指定新生代大小(-Xmn)、Eden和Survivor区的比例(-XX:SUrvirvorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据目前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间和最大吞吐量了。这种方式称为垃圾收集自适应调节策略。此时用户只需要将基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(更关注最大停顿时间)或-XX:GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就又由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个特性

2.4 Serial Old 收集器

  • 简介

Serial Old收集器是是Serial收集器的老年版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:

  1. 在JDK5以及之前的版本中于Parallel Scavenge收集器搭配使用
  2. 作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用

在这里插入图片描述

2.5 Parallel Old 收集器

  • 简介

Parallel Old是Parallel Scavenge收集器的老年版本,支持多线程并发收集,基于标记-整理算法。这个收集器是JDK6时才开始提供,在这个收集器出现之前Parallel Scavenge处于很尴尬的地位,原因是没有它除了Serial Old搭配没有其它收集器搭配。知道Parallel Old出现,“吞吐量优先”收集器才有了比较名副其实的搭配组合。

在这里插入图片描述

2.6 CMS 收集器

  • 简介

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前的B/S系统的服务端上,这类应用就通常关注服务响应速度,希望系统停顿时间尽可能短,以带来良好的交互体验。该收集器基于标记-清除算法实现,它的运作过程包括下面四个部分:

1.初始标记(CMS initial Mark)
2.并发标记(CMS concurrent mark)
3.重新标记(CMS remark)
4.并发清除(CMS concurrent sweep)

初始标记、重新标记这两个步骤仍然需要“Stop The World:。初始标记仅仅只是标记一个GC Roots直接关联到的对象,速度很块。并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但不需要停顿用户线程。重新标记就是修正并发标记期间,因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录(增量更新)。并发清除是清理掉标记阶段判断死亡的对象。

在这里插入图片描述

  • 特点

1.CMS收集器的内存回收过程是与用户线程一起并发执行的。这款收集器是真正意义上支持并发的垃圾收集器。
2.CMS收集器只能和Serial和ParNew收集器配置使用。它是HotSpot虚拟机追求低停顿的第一次尝试成功,但它并不是完美的,也存在一些缺点:

  • CMS收集器对处理器资源十分敏感:虽然它不会导致用户线程停顿,但它本身占用了CPU资源,也会让程序变慢,降低了吞吐量。(处理器核心越少影响越大)。
  • 由于CMS无法处理浮动垃圾,有可能出现“Concurrent Mode Failure”失败进而导致另一次“Stop The Word”的Full GC的产生。浮动垃圾是指,在CMS的并发标记和并发清理阶段,用户线程还在继续运行,程序在会产生新的垃圾对象。这些垃圾对象出现在标记过程后,CMS无法在当次收集无法处理它们,所以只能下次垃圾收集处理它们。同样也是,在垃圾收集阶段,由于用户线程还在运行,所以必须预留一部分空间给用户线程使用,所以它不能像其他收集器一样,等待老年区几乎快满了再收集。JDK 5默认68%空间被使用后出发CMS垃圾收集,可以通过调节-XX:CMSInitiatingOccu-pancyFraction的值来控制该比例。JDK 6该值就提升到了92%,此时又来了新的问题,如果预留内存空间无法满足用户线程,就会出现“并发失败”(Concurrent Mode Failure)。这时虚拟机不得不启动后被方案:冻结用户线程,临时启用Serial Old收集器进行老年代的垃圾收集,这样停顿时间会很长。所以参数-XX:CMSInitiatingOccu-pancyFraction设置过大会很容易导致大量的并发失败产生,性能反而降低。
  • CMS基于“标记-清除”算法实现,会产生大量内存碎片,如果内存碎片过多导致无法分配大对象,会触发一次“Full GC”,为了解决这个问题,CMS收集器提供了-XX:+UseCMSCompactAtFullCollection开关参数(默认开始,JDK 9 已经废弃),用于菜CMS收集器不得不进行Full GC时开启内存碎片合并整理过程,这个过程同样“Stop The World”,会时增加用户停顿时间,因此又提供了-XX:CMSFullGCsBeforeCompaction(JDK 9废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量参数决定)不整理空间的Full GC后,下一次进入Full GC时先进行碎片整理(默认为0,表示每次进入Full GC时都进行碎片整理)

“Full GC”(Full Garbage Collection)是一种Java垃圾收集的类型,它与部分垃圾收集(Partial GC)有所不同。Full GC 是一次更为全面的垃圾回收过程,它的目标是清理整个堆内存,包括新生代和老年代,以回收不再使用的对象和释放内存。Full GC 运行时,通常会暂停整个应用程序的执行,因为它需要扫描和清理整个堆内存,这可能会导致较长的停顿时间。

2.7 Garbage First收集器(重点)

  • 简介

Garbage First收集器(G1收集器)是垃圾收集技术发展历史上里程碑式的成果,它开创了收集器局部收集设计思路和基于Region的内存布局形式,它是一款“全功能的垃圾收集器”。

  • 特点

1.G是一款面向服务端应用的垃圾收集器(JDK 8 Update 40正式从实验版本投入使用)。它已经宣告取代Parallel Scavenge + Parallel Old组合,称为服务端模式下的默认收集器,并在未来取代CMS

如果你在JDK 9 及以上版本使用-XX:UseConcMarkSweepGC开启CMS会收到系统的警告信息。但CMS作为一款广泛应用过的垃圾收集器,它和HotSpot的内存管理、执行、编译、监控等子系统有着千丝万缕的联系,因此JDK 10 ,HotSpot提出了“统一垃圾收集接口”,将内存回收的行为与实现进行分离,CMS以及其他收集器都重构成这套接口的一个实现,此后加入某一款收集器将变得很简单,风险也可以控制,为CMS退出历史舞台铺下最后的道路了。

作为CMS收集器的替代人,设计者希望做出一款“停顿时间模型”的收集器,所谓停顿时间模型是指能够支持在指定长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概不超过N毫秒这样的目标。那具体怎么实现这个目标,首先思想上需要有一个转变,在G1收集器之前所有的收集器要么是新生代收集器,要么是老年代收集器,再要么是整个java堆(Full GC)。而G1做出了改变,他可以面向堆内存的任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量越多,回收收益越大,这个就是G1收集器的Mixed GC模式。

G1开创的基于Region的堆内存布局是它能实现这个目标的关键。G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域,每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够堆扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是存活了一段时间、熬过多次收集的旧对象都能获得很好的收集效果。

在这里插入图片描述

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过一个Region容量一半的对象即可认为是一个大对象。每个Region大小可以通过-XX:G1HeapRegionSize来设定,取值范围是1MB~32MB,且应为2的N次幂。对于超过整个Region容量的超大对象,G1会设置几个连续的Humongous区域来存储超大对象。G1通常会将Humongous Region作为老年代的一部分看待。

从上面看出,G1虽然也是基于分代理论来设计的,但不同的是G1的新生代和老年代不再是固定的了,它们是一系列区域(不一定连续)的动态集合。

再回到前面分析G1是怎么实现“停顿时间模型”的,是因为它将Region作为单次回收的最小单位,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾回收,而是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后G1会在后台维护一个优先级列表,每次根据用于设定的停顿时间(-XX:MaxGCPauseMillis指定,默认是200毫秒),优先处理回收价值收益最大的哪些Region。所以这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在优先时间内获取尽可能高的收集效率

G1收集器的一些细节:

  1. 跨代引用问题:Java堆分成多个Region后,跨Region引用的问题就会出现,解决方式还是前面的记忆集,每个Region维护都维护自己的一份记忆集,记录别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。与前面的介绍的卡表结构不同,它的卡表是一种“双向”结构(包括“我指向谁”,这种结构还记录了“谁指向我”)。由于Region数量比传统收集器的分代数量要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。G1至少要耗费相当于Java堆容量的10%至20%的额外内存来维持收集器工作。
  2. 用户线程与垃圾收集线程并发问题:前面介绍的CMS收集器是多线程并行收集以及与用户线程并发运行的垃圾收集器。这里的关键是在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?首先要解决是在执行并发标记时,用户改变对象引用关系怎么样,前面介绍的CMS收集器采用增量更新的方式,而G1这里采用的是原始快照的方式。另外一个问题是,在垃圾收集过程中,程序继续运行创建新对象如何为这些对象分配空间的问题,G1位每一个Region设计了两个名为TAMS(Top at Mark Start)指针,把Region中的一部分空间划分出来用于并发回收过程中新对象的分配,并发回收时新分配的对象地址都必须要在这两个指针的的位置上,(以确保这些对象不会被垃圾收集器回收到)。与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC。

Concurrent Mode Failure是指在CMS垃圾回收器中的并发垃圾回收阶段(Concurrent Mark and Sweep)中,如果发现当前的内存空间不足以容纳垃圾对象,就会发生这种情况。

  1. 建立可靠的时间停顿预测模型:前面说到用户通过参数-XX:MaxGCPauseMillis可以指定停顿时间,但这个值只是一个期望值,但G1要怎么做才能满足该期望值?G1收集器的停顿预测模型是以衰减均值为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region的记忆集里的脏卡数量等各个可测量的步骤花费成本,并分析得出平局值、标准偏差、置信度等统计信息。这里强调的衰减平均值是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体状态,但衰减平均值更准确地代表“最近的”平均状态,简而言之,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成的回收集才可以在不超过期望停顿时间的约束下获得最高收益。

"衰减均值"是一种统计概念,它比传统平均值更容易受到新数据的影响。它使用了过去的观测数据,但会赋予较新的数据更高的权重。这意味着近期观测的数据对模型的决策影响更大

G1收集器的运作过程如下:

1.初识标记:仅仅只是标记一下GC Roots能直接关联的对象,并且修改TAMS指针的值,让下一阶段用户线程并发执行时,能正确在Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且时借用Minor GC的时候同步完成的,所以G1在这个阶段并没有额外的停顿。

由于Minor GC 本身需要停顿,"初识标记"阶段被设计成与Minor GC 同步执行。这意味着在进行Minor GC 的同时,G1 也会在后台完成初识标记。这个同步执行的设计避免了额外的垃圾回收暂停时间。因此,虽然初识标记需要某种停顿,但它与Minor GC 同时发生,不会增加额外的垃圾回收暂停时间,从而有助于减小应用程序的停顿时间。

2.并发标记:从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但是和用户线程并发执行。当对象图扫描完成后,还要重新处理SATB(快照)记录下的在并发时有引用变动的对象

3.最终标记:对用户线程做另一个短暂的停顿,用于处理并发阶段结束后遗留下来的最后少量的SATB记录。

4.筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意个Region构成回收集,然后决定回收的那一部分Region的存活对象复制到空的Region中(其实G1也是基于标记清除算法的,但G1从整体来看是标记-整理算法,但从两个Regionla),再清理到整个Region区域。这里涉及内存对象的移动,所以必须暂停用户线程,由多条收集器线程并行完成的。

在这里插入图片描述

毫无疑问,可以由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。这个参数值不能设置太小,否则会导致每次回收的垃圾就占堆内存的一小部分,导致垃圾越来越到最后导致低效率的Full GC。

G1与CMS相比的劣势:

  1. G1无论垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS高
  2. G1和CMS都是用卡表解决跨代引用,但G1的卡表实现更复杂,每个Region都有一份卡表,导致G1的记忆集占用整个堆的20%内存,甚至更多
  3. CMS使用写后屏障维护卡表,G1除了使用写后屏障维护卡表,还实现了原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变换情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值