Java 垃圾回收器

Java 垃圾回收器

判断对象是否可回收

如何判断一个对象属于垃圾对象呢?

引用计数法

对于一个对象 A,只要有任意一个对象引用了 A,则 A 的计数器加 1,当引用失效的时候,引用计数器就减 1。如果 A 的应用计数器为 0,则对象 A 就不可能再被使用。

缺点:无法处理循环引用的问题。

可达性分析算法

通过一系列的称为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的循环称为引用链。当一个对象到 GC Roots 没有任何引用链的时候,则证明此对象是不可达的,因此它们会被判定为可回收对象

可以作为 GC Roots 的对象:

  • 类静态属性中引用的对象
  • 常量引用的对象
  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象

垃圾回收算法

标记-清除算法

从每个 GC Roots 对象出发,依次标记有引用关系的对象,最后将没有被标记的对象清除。

缺点:带来大量空间碎片,导致需要分配一个较大连续空间时,容易触发 GC

标记-整理(标记-压缩)算法

从每个 GC Roots 对象出发,标记存活的对象,然后将存活的对象整理到内存空间的一端,形成连续的已使用空间,最后将已使用空间外的部分全部清理掉,消除空间碎片问题。

标记-复制算法

为了能够并行的标记和整理,将整个空间分为两块,每次只激活一块,垃圾回收只需把存活的对象复制到另一块未激活的空间上,将未激活空间标记为已激活,将已激活空间标记为未激活,然后清除原空间中的原对象。

分代收集算法

垃圾收集器一般根据对象存活周期的不同,将内存划分为几块,根据每块内存空间的特点,使用不同的回收算法,提供回收效率。

分区算法

将整个堆空间划分为连续的不同小空间,每一个小空间独立使用,独立回收。

优点:可以控制一次回收多少个小区间。

HotSpot 虚拟机垃圾收集器

Serial

新生代 Serial 收集器采用复制算法,使用单线程进行垃圾回收,回收时 Java 应用程序中的线程都需要暂停 (Stop-The-World),以等待回收完成。使用 -XX:+UseSerialGC 可以指定新生代采用 Serial 收集器,老年代采用 Serial Old 收集器。

ParNew

新生代 ParNew 将 Serial 收集器多线程化,在并发能力强的 CPU 上,产生的停顿时间短于串行回收器。开启 ParNew 回收器:

  • -XX:+UseParNewGC:新生代 ParNew,老年代采用 Serial Old
  • -XX:+UseConcMarkSweepGC:新生代 ParNew,老年代采用 CMS

-XX:ParallelGCThreads 可以指定并行回收的线程数,这个线程数的默认值是:

threadsNum = 0;

if (CORE_OF_CPU < 8) {
	threadsNum = CORE_OF_CPU;
} else {
	threadsNum = 3 + 5 * CORE_OF_CPU / 8;
}

Parallel

新生代 Parallel 采用复制算法,多线程、独占式,它与 ParNew 的不同之处:

  • 关注系统的吞吐量
  • 支持自适应 GC 调节

以下参数启用 Parallel 回收器:

  • -XX:+UseParallelGC:新生代 ParallelGC,老年代:Serial Old
  • -XX:+UseParallelOldGC:新生代 ParallelGC,老年代 ParallelOldGC

用于控制吞吐量的两个重要参数:

  • -XX:MaxGCPauseMills: 设置最大垃圾收集停顿时间。
  • -XX:GCTimeRatio: 设置吞吐量大小,范围 0 ~ 100。假设这个值是 n,那么默认不超过 1 / (1 + n) 的时间百分比用于垃圾收集,n 默认为 99

用于控制自适应调节 GC 的参数:

  • -XX:+UseAdaptiveSizePolicy: 新生代、eden 和 survivor 的比例会动态调整。

Serial Old

老年代串行收集器 Serial Old 采用标记-整理算法,也使用单线程进行垃圾回收。使用如下参数开启 Serial Old 回收器:

  • -XX:+UseSerialGC:新生代、老年代都使用 Serial 回收器 (老年代用的是 Serial Old)
  • -XX:+UseParNewGC:新生代采用 ParNew,老年代采用 Serial Old
  • -XX:+UseParallelGC:新生代采用 ParallelGC,老年代采用 Serial Old

Parallel Old

老年代 Parallel Old 回收器采用标记-整理算法,多线程进行垃圾回收。使用 -XX:+UseParallelOldGC 可以在新生代采用 Parallel,老年代采用 Parallel Old 收集器。参数 -XX:ParallelGCThreads 可以用于设置垃圾回收时的线程数量。

CMS

CMS 是一个基于标记-清除的算法,启用 CMS 的参数是 -XX:+UseConcMarkSweepGC,默认启动的工作线程数是 (ParallelGCThreads + 3) / 4。 CMS 不会等到堆内存饱和的时候才进行垃圾回收,而是当老年代的堆内存使用率达到某个阈值 -XX:CMSInitiatingOccupancyFraction 的时候便开始进行回收。CMS 基于标记-清除算法,因此执行垃圾回收完毕之后,会出现大量内存碎片,造成如果需要将内存分配给较大的对象,则必须被迫进行一次垃圾回收,以换取连续的内存空间。未解决这个问题,可以使用 -XX:+UseCMSCompactAtFullCollection 开关,使得 CMS 垃圾收集完毕之后,进行一次内存碎片整理;-XX:CMSFullGCsBeforeCompaction 参数可以用于设定进行多少次 CMS 回收后,执行一次内存压缩。

CMS 的代价:应用程序消耗更多的 CPU。

G1

使用 -XX:+UseG1GC 可以打开 G1 收集器开关。参数 -XX:MaxGCPauseMills 可以调整最大停顿时间,另外一个参数 -XX:ParallelGCThreads 可以设置并行回收时,GC 的工作线程数量。

G1 引入的目的是为了缩短处理超大堆的停顿时间

ZGC

FullGC

什么情况下会触发 FullGC ?

System.gc()

默认情况下(即未开启 -XX:+DisableExplictGC 参数的情况下),调用 System.gc() 会显示触发 FullGC,同时对新生代和老年代进行回收。

对象何时进入老年代

老年对象达到年龄

新生代的对象,每经历一次 GC,年龄加 1,当年龄的最大值最多达到 MaxTenuringThreshold (默认值 15) 的情况下,就可以晋升到老年代。

对象的实际晋升年龄是根据 survivor 区的使用情况动态计算的。

大对象

新生代空间无法容纳大对象,则会直接晋升到老年代。

参数 PretenureSizeThreshold 可以设置对象直接晋升到老年代的阈值,单位是字节,不过只对 SerialParNew 收集器有效,默认值为 0,即不指定最大晋升大小。

参考

  • 《深入理解 Java 虚拟机》
  • 《实战 Java 虚拟机》
  • 《码出高效:Java 开发手册》