跳至主要內容

垃圾收集器

blacklad大约 9 分钟JvmJavaJvm

垃圾收集器

1 Serial收集器

单线程收集器,在收集过程中必须暂停其他所有工作线程直到收集结束。

简单高效,所消耗内存资源小,适用于内存资源受限的环境。

2 ParNew

Serial的并行版本,使用多个线程进行垃圾收集。仅在多处理器的情况下优于Serial。可以与CMS搭配作为新生代的收集器。

3 Parallel Scavenge

新生代收集器,基于标记复制算法实现,目标是为了提高吞吐量。

原理: 自动分配新生代的大小,指定小的新生代,就会提升每次垃圾收集的时间,但与此同时频率就会提高导致吞吐量下降。

吞吐量=运行用户代码时间/总时间

可以通过

  1. -XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间, 值越小新生代空间越小,垃圾收集越频繁。
  2. -XX:GCTimeRatio设置吞吐量的大小,假设设置19,即1/(1+19) 垃圾收集占比5%

4 Serial Old

Serial的老年代版本。

5 Parallel Old

Parallel Scavenge的老年代版本。

6 CMS收集器

Concurrent Mark Sweep以最短回收停顿时间作为回收目标的老年代收集器。

6.1 回收方法

通过标记清除实现的。

6.1.1 初始标记

标记1. GCRoot能直接关联到的对象, 2. 新生代直达的老年代对象。

速度快,也需要暂停用户线程。

6.1.2 并发标记

从直接关联对象遍历整个对象图的过程。

  1. 标记存活的对象为可达对象
  2. 将并发标记阶段从新生代晋升到老年代对象、直接在老年代分配的对象和老年代引用发生变化的对象标记位dirty,避免再重新扫描阶段全局扫描

时间较长,但不需要暂停用户线程。

6.1.3 重新标记

修正并发标记期间变动的那一部分对象的标记记录,时间较短,需要暂停用户线程。

6.1.4 并发清除

清除标记判断为死亡的对象。不需要暂停用户线程。

6.2 缺点

  1. 资源敏感,默认回收线程为 (处理器核心+3)/4 如果处理器核心大于4时只占用 25%,反之就会导致处理器负载很大。
  2. 无法处理浮动垃圾。在并发标记过程中会产生新的垃圾对象,但CMS需要留到下次垃圾回收才能清理,如果垃圾回收过程中,新的对象无法分配内存就会发生失败,就会采用Serial Old收集,导致停顿时间更长。
  3. 采用标记清除实现,会导致内存碎片过多无法分配大对象,触发Full GC并整理内存碎片,使停顿时间变长。

7 G1收集器

G1把连续的java堆划分为多个相同大小相等的独立区域(region),每个region都可以代表新生代老年代空间,收集器对不同的region用不同的策略处理。同时G1中添加了一个Humongous区域,用来存储大对象,当一个对象超过region的一半就被认为是大对象,region的大小可以通过-XX:G1HeapRegionSize设置, 如果超过了region就会使用连续的region存放。

7.1 原理

  1. 对于跨region引用的对象,G1也需要通过记忆集的方式避免全堆扫描,需要在每个region记录下别的region指向自己的指针,并标记这些指针在哪个卡页范围内, 需要双向记录,同时region数量较多导致内存占用更大。
  2. 采用SATB原始快照的方式保证并发收集的问题。
  3. 以衰减均值作为停顿预测模型,更接近新数据的状态。

7.2 步骤

  1. 初始标记:标记GCRoots能直接关联到的对象,同时让TAMS指针指向可用内存,需要停顿用户线程,停顿时间短。
  2. 并发标记:可以与用户线程并发执行,完成后需要重新处理SATB记录下发生引用变化的对象。
  3. 最终标记:对用户线程做短暂暂停,处理仍然遗留的少量SATB记录。
  4. 筛选回收:对各个region统计按回收价值成本排序。将任意多个region作为回收集,把回收的那一部分region存活的对象复制到其他region中,然后清理旧的region。 并发执行,需要停止用户线程

7.3 缺点

  1. 记忆集导致占用内存大于CMS,而且CMS只维护老年代到新生代的引用,由于新生代都是朝生夕死所以没有维护新生代到老年代的引用,导致回收老年代需要把整个新生代作为GCRoot扫描
  2. 使用写前屏障记录指针变化情况。G1异步方式实现, CMS同步方式。

8 Shenadnoah收集器

同样适用了Region的内存布局,相比G1收集器:1.支持并发的回收 2.改用连接矩阵(仅简单记录了两个region之间有无引用)维护跨Region的引用关系。

