0%

Java垃圾回收器

常见的垃圾回收器有哪些?

  1. 串行(Serial)回收器是单线程的一个回收器,简单、易实现、效率高。

  2. 并行(ParNew)回收器是Serial的多线程版,可以充分的利用CPU资源,减少回收的时间。

  3. 吞吐量优先(Parallel Scavenge)回收器,侧重于吞吐量的控制。

  4. 并发标记清除(CMS,Concurrent Mark Sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,该回收器是基于“标记-清除”算法实现的。

  5. G1(Garbage First)收集器是一款在server端运行的垃圾收集器,专门针对于拥有多核处理器和大内存的机器,在JDK 7u4版本发行时被正式推出,在JDK9中更被指定为官方GC收集器。它满足高吞吐量的同时满足GC停顿的时间尽可能短。

参考《深入理解Java虚拟机(第2版)》 76页 3.5 垃圾收集器。

Serial收集器

新生代收集器,单线程收集,收集时Stop the world(暂停所有线程),采取复制算法

客户端新生代默认收集器,因为用户桌面应用场景中,分配给虚拟机的内存不会很大,停顿时间较短,只要不是频繁发生,可以接受,这样就发挥出Serial收集器简单而高效的优点。

Serial Old收集器

老生代收集器,单线程收集,收集时Stop the world(暂停所有线程),采取标记整理算法

Parallel New收集器

新生代收集器,Serial收集器的多线程版本,多个GC线程同时进行回收

Parallel Scavenge收集器

新生代收集器,采用复制算法多线程收集

目标是达到一个可控的吞吐量,吞吐量优先的收集器

吞吐量=程序运行时间/(程序运行时间+垃圾回收时间)

适合后台运算不需要太多实时交互响应的场景

适合对吞吐量和CPU资源很在意,对交互响应不敏感的的场景,例如后台服务,可以尽可能的多执行任务

可以开启自适应调解策略,让虚拟机根据系统运行情况动态调节内存分配的参数(如新生代大小、Eden与Survivor比例、晋升到老年代的年龄阈值等),以达到最佳的吞吐量,这是与Parallel New的重要区别。

还可以设置最大GC停顿时间和吞吐量给虚拟机设定优化的目标,由虚拟机自动调解最适合的参数来进行内存分配管理。

参考:

Parallel Old收集器

老年代收集器,Parallel Scavenge的老年代版本,使用多线程和标记整理,吞吐量优先

Concurrent Mark Sweep收集器

老年代收集器,以获得最短的GC停顿时间为目标的收集器,适合卡顿容忍度低的场景,例如有用户交互的情景(如Android手机交互)

收集过程为:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

初始标记和重新标记需要Stop the world(暂停所有线程)。初始标记仅标记跟GC Roots直接关联的对象,速度很快;并发标记就是进行完整的可达性分析(GC Roots Tracing);重新标记是为了修正并发标记期间进程继续运行而导致对象关系变动而重新标记这些变动的对象关系,运行时间比初始标记稍长,但是远比并发标记时间短,要扫描全堆。

缺点:

  1. 并发标记和并发清除所启用的多个GC线程占用了CPU资源,降低系统吞吐量。
  2. 无法处理并发清除期间程序继续运行而产生的浮动垃圾,不能等待老年代满了才收集,等到老年代占用的空间达到一个阈值,就要启用CMS清理,这样就预留了一定的空间来容纳并发清除期间产生的浮动垃圾,如果预留的空间无法满足程序继续运行的需要,则改用Serial Old执行清理。
  3. 标记清除算法会产生大量不连续的内存碎片,整理碎片只能串行执行,所以比较耗时,采用标记清除也是因为老年代的回收次数比较少,每次回收都标记整理性价比不高,可以在几次标记清除后再执行一次标记整理,调节一个可控的执行频率。

参考

为什么CMS只能用作老年代收集器,而不能应用在新生代的收集?

因为要并发清除,所以用的是标记清理算法,而标记清理算法会产生大量内存碎片,对新生代难以接受,新生代适合用复制算法,或者需要整理内存,以腾出连续的空间以供分配大对象。

因此新生代的收集器并未提供CMS版本。

为什么CMS不能用标记整理而要用标记清理?

因为老年代存活时间长,每次清理后,要整理的存活的对象太多了,比较耗时。

CMS主要关注低延迟,如果采用压缩算法,则涉及到要移动应用程序的存活对象,此时不停顿,是很难处理的,通常须要停顿下,移动存活对象,再让应用程序继续运行,但这样停顿时间变长,延迟变大。

参考:

Garbage First(G1)收集器

JVM内部知道,哪些region的对象最少(即:该区域最空),总是会优先收集这些region(因为对象少,内存相对较空,肯定快),这也是Garbage-First得名的由来,G即是Garbage的缩写, 1即First(第1)。

G1 根据存活对象的字节数统计每个区域的 活跃度liveness,然后根据期望停顿时间来确定该 CSet 的大小,并保证那些垃圾多(活跃度低)的区域会被优先回收,故此得名 垃圾优先。

设计目的

G1 能够在大内存、多处理器计算机上,保证 GC 暂停时间可控,并实现高吞吐量。

其最终目的是取代 CMS 成为服务端 GC 更好的解决方案:

  1. 采用 标记-整理 算法,可以避免使用细粒度的空闲列表进行分配。简化了收集器设计并消除了潜在的碎片问题。
  2. 提供可预测的GC暂停时间,无需牺牲很多吞吐量。

G1相对于CMS的优势

  1. CMS是标记清理,回收过后有大量内存碎片;G1从整体来看是基于标记-整理算法实现的,从局部(两=08u ym,ghvtbfrcRegion之间)上来看是基于“复制”算法实现的,避免了过多的内存碎片
  2. G1在逻辑上虽然也Eden、Survivor、Old区,但是都是以Region为单位,不需要扫描整个内存空间,只要扫描有存活对象的Region,减少了扫描的时间
  3. G1可以通过设置预期停顿时间( Pause Time) 来控制垃圾收集时间,在垃圾回收时尽量满足设置的停顿时间。G1会通过一个合理的计算模型,计算出每个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的Regions作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。
  4. G1会在Young GC中使用,而CMS只能在Old区使用,因为年轻代需要用复制算法以腾出大量连续内存空间,CMS是标记清理算法会产生大量内存碎片

参考:

G1的适合场景

  1. 服务端多核CPU、JVM内存占用较大的应用。
  2. 应用在运行过程中会产生大量内存碎片、需要经常压缩空间。
  3. 想要更可控、可预期的GC停顿周期:防止高并发下应用的雪崩现象,实现高吞吐量。

一句话总结:实现高吞吐、没有内存碎片、收集时间可控。

G1收集器是server-style的垃圾收集器,适用于具有大内存的多处理器计算机。它极有可能满足垃圾回收(GC)暂停时间目标,同时实现高吞吐量。

对于打算从CMS或者ParallelOld收集器迁移过来的应用,按照官方 的建议,如果发现符合如下特征,可以考虑更换成G1收集器以追求更佳性能:

  1. 实时数据占用了超过半数的堆空间;
  2. 对象分配率或“晋升”的速度变化明显;
  3. 期望消除耗时较长的GC或停顿(超过0.5——1秒)。

G1内存划分

G1把内存分为大约2000个左右的一个个大小固定的小块,小块叫Region,每个Region被标记为分代标志,如Eden、Survivor、Old、Humongous,每个Region在物理内存中可以不连续

G1把内存划分为Region区域有什么好处?

  1. 其他收集器会在全堆做扫描,导致gc停顿时间会比较长,而G1只是整理特定几个region,不必扫描全堆
  2. 在物理,上不需要连续,则带来了额外的好处有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾;停顿时间可预测,用户可以指定收集操作在多长时间内完成
  3. G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩,避免了内存碎片。

G1收集器中大对象怎么分配

  1. 对象的大小<0.5个RegionSize直接存在新生代Eden Region区
  2. 对象的大小>=0.5个RegionSize且对象的大小<1个RegionSize,存到大对象区Humongous Region
  3. 对象的大小>=1个RegionSize存到连续的大对象区Humongous Region

参考:

G1预测停顿时间是怎么做到的?

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。

G1会通过一个合理的计算模型,计算出每个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的Regions作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。

G1为什么高效?

为了提高扫描根对象和标记的效率,G1使用了二个新的辅助存储结构:

  • Remembered Sets:简称RSets,用于根据每个region里的对象,是从哪指向过来的(即:谁引用了我),每个Region都有独立的RSets。(Other Region -> Self Region)
  • Collection Sets :简称CSets,记录了等待回收的Region集合,GC时这些Region中的对象会被回收(copied or moved)。

Rememberred Set是什么?

记录哪些分区引用了当前的Region。

使得垃圾回收器不需要扫描整个堆,就可以找到谁引用了当前分区的对象,扫描RSet就行了。

Young区到Old区的引用则不需要单独处理,因为Young区中的对象本身变化比较大,没必要浪费空间去记录下来。

  1. RSet:全称Remembered Sets, 用来记录外部指向本Region的所有引用,每个Region维护一个RSet。
  2. Card: JVM将内存划分成了固定大小的Card。这里可以类比物理内存上page的概念。

每个Region初始化时,会初始化一个remembered set(已记忆集合),这个翻译有点拗口,以下简称RSet,该集合用来记录并跟踪其它Region指向该Region中对象的引用,每个Region默认按照512Kb划分成多个Card,所以RSet需要记录的东西应该是 xx Region的 xx Card。

参考:

参考资料

JDK8默认垃圾回收器是什么?

Parallel Scavenge + Parallel Old

Parallel Scavenge是吞吐量优先的垃圾收集器,采用复制算法,多线程收集。

Parallel Old是Parallel Scavenge的老年代版本,采用标记整理算法,多线程收集。

参考:

JDK9默认垃圾收集器是什么?

G1。

采用G1因为性能优越。

参考:

Android ART虚拟机垃圾回收机制是怎样的?

参考: