深入理解GC 回收机制与分代回收策略

垃圾回收(Garbage Collection,GC) 指的是JVM的自动内存管理机制,即:当堆内存中分配的对象所占的内存不再被引用时,就会触发JVM自动释放内存以让出对象所占用的存储空间,虽然 Java 不用手动管理内存回收,代码写起来很顺畅。但是你有没有想过,这些内存是怎么被回收的?

其实,JVM 是有专门的线程在做这件事情。当我们的内存空间达到一定条件时,会自动触发这个过程。而这个过程就叫作 GC,负责 GC 的组件,就叫作垃圾回收器。

按照语义上的意思,垃圾回收,首先就应该找到这些垃圾,然后回收掉。但是在GC 过程正好相反,它是先找到活跃的对象,然后把其他不活跃的对象判定为垃圾,然后清理掉。所以垃圾回收只与活跃的对象有关,和堆的大小无关,即:GC过程是逆向的。总体就只有以下几点:

  • 什么是垃圾?
  • 如何识别内存中该对象是垃圾?
  • 何时回收释放垃圾占用的内存(何时触发GC)?
  • 如何垃圾回收(垃圾收集算法)?

什么是垃圾?

垃圾 顾名思义就是JVM分配的内存不再被引用,即对象不在GCRoot引用链上了。 我们已经知道垃圾是什么了,但是JVM是如何识别该对象是垃圾?

如何识别该对象是垃圾?

引用计数算法(Reference Counting)

引用计数算法 就是在分配对象时,会额外为对象分配一段空间,用于记录指向该对象的引用个数。如果有一个新的引用指向该对象,则计数器加 1;当一个引用不再指向该对象,则计数器减 1 。当计数器的值为 0 时,则该对象为垃圾对象,我记得在Glide中正在被使用的ActivityResoure就是用的这个算法。

不过引用计数算法存在一个致命问题就是无法回收循环引用对象。当有两个对象相互引用时,由于它们互相引用对方所以计数都不为零,这就会导致这两个对象无法回收。所以,JVM采用的是另一种算法来判断对象是否存活:可达性分析算法

可达性分析算法(Reachability analysis algorithm)

可达性分析算法是从离散数学中的图论引入的,JVM 把堆内存中所有的对象之间的引用关系看作一张图,从GC Root对象为起始点,从这些节点开始搜索,搜索所走过的路径称为 引用链,在引用链上的对象就存活,而不在引用链上的对象就认定为可回收对象。如下图所示:

gcRoot.png

图中的ABCD/E对象引用与GC Root对象引用之间都存在一条直接或者间接的引用链,这也就说明它们与 GC Root引用链上是可达的,因此它们不是垃圾,是不能被GC回收掉的。而对象M和K虽然被对J 引用到,但是并不存在一条引用链连接它们与 GC Root,所以当 GC 进行垃圾回收时,只要遍历到 J/K/M 这 3 个对象,就会将它们回收。

注意:上图中圆形图标虽然标记的是对象,但实际上代表的是此对象在内存中的引用。包括 GC Root 也是一组引用而并非对象。这里的引用就好比C语言中内存地址。

什么GC Root 对象,哪些对象可以作为GC Root 对象引用?一般情况有以下几种,下面会通过代码去做验证:

1)、Java 虚拟机栈(局部变量表)中的引用的对象(正在运行的方法使用到的变量、参数等)。
2)、方法区中静态引用属性引用的对象(static关键字声明的字段)。
3)、仍处于存活状态中的线程对象。
4)、本地方法栈中引用的对象(Native 方法中 JNI 引用的对象)。

下面我们来通过代码验证能够作为GC Root的几种情况:
注意:为了验证能够作为GC Root的引用对象,从物理内存中分配出 200M 空间分配给 JVM 内存:java -Xms200m HelloWorld,需要记住的是我们验证的是能够作为GC Root,而GC Root就是当GC触发也不会被回收。