8.1 回收过程

  1. 初始标记:stw,标记与GC Roots直接关联的对象。
  2. 并发标记:遍历整个对象图,标记全部可达的对象。
  3. 最终标记:stw,处理剩余的SATB扫描,统计回收价值最高的Region,构成一组回收集。
  4. 并发清理:清理没有任何存活对象的Region。
  5. 并发回收:把存活对象复制到其他未被使用的Region中,通过读屏障解决并发过程中的问题
  6. 初始引用更新:stw,确保所有对象移动完成。
  7. 并发引用更新:与用户线程并发更新引用,按照内存的物理地址的顺序,线性的搜索出引用类型把旧值改成新值。
  8. 最终引用更新:stw,修正存在于GC Roots中的引用。
  9. 并发清理:回收Region。

8.2 转发指针

在原有对象布局结构的最前面统一增加一个新的引用字段,当对象拥有一个新的副本时,只需要修改旧对象上的转发指针的值,使其指向新的对象。这样所有对于就对象的访问都会被转发到新的对象。这种方式会存在并发问题。

8.3 缺点

使用了读屏障,提高了对象访问的代价。

9 ZGC

在吞吐量影响不大的情况下实现低延迟。也是基于region内存布局,不分代,使用读屏障、染色指针、内存映射实现可并发的标记整理算法。

zgc的region具有动态性,被动态的创建销毁,以及3种大小的容量。2MB, 32MB, n*2MB,第三种可以动态变化且只能存放一个对象。

9.1 染色指针

HotSpot虚拟机的标记实现方案有如下几种:

  1. 把标记直接记录在对象头上(如Serial收集器)。
  2. 把标记记录在与对象相互独立的数据结构上(如G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息)。
  3. 直接把标记信息记在引用对象的指针上(如ZGC)。

在对象标记过程中只与对象的引用有关,而与对象本身的属性无关,因此ZGC将对象的标记信息记录在引用对象的指针上,在64位系统中理论上可以访问2^64的内存, 目前的 linux操作系统中支持最大的空间为2^46,zgc将高四位提取出来存储标记信息记录标记状态。

image-20210702082956084
image-20210702082956084
  • Finalizable:表示是否只能通过finalize()方法才能被访问到,其他途径不行。
  • Remapped:表示是否进入了重分配集(即被移动过)。
  • Marked1、Marked0:表示对象的三色标记状态。

9.1.1 缺点

  1. 占用4位导致只剩下42位可以访问(2^42=4TB)
  2. 不支持32位平台

9.1.2 优势

  1. 一旦对象被移走后,region就能立即被释放和重用掉,不必等到region的引用被修正后才能清理,使得只要有一个region空闲就能完成收集。
  2. 可以减少内存屏障使用数量,只使用了读屏障。
  3. 可以扩展记录更多的信息,以后利用操作系统未使用的高18位,提高性能。

使用多重映射将多个不同的虚拟内存地址映射到同一个物理内存地址上。

9.2 收集过程

  1. 并发标记:并发标记,更新染色指针上的标记位。 前后需要经历初始标记、最终标记的两个短暂停顿。
  2. 并发预分配:计算本次需要清理的region组成重分配集。
  3. 并发重分配:把重分配集中的存活对象复制到新的region中,并对重分配的region维护一个转发表,记录旧对象到新对象的转发关系。 如果此时用户线程访问位于分配集中的对象,会被读屏障截获根据转发表将访问转发到新的对象上,同时修正更新引用值,使其直接指向新对象(Self-Healing自愈能力)。
  4. 并发重映射:不等访问过程,主动修正指向重分配集中的旧对象的引用,提高访问速度。由于有自愈能力,所以不是必需要立刻完成的,也可以等到下次并发标记阶段(也会进行全堆遍历)里面完成。

9.3 问题

对于一个很大的堆用ZGC收集,由于要对所有region进行扫描会造成整个收集过程持续时间较长,如果对象分配速度过快,产生大量浮动垃圾导致堆空间不够用内存分配失败。可以通过增大堆容量解决。

10 Epsilon收集器

不做垃圾收集的收集器,只负责堆管理,内存分配等。适用于那些运行一次或者在堆内存耗尽前就退出的应用。

11 相关常用参数

12 内存分配策略

对于Serial+Serial Old

  1. 对象优先在Eden区分配,如果空间不够发生Minor GC, 如果GC过程中Survior不够存放现有存活对象,则通过分配担保机制移到老年代。
  2. 通过配置设置大对象直接进入老年代。
  3. 通过配置年龄到达一定值进入老年代。
  4. 如果Survior中相同年龄的所有对象总合大于Survior空间的一半,年龄大于等于该年龄的可以直接进入老年代。
  5. 分配担保。只要老年代的连续空间大于新生代对象总大小或者历次的平均大小就会进行MinorGC,否则进行Full GC。
上次编辑于:
贡献者: blacklad