首页 > 代码库 > Java 中的垃圾回收策略

Java 中的垃圾回收策略

垃圾回收需要解决的问题

  • 谁需要被回收
  • 什么时候回收
  • 怎么回收

谁需要被回收

如果一个对象再也不会被用到,就可以回收它了,所以关键在于如何知道一个对象再也不被使用了。

引用计数

当一个对象被引用时,引用计数加1,当引用失效时,计数减1。简单直观,但会出现循环引用问题。

a.tb = b
b.ta = a

即使 a 和 b 再也不会被用到了,但他们之间互相引用,导致引用计数一直不为0,无法被回收。

可达性分析

Java, C# 的主流实现都是使用可达性分析来判断一个对象是否存活的。如果从 GC Root 可以到达一个对象,那么 这个对象是可达的,还存活着,否则就不存活,可以被回收。 GC Root 包含以下几类。

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

何时及如何回收

何时及如何回收,涉及具体的回收策略,就放一起说了。

标记回收算法

最基本的垃圾回收算法,第一次扫描,标记所有可以被回收的对象,第二次扫描,回收被标记的对象。

不足之处

  • 效率低
  • 会产生内存碎片

复制算法

复制算法将内存分成两部分,每次只使用两部分。假如当前使用的是左半块,右半块没使用。现在要垃圾回收了, 就将左半块还存活的对象复制到右半块。然后将左半块一次性全部回收。

好处是

  • 高效
  • 没内存碎片

坏处是

  • 空间效率低,有半块都不能用

解决方法是,调整左右两块的比例。在 HotSpot 中,内存分成一块 Eden,两块 Survivor,比例是 8:1. 每次使用一块 Eden, 一块 Survivor A, 另一块 Survivor B 备用。 垃圾回收时,将 Eden 和 Survivor A 上存活的对象复制到 Survivor B 上,然后将 Eden 和 Survivor A 回收。然后下次使用 Eden, Survivor B, 而 Survivor A 这次备用。

这种方法的前提是每次回收时,大量对象都死掉了,只有一小部分存活着,这样,只复制一小部分就好。但是,如果大量对象存活时间比较长,就要反复来回复制,简直浪费生命。从这一点也可以看出,要根据对象存活时间的特点使用不同的回收策略。

标记整理算法

标记整理算法是对标记清除算法的改进。第一次扫描标记需要回收的对象。第二次不是将这些对象清除,而是将还存活的对象移动到内存区域的一端,全他们连在一起。然后对这块区域边界以外的地方全部回收。

分代收集算法

从上面的讨论可以看出,要根据对象存活时间使用不同回收策略。有些对象存活时间比较短,这样每次回收时,存活对象少,可以使用复制算法。而有些对象存活时间比较长,这样每次回收时,需要回收的对象少,可以使用标记清除和标记整理算法。Java 堆据此分为新生代和老年代,新生代中对象存活时间短,老年代中对象存活时间长。

分代的标准有两个,一个是存活时间,一个是对象大小,大对象会直接进入老年代。

HotSpot 垃圾回收器

HotSpot 垃圾回收器按分代,可以分为新生代回收器和老年代回收器。按垃圾回收器线程数可以分为单线程回收器和多线程回收器。按垃圾回收时是否需要停止所有工作线程可以分为并发回收器和非并发回收器。所以,看到一个回收器,需要明白它:

  • 用于新生代还是老年代
  • 单线程还是多线程
  • 工作时是否需要停止所有工作线程

Serial 回收器

  • Client 模式下默认新生代回收器,采用复制算法
  • 单线程
  • 需要暂停所有工作线程

ParNew 回收器

  • Server 模式下首选新生代回收器,采用复制算法
  • 多线程
  • 不需暂停所有工作线程
  • 只有它可与 CMS 回收器配合使用

Parallel Scavenge 回收器

  • 新生代回收器,采用复制算法
  • 多线程
  • 目标在于达到一个可控的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾回收时间))

其它回收器目标在于减少用户程序因为垃圾回收而停顿的时间,而Parallel Scavenge 回收器 的目标在于可按的吞吐量,因为它又被称为吞吐量优先回收器。 这里有一个矛盾,如果每次停顿时间减少,那么用户程序可以得到更快的响应,但这同 时意味着,垃圾回收变得频繁,垃圾回收总体时间变长,吞吐量下降。可以使用 -XX:MaxGCPauseMillis 调整垃圾回收停顿时间,也可以使用 -XX:GCTimeRation 调整吞吐量。

Serial Old 回收器

  • 老年代回收器,采用标记整理算法
  • 单线程
  • 需要暂停所有工作线程
  • Client 模式下使用
  • Serial 回收器老年代版本

Parallel Old 收集器

  • 老年代回收器,采用标记整理算法
  • 多线程
  • Parallel Scavenge 老年代版本

CMS(Concurrent Mark Sweep) 回收器

  • 老年代回收器,采用标记清除算法
  • 多线程
  • 总体上,不需要暂停所有工作线程

运行过程:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

其中并发标记和重新标记需要暂停所有用户线程,但这两个阶段用时相对另外两个阶段非常少。而并发标记与并发清除消耗时间长,但他们可以与 用户线程一起工作。

初始标记只标记 GC Root 直接关联的对象,重新标记修正并发标记期间因用户程序运行导致标记变动的对象。

G1(Garbage-First) 回收器

  • 新生代与老年代不再物理隔离,打破原有分代概念
  • 多线程
  • 不需要暂停所有工作线程
  • 不需要与其它回收器配合使用
  • 整体上标记整理算法,局部是复制算法,不会产生内存碎片
  • 可以预测停顿,这意味着用户可以指定回收操作在多长时间内完成

这篇文章是学习《深入理解Java虚拟机》的总结。

Java 中的垃圾回收策略