1)、Java 虚拟机栈(局部变量表)中的引用的对象(,正在运行的方法使用到的变量、参数)。

public class GCRootLocalVariable {
  private byte[] memory = new byte[100 * 1024 * 1024];

  public static void main(String[] args) {
      System.out.println("Start:");
      printMemory();
      method();
      System.gc();
      System.out.println("Second GC finish");
      printMemory();
}

  public static void method() {
      GCRootLocalVariable g = new GCRootLocalVariable();
      System.gc();
      System.out.println("First GC finish");
      printMemory();
  }

  /**
   * 打印出当前JVM剩余空间和总的空间大小
   */
  public static void printMemory() {
      System.out.print("free is " + Runtime.getRuntime().freeMemory() / 1024 / 1024 + " M, ");
      System.out.println("total is " + Runtime.getRuntime().totalMemory() / 1024 / 1024 + " M, ");
      }
  }

打印日志:

Start:
free is 189 M, total is 192 M, 
First GC finish
free is 90 M, total is 192 M, 
Second GC finish
free is 190 M, total is 192 M, 

结论:
(1)当第一次 GC时,即:System.gc()时,g 作为局部变量,引用了 new 出的对象(100M),并且g作为 GC Roots,在 GC 后并不会被 GC 回收。
(2)当第二次 GC:method() 方法执行完后,局部变量 g 跟随方法销毁,不再有引用类型指向该 100M 对象,所以第二次 GC 后此 100M 也会被回收。

注意:上面日志包括后面的实例中,因为有中间变量,所以会有 1M 左右的误差,但不影响我们分析 GC 过程。

2)、方法区中静态引用属性引用的对象,static关键字声明的字段。

public class GCRootStaticVariable {
   private static int _10MB = 10 * 1024 * 1024;
   private byte[] memory;
   private static GCRootStaticVariable staticVariable;
   GCRootStaticVariable memberVariable;

  public GCRootStaticVariable(int size) {
      memory = new byte[size];
  }

  public static void main(String[] args) {
      System.out.println("Start:");
      printMemory();
      GCRootStaticVariable g = new GCRootStaticVariable(4 * _10MB);
      g.memberVariable = new GCRootStaticVariable(2 * _10MB);
      GCRootStaticVariable.staticVariable = new GCRootStaticVariable(8 * _10MB);
      // 将g置为null, 调用GC时可以回收此对象内存
      g = null;
      System.out.println("Start GC before");
      printMemory();
      System.gc();
      System.out.println("GC Finish after");
      printMemory();
    }

  public static void printMemory() {
      System.out.print("free is " + Runtime.getRuntime().freeMemory() / 1024 / 1024 + " M, ");
      System.out.println("total is " + Runtime.getRuntime().totalMemory() / 1024 / 1024 + " M, ");
  }
}

打印日志:

Start:
free is 189 M, total is 192 M, 
Start GC before
free is 50 M, total is 192 M, 
GC Finish after
free is 110 M, total is 192 M,

结论:
程序刚开始运行时内存为 189M,并分别创建了 g 对象(40M),同时也初始化 g 对象内部的静态变量 staticVariable 对象(80M)和g对象成员变量(20M)memberVariable 。当调用 GC 时,只有 g 对象(40M) 和g成员变量memberVariable 被 GC 回收掉,因为memberVariable 并不是GC Root,而他的GC Root(g)已经被置为了null ,而静态变量 staticVariable 作为 GC Root,它引用的 80M 并不会被回收。静态变量作为类变量是随着类生命周期为存在的,

3)、仍处于存活状态中的线程对象。

public class GCRootThread {
private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];

public static void main(String[] args) throws Exception {
    System.out.println("开始前内存情况:");
    printMemory();
    AsyncTask at = new AsyncTask(new GCRootThread());
    Thread thread = new Thread(at);
    thread.start();

   (1) at = null; // 并没有会回收掉,所以证明 验证活跃线程可以作为GC Root
    System.gc();
    System.out.println("main方法执行完毕,完成GC");
    printMemory();

    thread.join();
   (2) at = null;
    System.gc();
    System.out.println("线程代码执行完毕,完成GC");
    printMemory();
}

private static class AsyncTask implements Runnable {
    private GCRootThread gcRootThread;

    public AsyncTask(GCRootThread gcRootThread){
        this.gcRootThread = gcRootThread;
    }

    @Override
    public void run() {
        try{
            Thread.sleep(500);
        } catch(Exception e){}
    }
}
}

打印日志:

free is 189 M, total is 192 M, 
main方法执行完毕,完成GC
free is 110 M, total is 192 M, 
线程代码执行完毕,完成GC
free is 190 M, total is 192 M, 

分析:

  • 通过第一点我们知道虚拟机栈局部变量是可以作为GC Root的,也就是说 at 可作为GC Root,在线程还没有结束的情况下,我在代码(1)处将at置为null,即:将GC Root的引用链给断了,但是at的成员变量(gcRootThread)并没有被GC回收掉,说明在这个例子中at 并不是GC Root。
  • 在线程已经结束的情况下,我在代码(1)处将at置为null,at的成员变量(gcRootThread)可以被GC回收掉,所
  • 通过上述两点在at置为null,线程是否结束足以证明活跃线程可以作为GC Root。

结论:
程序刚开始时是 189M 内存,当调用第一次 GC 时线程并没有执行结束,并且它作为 GC Root,所以它所引用的 80M 内存并不会被 GC 回收掉。 thread.join() 保证线程结束再调用后续代码,所以当调用第二次 GC 时,线程已经执行完毕并被置为 null,这时线程已经被销毁,所以之前它所引用的 80M 此时会被 GC 回收掉。

4)、成员变量可作为 GC Root

public class GCRootClassVariable {
private static int _10MB = 10 * 1024 * 1024;
private byte[] memory;
private GCRootClassVariable classVariable;

public GCRootClassVariable(int size) {
    memory = new byte[size];
}

public static void main(String[] args) {
    System.out.println("Start:");
    printMemory();
    GCRootClassVariable g = new GCRootClassVariable(4 * _10MB);
    g.classVariable = new GCRootClassVariable(8 * _10MB);
    g = null;
    System.gc();
    System.out.println("GC Finish");
    printMemory();
}
}

打印日志:

Start:
free is 189 M, total is 192 M, 
GC Finish
free is 190 M, total is 192 M,

从日志中可以看出当调用 GC 时,因为 g 已经置为 null,因此 g 中的成员变量 classVariable 此时也不再被 GC Root 引用链上。所以最后 g(40M) 和 classVariable(80M) 都会被回收掉。这也表明成员变量与静态变量不同,它不会被当作 GC Root。

何时回收释放垃圾占用的内存(何时触发GC)

1)、Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
2)、System.gc():开发者主动调用System.gc()来请求一次 GC。

如何回收垃圾

标记-清除算法(Mark and Sweep GC)

GC Roots集合开始,先扫描整个堆内存,标出所有被 GC Roots 直接或间接引用到的对象,然后执行 sweep 操作回收不可到达对象,过程分两步:
1)、Mark 标记阶段:先找到堆内存中的所有 GC Root 对象,从GC Roots集合开始,只要是和 GC Root 对象直接或者间接相连则标记为灰色,即:存活对象,否则标记为黑色,即:垃圾对象。
2)、Sweep 清除阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清除。

如下图所示:


mark and sweep.png

但是这种简单的清除方式,有一个明显的弊端,那就是内存碎片问题。

比如我申请了 1k、2k、3k、4k、5k 的内存。


mark.jpg

由于某种原因 ,2k 和 4k 的内存,我不再使用,就需要交给垃圾回收器回收。


mark2.jpg

理论上来说我应该有足足 6k 的空闲空间。那么接下来申请5k 的空间,结果系统告诉我内存不足了。系统运行时间越长,这种碎片就越多。

复制算法(Copying)

将现有内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收,即:提供一个对等的内存空间,将存活的对象复制过去,然后清除原内存空间。

1)、复制算法之前,内存分为 A/B 两块,并且当前只使用内存 A,内存的状况如下图所示:


coping_before.png

2)、标记完之后,所有可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存。内存状况如下图所示:
coping_after.png

这种方式看似非常完美的解决了内存碎片问题。但是,它的弊端也非常明显。它浪费了几乎一半的内存空间来做这个事情,如果资源本来就很有限,这就是一种无法容忍的浪费。

  • 优点:按顺序分配内存即可,实现简单、运行高效,不会产生内存碎片。
  • 缺点:可用的内存大小缩小为原来的一半(所以官方引入Eden、Survivor区优化的原因),对象存活率高时会频繁进行复制。
标记-压缩算法

需要先从GC Root开始对所有可达对象做一次标记,然后,它并不是简单地清理未标记的对象,这点和标志清除算法是有区别的,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。因此标记压缩也分两步完成:

1)、Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
2)、Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。


mark_compat.png
  • 优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
  • 缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。

JVM分代回收策略

上面介绍的垃圾回收算法每个都有优缺点,没有最好算法只有合适的算法,所以JVM引入的分代垃圾回收策略,根据不同的区域使用不同的算法。

JVM根据对象存活的周期不同,堆中的内存可以划分为新生代(Young Generation)、老年代(Old Generation),而新生代则又可以细分为Eden 区、From Survivor 区、To Survivor 区,这就是 JVM 的内存分代策略。

JVM将堆区划分成这么多的区域,主要是为了提高垃圾收集器(GC)对对象进行管理的效率,这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,进而提高垃圾回收效率。

注意: 在 HotSpot 中除了新生代和老年代,还有永久代。

分代回收的中心思想就是:对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短。如果经过多次回收仍然存活下来,则将它们转移到老年代中。

年轻代(Young Generation)

新生成的对象会优先存放在新生代中,新生代的对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是复制算法

年轻代细分为 3 部分:Eden、Survivor0(简称 S0)、Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代。这 3 块区域的内存分配过程如下:

绝大多数刚刚被创建的对象会存放在 Eden 区,非常大的对象会直接放到老年代中。如图所示:


young.png

当 Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1是空的。如图所示:


young1.png

下一次 Eden 区满时,再执行一次垃圾回收。此次会将 Eden和 S0区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0变为空。如图所示:
young2.png

如此反复在 S0 和 S1之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。如图所示:


young3.png

需要注意的是: 每次s0和s1之间交换时,都会保证一方中没有任何剩余对象,比如从s0转移到s1时,如果s1的空间不足以装下所有此时s0中的对象,则直接转移到老年代。

年老代(Old Generation)

一个对象如果在新生代存活了足够长的时间而没有被GC清理掉,则会被复制到老年代,当然这只是其中一种情况会被复制到老年代。

老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

我们可以使用-XX:PretenureSizeThreshold参数控制直接升入老年代的对象大小,大于这个阈值的对象会直接分配在老年代上。老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。

老年代可能存在这么一种情况,就是老年代中的对象引用到新生代对象。这时如果要执行新生代 GC,则可能需要遍历整个老年代上可能存在引用新生代的情况,这显然是低效的。所以,老年代中维护了一个 512 byte 的 card table,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个 card table 即可,大大提高了性能。

GC日志分析

为了让上层应用开发人员更加方便的调试 Java 程序,JVM 提供了相应的 GC 日志。在 GC 执行垃圾回收事件的过程中,会有各种相应的 log 被打印出来。其中新生代和老年代所打印的日志是有区别的。

  • 新生代 GC:这一区域的 GC 叫作 Minor GC。因为新生代的Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • 老年代 GC:发生在这一区域的 GC 也叫作 Major GC 或者 Full GC。当出现了 Major GC,经常会伴随至少一次的 Minor GC。

首先我们需要理解几个 Java 命令的参数:

命令参数 功能描述
-verbose:gc 显示gc的操作内容
-Xms10m 初始堆大小为 10m
-Xmx10m 设置堆的最大分配内存为10m
-Xmn10m 设置新生代的内存大小为10m
-XX:+PrintGCDetails 打印GC的详细log日志
-XX:SurvivorRatio=8 新生代中的Eden区域和Survivor区域的带下比值为8:1:1

下面使用代码查看GC的日志,在内存中创建 4 个 byte 类型数组来演示内存分配与 GC 的详细过程。代码如下:

//VM agrs:
// -Xms20M  初始堆大小为20M
// -Xmx20M  堆最大分配内存20M
// -Xmn10M  新生代内存大小
// -XX:+PrintGCDetails
// -XX:SurvivorRatio=8 新生代比例
public class MinorGCTest {
private static final int _1MB = 1024 * 1024;

public static void testAllocation() {
      byte[] a1, a2, a3, a4;
      a1 = new byte[2 * _1MB];
      a2 = new byte[2 * _1MB];
      a3 = new byte[2 * _1MB];
      a4 = new byte[1 * _1MB];
  }
    public static void main(String[] agrs) {
        testAllocation();
    }
}

因为使用的intelLij 开发工具,我直接设置java命令参数为:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8。通过上面的参数,可以看出堆内存总大小为 20M,其中新生代占 10M,剩下的 10M 会自动分配给老年代。执行上述代码打印日志如下:

 Heap
 PSYoungGen   total 9216K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   eden space 8192K, 100% used [0x00000000ff600000,0x00000000ffe00000,0x00000000ffe00000)
   from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
   to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
   object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
 Metaspace       used 2670K, capacity 4486K, committed 4864K, reserved 1056768K
    class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

其中日志中的各字段的含义如下:

字段 含义
PSYoungGen 新生代
eden 新生代的Eden区域
from 新生代的Survivor1区域
to 新生代的Survivor2区域
ParOldGen 老年代
Metaspace 元数据区域

从日志中可以看出:程序执行完之后,a1、a2、a3、a4 四个对象都被分配在了新生代的 Eden 区(eden space 8192K, 100% used )

如果我们将测试代码中的 a4 初始化改为 a4 = new byte[2 * _1MB] 则打印日志如下:

[GC (Allocation Failure) [PSYoungGen: 7128K->704K(9216K)] 7128K->6856K(19456K), 0.0033750 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 704K->0K(9216K)] [ParOldGen: 6152K->6673K(10240K)] 6856K->6673K(19456K), [Metaspace: 2664K->2664K(1056768K)], 0.0042054 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
PSYoungGen      total 9216K, used 2130K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
 eden space 8192K, 26% used [0x00000000ff600000,0x00000000ff814930,0x00000000ffe00000)
 from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen       total 10240K, used 6673K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
 object space 10240K, 65% used [0x00000000fec00000,0x00000000ff2846b0,0x00000000ff600000)
Metaspace       used 2670K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

从日志中可以看出:分配内存是GC (Allocation Failure)失败了的。
这是由于 在分配a4 内存之前,Eden 区已经被占用 6M(a1,a2,a3),已经无法再分配出 2M 的内存来存储 a4 对象,所以造成的分配内存失败,因此会执行一次 Minor GC。并尝试将存活的 a1、a2、a3 复制到 S1 区。但是 S1 区只有 1M 空间,所以没有办法存储 a1、a2、a3 任意一个对象。在这种情况下 a1、a2、a3 将被转移到老年代,最后将 a4 保存在 Eden 区。所以最终结果就是:Eden 区占用 2M(a4),老年代占用 6M(a1、a2、a3)。当然如果Eden区也不够存放a4,那么a4也会直接放到老年代中。

