天天看点

四种引用状态

在JDK1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种情况下只有被引用或者没有被引用两种状态,对于如何描述一些"食之无味,弃之可惜"的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的引用场景。

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4中引用强度一次减弱。

  • 强引用就是指在程序代码之中普遍存在的,类似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用但并非必需的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
  • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象,只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
  • 虚引用也成为幽灵引用或者幻影引用,它是最弱的一中引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供给了PhantomReference类来实现虚引用

写于代码开始前

在通过代码研究几种引用状态之前,先定义一些参数,后面所有部分的代码示例都使用这些参数。

首先是JVM的参数,这里我使用的是:

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseParNewGC -verbose:gc -XX:+PrintGCDetails      

这意味着:

  • 堆大小固定为20M
  • 新生代大小为10M,SurvivorRatio设置为8,则Eden区大小=8M,每个Survivor区大小=1M,每次有9M的新生代内存空间可用来new对象
  • 新生代使用使用ParNew收集器,Server模式下默认是Parallel收集器,不过这个收集器的GC日志我看着没有ParNew收集器的GC日志舒服,因此就改成ParNew收集器了
  • 当发生GC的时候打印GC的简单信息,当程序运行结束打印GC详情

其次,再定义一个常量类"_1MB":

private static final int _1MB = 1024 * 1024;      

 1.强引用

先运行一个空方法:

public static void testStrongReference() {

}      

运行结果为:

Heap
 par new generation   total 9216K, used 2162K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee1cad8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3226K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K      

可以知道,在未手动添加任何变量的情况下,新生代为2162K。

下面为该方法添加4M的数组。

public static void testStrongReference() {
    byte[] bytes = new byte[4 * _1M];
    // 触发一次gc
    System.gc();
}      

运行结果为:

