垃圾回收

参考: 深入理解Java虚拟机 极致八股文之JVM垃圾回收器G1&ZGC详解

对象已死?

引用计数算法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* testGC()方法执行后,objA和objB会不会被GC呢?
* @author zzm
*/
public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /**
    * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过
    */
    private byte[] bigSize = new byte[2 * _1MB];
    public static void testGC() {
    ReferenceCountingGC objA = new ReferenceCountingGC();
    ReferenceCountingGC objB = new ReferenceCountingGC();
    objA.instance = objB;
    objB.instance = objA;
    objA = null;
    objB = null;
    // 假设在这行发生GC,objA和objB是否能被回收?
    System.gc();
    }
}

缺点:这两个对象互相引用,导致他们的 reference counting 始终不能为 0,无法被回收。

可达性分析算法

从GC Root出发,根据引用关系向下搜索,标记所有可以达到的对象。搜索过程中走过的路径叫做“引用链”。

🌟可作为GC Root的对象:

  1. 虚拟机栈栈帧中局部变量表中的对象(方法参数、临时变量、实例方法的this对象)
  2. 方法区中类静态变量
  3. 方法区中常量引用的对象
  4. 本地方法栈中 Native方法引用的对象
  5. 所有被 synchronized 持有的对象
  6. Java 虚拟机内部的引用:基本数据类型对应的 Class对象、常驻的异常对象、系统类加载器

即使被标记为不可达,也不是一定会被回收。

引用的概念

一般只用强引用和软引用,因为软引用可以加速JVM垃圾回收的速度。

  1. 强引用:Object obj=new Object(),垃圾回收器绝对不会回收它。
  2. 软引用:有用,但非必需。只要内存还够,就不会回收它。如果内存快满了,就回收它。
  3. 弱引用:只能生存到下一次垃圾回收。无论当前内存是否充足,都会回收它。 弱引用是通过java.lang.ref.WeakReference类来创建的。
  4. 虚引用:仅持有虚引用的对象相当于没有任何引用。虚引用只是用来跟踪该对象的引用情况的,因为每次回收对象内存时,如果它有虚引用,JVM会把虚引用加入引用队列,由此可知它将要被垃圾回收。

回收类和常量

(在方法区中)回收类需要满足三个条件

  1. 该类的所有实例都被回收
  2. 加载它的类回收器被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

回收常量

分代收集理论

假说:

  1. 大部分对象都是朝生夕死的
  2. 熬过越多次垃圾收集过程的对象就越难以消亡

部分收集:

  1. 新生代收集(Minor GC/Young GC):目标只是新生代
  2. 老年代收集(Major GC/Old GC)
  3. 混合收集(Mixed GC):收集整个新生代和部分老年代

整堆收集:回收整个堆和方法区。

标记-清除算法

首先标记不需要被回收的对象,接着把没有被标记的对象全部回收。

缺点:效率低下、会产生大量不连续的内存碎片。

标记-复制算法

Alt text 每次只用内存的一半。标记完之后,把存活的对象移动到另一半内存去,然后把之前那半内存全部回收。

现代商用虚拟机多采用这种算法来回收新生代。由于对象朝生夕死的特点,新生代中分为Eden, Survivor1, survivor2,大小比例为8:1:1。每次只使用Eden和一个 Survivor,等到发生垃圾回收,把 Eden里面和Survivor里面存货的对象全部复制到另一个Survivor中。

缺点:可用内存空间变小。

标记-整理算法

适合老年代。与标记-清除算法的不同仅在于,该算法在回收垃圾之后,把剩余的对象移动到内存连续的空间。

经典垃圾收集器

Alt text

Serial收集器

历史悠久的单线程收集器,并且它执行的时候会暂停其它所有用户线程。新生代采用标记-复制,老年代采用标记-整理。

ParNew收集器

Serial收集器的多线程版本。

Parallel Scavenge收集器

关注吞吐量。JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old。

吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)

-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间。

降低停顿时间,通过减小新生代的内存大小。但这样垃圾回收会更频繁,吞吐量也会降低。

-XX:GCTimeRatio:设置吞吐量大小。

一个大于0小于100的整数,代表垃圾收集占用时间的百分比。

Serial Old收集器

Serial 收集器的老年代版本。

Parallel Old收集器

Parallel Scavenge收集器的老年代版本。

CMS收集器

目标是让回收停顿时间最短。采用标记-清除算法,是Hotspot第一款做到并发的垃圾收集器。

  1. 初始标记🌟:标记与root直接相连的对象,停顿一下。
  2. 并发标记:并发地标记所有对象,不需要停顿用户线程。
  3. 重新标记🌟:并发标记的时候,用户线程可能队某些对象进行了更改,所以这里补充一次,会停顿稍长时间。
  4. 并发清除:不需要停顿用户线程,但是会产生内存碎片

G1 收集器(Garbage First)

面向服务器,同时满足高吞吐量和短停顿时间;JDK9开始的默认垃圾收集器。整体来看,采用标记-整理算法;局部来看,采用标记-复制算法,没有内存碎片。

与 CMS 的不同在于,G1建立起了可预测停顿模型,也就是在M毫秒的时间片之内,处理垃圾收集的时间大概率不超过N毫秒。

-XX:MaxGCPauseMillis=50

采用 Mixed GC 模式,可以把堆内存的任何部分组成一个回收集。它把 Java 堆分成许多个大小相等的region,每个 region 可以去扮演新生代或者老年代。

G1跟踪每个region中垃圾的价值(回收所获得的内存大小&回收所需时间的经验值),维护一个优先级列表,根据用户所设定的允许停顿时间,去优先处理价值大的 region。

  1. 初始标记🌟:短暂停顿,仅标记GC Root直接相连的对象
  2. 并发标记:与应用并发地执行,标记所有可达对象
  3. 最终标记🌟:短暂停顿,处理上一阶段结束后发生变化的对象
  4. 筛选回收🌟:暂停用户线程,多线程并行执行回收。对每个 region 的回收价值和回收成本进行排序,根据用户期望的停顿时间来制定回收计划。把仍然存活的region复制到另一块大region 中,清除之前的那块大region。

ZGC收集器

在对吞吐量影响不太大的前提下,实现在任意堆内存大小下,都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。 Alt text 把堆内存分成很多个不同大小的 page

面试问题

  1. G1的垃圾回收过程(伴随minorGC进行的初始标记、并发标记、重新标记、筛选回收)
  2. G1是怎么实现MaxGCPauseMillis的
  3. STW是如何实现的
  4. fullGC都GC哪些区域
  5. 为什么java8用元空间替换了永久代,增大永久代大小配置不行吗
  6. OOM、CPU 100% 如何排查