Java 四大引用

上面说过,判断对象是否存活我们是通过GC Roots的引用可达性来判断的。但是在Java中引用关系有四种,根据引用强度的由强到弱,分别是:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)

以下示例设置JVM的参数:-Xms20M 初始堆大小为20M -Xmx20M 堆最大分配内存20M

强引用

JVM中的引用默认就是强引用,任何一个对象的赋值操作就产生了对这个对象的强引用。

public class StrongReferenceTest {
private static final int _1MB = 1024 * 1024;
byte[] a1 = new byte[11 * _1MB];

public static void main(String[] args){
    //先打印内存
    printMemory();
    // 创建强引用  11M
    StrongReferenceTest strongReference = new StrongReferenceTest();
    //gc完成之后再打印内存
    printMemory();
    // 来一次 gc
    System.gc();
    //堆只有9M JVM 宁愿抛出OutOfMemoryError 运行时错误让程序异常终止,也不会回收强引用所指向的对象实例
    byte[] ref1 = new byte[8 * _1MB];
}
}

输出日志:

free is 18 M, total is 19 M, 
free is 7 M, total is 19 M, 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.gituhub.jvm.StrongReferenceTest.main(StrongReferenceTest.java:22)

上面代码我设置了堆大小只有20M,而我们创建了strongReference 就使用了11M,当我们尝试创建 ref1(8M)时堆内存是明显不够的,这时会触发GC,但是由于 strongReference 是强引用 不能被回收,最后JVM直接抛出OutOfMemoryError。

强引用所引用的对象实例不会被 GC 回收,JVM 宁愿抛出OutOfMemoryError 运行时错误让程序异常终止,也不会回收强引用所指向的对象实例。

软引用

软引用是一种相对强引用弱化了一些的引用,用java.lang.ref.SoftReference实现,可以让对象豁免一些垃圾收集。当系统内存充足的时候,不会被回收;当系统内存不足的时候,会被回收。

软引用一般用于对内存敏感的程序中,比如高速缓存。

public class SoftReferenceTest {
public static void main(String[] args) {
    //先打印内存
    printMemory();
    // 创建强引用  11M
    RefObject object =
            new RefObject(11);
    // 因为object是强引用 为不影响 测试软引用 把object = null


    // 使用软引用 引用 object对象
    SoftReference softReference =
            new SoftReference<>(object);

    object = null;//断开强引用


    System.out.println("softReference = " + softReference.get());
    System.out.println("object = " + object);

    //gc完成之后再打印内存
    printMemory();
    // 来一次 gc
    System.gc();
    printMemory();

    try {
        //堆只有9M JVM 宁愿抛出OutOfMemoryError 运行时错误让程序异常终止,也不会回收强引用所指向的对象实例
        RefObject object8M = new RefObject(8);
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    } finally {
        System.out.println("softReference = " + softReference.get());
        System.out.println("object = " + object);
        printMemory();
    }
}

}

输出日志:

free is 18 M, total is 19 M, 
softReference = com.gituhub.jvm.RefObject@15db9742
object = null
free is 7 M, total is 19 M, 
free is 7 M, total is 19 M, 
softReference = null
object = null
free is 10 M, total is 19 M, 

为了避免强引用的干扰我将object置为null。

我们创建了object 就使用了11M,此时内存时充足的,我们收动调用请求GC,但是softReference.get()并不为null,说明软引用所引用的对象内存没有被回收。
但是当我们申请 object8M时,此时系统明显内存不足够分配8M空间给object8M对象,这是系统主动会触发GC,最后将软引用所引用的对象回收,在分配8M空间给object8M对象。

可以看出在内存充足的情况下,SoftReference引用的对象是不会被回收的,相反在内存不足时,SoftReference引用的对象就会被回收掉。

弱引用

WeakReference和SoftReference很类似,不同的是WeakReference所引用的对象只要垃圾回收执行,就会被回收,而不管是否内存不足。