Heap
 par new generation   total 9216K, used 82K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   1% used [0x00000000fec00000, 0x00000000fec14920, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4729K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  46% used [0x00000000ff600000, 0x00000000ffa9e780, 0x00000000ffa9e800, 0x0000000100000000)
 Metaspace       used 3225K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K      

在触发了gc之后,老年代的空间变为4729K,而原来的新生代也只有2162K,因此4M的bytes数组并没有被移除,可以知道,一部分无用的对象被gc掉了,而被bytes强引用的4M的数组移入到了老年区。

那么,如果我们把强引用断开(置为null),看看下面的结果:

public static void testStrongReference() {
    byte[] bytes = new byte[4 * _1M];
    // 断开引用
    bytes = null;
    // 触发一次gc
    System.gc();
}      

运行结果:

Heap
 par new generation   total 9216K, used 166K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   2% used [0x00000000fec00000, 0x00000000fec299a0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 609K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   5% used [0x00000000ff600000, 0x00000000ff698560, 0x00000000ff698600, 0x0000000100000000)
 Metaspace       used 3224K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K      

当把bytes置空后,并手动触发GC之后,老年代只剩下609K。因为此时4M数组没有了强引用,它也就被垃圾收集器回收掉了。

由这个例子我们回顾可以作为GC Roots的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,比如在方法中定义"Object obj = new Object();"
  • 方法区中类静态属性引用的对象,比如在类中定义"private static Object lock = new Object();",将Object对象作为一个锁,所有类共享
  • 方法区中常量引用的对象,比如在接口中定义"public static final char c = 'a';",字符'a'是一个常量
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象,这个不好找例子

这次的回收正是因为第一条。本身有bytes(在虚拟机栈中)指向4M的byte数组,由于将bytes置空。因此4M的byte数组此时没有任何一个可以作为GC Roots对象的引用指向它,即4M的byte数组被虚拟机标记为可回收的垃圾,在GC时被回收。

稍微扩展一下,这里上面代码的做法是手动将bytes置空,其实方法调用结束也是一样的,栈帧消失,栈帧消失意味着bytes消失,那么4M的byte数组同样没有任何一个可以作为GC Roots对象的引用指向它,因此方法调用结束之后,4M的byte数组同样会被虚拟机标记为可回收的垃圾,在GC时被回收。

2.软引用

JDK提供了SoftReference类共开发者使用,那我们就利用SoftReference研究一下软引用,测试代码为:

public static void testSoftReference() throws InterruptedException {
    byte[] bytes = new byte[4 * _1M];
    SoftReference<byte[]> sr0 = new SoftReference<>(bytes);
    System.out.println("before GC: " + sr0.get());
    // 4M的数组失去了强引用,只剩下弱引用
    bytes = null;
    System.gc();
    System.out.println("after GC: " + sr0.get());
}      

运行结果:

before GC: [[email protected]

[Full GC (System.gc()) [Tenured: 0K->4731K(10240K), 0.0036269 secs] 6094K->4731K(19456K), [Metaspace: 3221K->3221K(1056768K)], 0.0036821 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

after GC: [[email protected]      
Heap
 par new generation   total 9216K, used 237K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   2% used [0x00000000fec00000, 0x00000000fec3b500, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4731K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  46% used [0x00000000ff600000, 0x00000000ffa9ec08, 0x00000000ffa9ee00, 0x0000000100000000)
 Metaspace       used 3228K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K      

从结果看出,在gc前后,从sr0中都可以获取到4M数组,老年区还有4731K的空间,gc并没有因为bytes置空把4M的数组回收。

当bytes = null之后,4M的byte数组失去了强引用,那么,在没有弱引用的情况下,它会像上面最后一个例子那样被回收掉,然后这里没有被回收,因为4M数组仍然拥有一个SoftReference,在堆空间足够的情况下它是不会被回收的,只是复制到老年区了。

上面是在内存足够的情况下,那么,如果堆内存不够呢?

由于我们的堆空间总共才20M,所以肯定不够空间存放下面24M的对象的。

/**
 * 测试堆内存不够的情况下弱引用的回收情况
 */
public static void testSoftReference() {
    byte[] bytes = new byte[4 * _1M];
    SoftReference<byte[]> sr0 = new SoftReference<>(new byte[4 * _1M]);
    SoftReference<byte[]> sr1 = new SoftReference<>(new byte[4 * _1M]);
    SoftReference<byte[]> sr2 = new SoftReference<>(new byte[4 * _1M]);
    SoftReference<byte[]> sr3 = new SoftReference<>(new byte[4 * _1M]);
    SoftReference<byte[]> sr4 = new SoftReference<>(new byte[4 * _1M]);
    SoftReference<byte[]> sr5 = new SoftReference<>(new byte[4 * _1M]);
    System.out.println(sr0.get());
    System.out.println(sr1.get());
    System.out.println(sr2.get());
    System.out.println(sr3.get());
    System.out.println(sr4.get());
    System.out.println(sr5.get());
}      

运行结果:

1 null
 2 null
 3 null
 4 null
 5 [[email protected]
 6 [[email protected]
 7 Heap
 8  par new generation   total 9216K, used 4337K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
 9   eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff03b510, 0x00000000ff400000)
10   from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400f70, 0x00000000ff500000)
11   to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
12  tenured generation   total 10240K, used 8803K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
13    the space 10240K,  85% used [0x00000000ff600000, 0x00000000ffe98c60, 0x00000000ffe98e00, 0x0000000100000000)
14  Metaspace       used 3229K, capacity 4500K, committed 4864K, reserved 1056768K
15   class space    used 350K, capacity 388K, committed 512K, reserved 1048576K      

从1-6行的代码来看,前四个数组都被回收了,只剩下后两个数组,说明被软引用关联的对象会在堆内存不足的情况下被回收。

如果是强引用呢?

首先,老年代可以放2个4M数组,Eden区可以放1个,那么当要new第4个数组的时候,就会抛出outOfMemoryError。

而在软引用的情况下,则可以无限创建,因为gc在内存不够的情况下会自动回收。

3.弱引用

JDK给我们提供的了WeakReference用以将一个对象关联到弱引用

public static void testWeakReference() {
    byte[] bytes = new byte[_1M * 4];
    WeakReference<byte[]> wr = new WeakReference<>(bytes);
    System.out.println("before GC: " + wr.get());
    bytes = null;
    System.gc();
    // 弱引用,gc之后就直接回收了
    System.out.println("after GC: " + wr.get());
}      

运行结果:

before GC: [[email protected]
[Full GC (System.gc()) [Tenured: 0K->635K(10240K), 0.0022898 secs] 6094K->635K(19456K), [Metaspace: 3222K->3222K(1056768K)], 0.0023307 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
after GC: null
Heap
 par new generation   total 9216K, used 237K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   2% used [0x00000000fec00000, 0x00000000fec3b500, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 635K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   6% used [0x00000000ff600000, 0x00000000ff69eca8, 0x00000000ff69ee00, 0x0000000100000000)
 Metaspace       used 3228K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K      

gc之后,bytes置空之后,跟软引用不同,弱引用对象直接就被清除了,堆空间也没有看到有4M的对象,而软引用

4.Reference与ReferenceQueue

在jdk中,对ReferenceQueue的注释:

Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected.      

翻译过来就是,引用队列,在检测到合适的可达性变化之后,垃圾收集器将已注册的引用对象添加到该队列中。

意思就是,当垃圾收集器检测到引用变更之后,它就会把那些需要清除的软引用或弱引用添加到ReferenceQueue中,表示这个引用关联的对象要被回收。

  • SoftReference、WeakReference、PhantomReference,在构造的时候可以通过构造函数传入一个ReferenceQueue,但是只有PhantomReference,ReferenceQueue是必须的
  • 以SoftReference为例,一个类型为SoftReference的sr关联了一个4M的byte数组,那么当内存不够的时候,回收此4M的byte数组,sr.get()为null,表示sr不再关联此4M的byte数组
  • 当sr对应的4M的byte数组被回收之后,sr本身被加入ReferenceQueue中,表示此软引用关联的对象被回收
  • ReferenceQueue本身是一个Queue,可通过poll()方法不断拿到队列的头元素,如果是null表示没有被回收的软引用关联的对象,如果不是null表示有软引用关联的对象被回收
  • SoftReference是这样的,WeakReference与PhantomReference同理
public static void testSoftReferenceQueue() {

    ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
    SoftReference<byte[]> sr0 = new SoftReference<>(new byte[4 * _1M],queue);
    SoftReference<byte[]> sr1 = new SoftReference<>(new byte[4 * _1M],queue);
    SoftReference<byte[]> sr2 = new SoftReference<>(new byte[4 * _1M],queue);
    SoftReference<byte[]> sr3 = new SoftReference<>(new byte[4 * _1M],queue);
    SoftReference<byte[]> sr4 = new SoftReference<>(new byte[4 * _1M],queue);
    SoftReference<byte[]> sr5 = new SoftReference<>(new byte[4 * _1M],queue);

    System.out.println(sr0 + " --> " + sr0.get());
    System.out.println(sr1 + " --> " + sr1.get());
    System.out.println(sr2 + " --> " + sr2.get());
    System.out.println(sr3 + " --> " + sr3.get());
    System.out.println(sr4 + " --> " + sr4.get());
    System.out.println(sr5 + " --> " + sr5.get());

    System.out.println(queue.poll());
    System.out.println(queue.poll());
    System.out.println(queue.poll());
    System.out.println(queue.poll());
    System.out.println(queue.poll());
    System.out.println(queue.poll());
}      

运行结果:

[email protected] --> null
[email protected] --> null
[email protected] --> null
[email protected] --> [[email protected]
[email protected] --> [[email protected]
[email protected] --> [[email protected]

// 已经被回收对象的引用
[email protected]
[email protected]1540e19d
[email protected]
null
null
null
      

sr0、sr1、sr2弱引用关联的对象为null,因此可以知道被回收了,而其对应的弱引用(SoftReference)被添加到了ReferenceQueue中。

5.虚引用

虚引用唯一的目的只是跟踪对象的回收。它不会决定对象的生命周期,任何时候都可能被垃圾回收器回收。必须和引用队列ReferenceQueue联合使用。

在jdk api中对PhantomReference的描述如下:

虚引用对象,在回收器确定其指示对象可另外回收之后,被加入队列。虚引用最常见的用法是以某种可能比使用 Java 终结机制更灵活的方式来指派 pre-mortem 清除动作。

如果垃圾回收器确定在某一特定时间点上虚引用的指示对象是虚可到达对象,那么在那时或者在以后的某一时间,它会将该引用加入队列。

为了确保可回收的对象仍然保持原状,虚引用的指示对象不能被获取:虚引用的 

get

 方法总是返回 

null

与软引用和弱引用不同,虚引用在加入队列时并没有通过垃圾回收器自动清除。通过虚引用可到达的对象将仍然保持原状,直到所有这类引用都被清除,或者它们都变得不可到达。

虚可达对象:

  • 如果一个对象既不是强可到达对象,也不是软可到达对象或弱可到达对象,它已经终止,并且某个虚引用在引用它,则该对象是虚可到达 对象。
1 public static void testPhantomReference() throws InterruptedException {
 2     ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
 3     byte[] bytes = new byte[_1M * 4];
 4     PhantomReference<byte[]> reference = new PhantomReference<>(bytes, queue);
 5     bytes = null;
 6     System.gc();
 7     System.out.println(queue.poll());
 8     Thread.sleep(200);
 9     System.out.println(queue.poll());
10 }      

运行结果:

null
[email protected]
Heap
 par new generation   total 9216K, used 237K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   2% used [0x00000000fec00000, 0x00000000fec3b500, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4731K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  46% used [0x00000000ff600000, 0x00000000ffa9ece8, 0x00000000ffa9ee00, 0x0000000100000000)
 Metaspace       used 3229K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K      

在代码的第7行,手动调用System.gc()之后,垃圾回收器并没有立刻回收,它只是发出一个通知,建议触发GC。

因此立刻调用queue.poll()里面是空的。

200ms之后,reference加入了ReferenceQueue中,意味着reference关联的对象(4M byte[])需要被清除。此时,尽管我们把bytes置为空,但从老年代看到,4M数组依然是存活的,它只是加入了ReferenceQueue中,并没有真正被清除。

至于什么时候被清除,现在还搞不懂啊。。。。

总结

引用类型 被垃圾回收的时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行
软引用 内存不足 对象缓存 内存不足时
弱引用 垃圾回收时 对象缓存 gc运行后
虚引用 Unknown    标记、哨兵 Unknown

转载于:https://www.cnblogs.com/yn-huang/p/10755895.html