public class WeekReferenceTest {
public static void main(String[] args) {
    //先打印内存
    printMemory();
    // 创建强引用  11M
    RefObject object = new RefObject(11);

    WeakReference softReference = new WeakReference<>(object);
    // 因为object是强引用 为不影响 测试软引用 把object = null
    object = null;//断开强引用


    System.out.println("softReference = " + softReference.get());
    System.out.println("object = " + object);

    //gc完成之后再打印内存
    printMemory();
    // 来一次 gc
    System.gc();
    printMemory();

    System.out.println("softReference = " + softReference.get());
    System.out.println("object = " + object);

}
}

输出日志:

free is 18 M, total is 19 M, 
softReference = com.gituhub.jvm.RefObject@15db9742
object = null
free is 7 M, total is 19 M, 
free is 18 M, total is 19 M, 
softReference = null
object = null

我们看到gc过后,弱引用的对象被回收掉了。

引用队列ReferenceQueue

我们希望当对象被GC回收后能够通知用户线程,然后用户可进行额外的处理。对于SoftReferenceWeakReference它构造函数中就有接受ReferenceQueue参数,当对象j被gc回收之后,Reference对象会被放入关联的ReferenceQueue中。我们可以从ReferenceQueue中获取到Reference信息,同时进行额外的处理,比如:流行的图片处理框架Glide 就使用这种机制,Glide 使用一个HashMap管理所有正在使用的WeakReference,并且开启一个后台线程监控 ReferenceQueue 被WeakReference包装的对象是否被GC回收,如果被回收了那么就需要将对应的WeakReference移除HashMap。下面的是我模拟Glide写的一段示例代码:

public class ReferenceQueueTest {
private static volatile boolean isShutdown = false;
private static Map cache = new HashMap<>();
private static ReferenceQueue referenceQueue = new ReferenceQueue<>();

public static void main(String[] args) throws InterruptedException {
    MemoryUtil.printMemory("Main method run Start");

    ReferenceObject referenceObject10M;
    ReferenceObjectWeakReference weakReference;

    for (int i = 0; i < 3; i++) {
        referenceObject10M =
                new ReferenceObject(i + 2);//2 + 3 + 4

        int hashCode = referenceObject10M.hashCode();
        weakReference =
                new ReferenceObjectWeakReference(hashCode, referenceObject10M, referenceQueue);

        cache.put(hashCode, weakReference);
    }

    referenceObject10M = null;
    weakReference = null;


    // 开启后台线程监控资源回收
    Thread thread = new Thread(() -> {
        while (!isShutdown) {
            try {
                ReferenceObjectWeakReference reference = (ReferenceObjectWeakReference) referenceQueue.remove();
                cache.remove(reference.key);
                MemoryUtil.printMemory("Object is recycled and Join RefQueue");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        MemoryUtil.printLog("Monitor thread finish");
    });

    thread.setDaemon(true);
    thread.setName("GC Monitor");
    thread.start();

    MemoryUtil.printMemory("GC before");
    Thread.sleep(1000);
    System.gc();
    Thread.sleep(5000);
    isShutdown = true;
    MemoryUtil.printMemory("GC after");
    MemoryUtil.printMemory("Main method run finish : cache size:  " + cache.size());
}


static class ReferenceObjectWeakReference extends WeakReference {
    public int key;

    public ReferenceObjectWeakReference(
            int key,
            ReferenceObject referent,
            ReferenceQueue q) {
        super(referent, q);
        this.key = key;
    }
}
}

总结

这篇文章很长,主要介绍了使用可达性分析来判断对象是否可以被回收,以及 3 种垃圾回收算法。最后通过分析 GC Log 验证了 Java 虚拟机中内存分配及分代策略的一些细节,以及JVM中四种引用将了三种。

你可能感兴趣的:(深入理解GC 回收机制与分代回收策略)