JVM 的四种引用状态
在 Java 虚拟机 5:Java 垃圾回收(GC)机制详解一文中,有简单提到过 JVM 的四种引用状态,当时只是简单学习,知道有这么一个概念,对四种引用状态理解不深。这两天重看虚拟机这部分的时候,写了很多例子详细研究了一下 JVM 的几种引用,对于 JVM 的引用理解加深了不少,因此总结写一篇文章总结并分享下。
首先,还是先从 JVM 四种引用状态开始,这部分摘抄自周志明老师的《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》一书。
在 JDK1.2 之前,Java 中的引用的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种顶一下只有被引用或者没有被引用两种状态,对于如何描述一些 "食之无味,弃之可惜" 的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象(注意和前面一段蓝字的对比学习)。很多系统的缓存功能都符合这样的引用场景。
在 JDK1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 中引用强度一次减弱。
写于代码开始前
在通过代码研究几种引用状态之前,先定义一些参数,后面所有部分的代码示例都使用这些参数。
首先是 JVM 的参数,这里我使用的是:
- -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8-XX:+UseParNewGC -verbose:gc -XX:+PrintGCDetails
这意味着:
其次,再定义一个常量类 "_1MB":
- /**
- * 1M
- */
- private static final int_1MB = 1024 * 1024;
代码示例使用 byte 数组,每个 byte 为 1 个字节,因此定义一个 "_1MB" 的常量就可以方便得到 1M、2M、3M... 的内存空间了。
强引用的研究
关于强引用的研究,研究的重点在于验证 "当一个对象到 GC Roots 没有任何引用链相连,则证明此对象是不可用的(要被回收)" 这句话的正确性。虚拟机参数上面列了,首先写一个空方法:
- 1 /**
- 2 * @author 五月的仓颉http://www.cnblogs.com/xrq730/p/7082471.html
- 3 */
- 4 @Test
- 5 public void testStrongReference0() {
- 6
- 7}
程序运行结果为:
- 1 Heap
- 2parnewgeneration total 9216K, used 3699K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
- 3eden space 8192K, 45% used [0x00000000f9a00000, 0x00000000f9d9cdc0, 0x00000000fa200000)
- 4from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
- 5to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
- 6tenured generation total 10240K, used 0K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
- 7the space 10240K, 0% used [0x00000000fa400000, 0x00000000fa400000, 0x00000000fa400200, 0x00000000fae00000)
- 8compacting perm gen total 21248K, used 4367K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
- 9the space 21248K, 20% used [0x00000000fae00000, 0x00000000fb243d88, 0x00000000fb243e00, 0x00000000fc2c0000)
- 10No shared spaces configured.
这意味着新生代中本身就有 3699K 的内存空间。很好理解,因为虚拟机启动的时候就会加载一部分数据到内存中,这部分数据的大小为 3699K。
下一步我们放一个 4M 的 byte 数组进去(4M 是因为找一个相对大点的数字,结果会比较明显),4M=4096K,加上原来的 3966K 等于 8062K。对这 8062K 内存空间触发一次 GC:
- 1 /**
- 2 * @author 五月的仓颉http://www.cnblogs.com/xrq730/p/7082471.html
- 3 */
- 4 @Test
- 5 public void testStrongReference0() {
- 6System.out.println("**********强引用测试(放一个4M的数组,触发GC)**********");
- 7
- 8 byte[] bytes =new byte[4 * _1MB];
- 9
- 10 // 手动触发GC
- 11 System.gc();
- 12}
运行结果为:
- 1**********强引用测试(放一个4M的数组,触发GC)********** 2[Full GC[Tenured: 0K->5161K(10240K), 0.0085630 secs] 7958K->5161K(19456K), [Perm : 4354K->4354K(21248K)], 0.0086002 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
- 3 Heap
- 4parnewgeneration total 9216K, used 284K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
- 5eden space 8192K, 3% used [0x00000000f9a00000, 0x00000000f9a47300, 0x00000000fa200000)
- 6from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
- 7to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
- 8tenured generation total 10240K, used 5161K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
- 9the space 10240K, 50% used [0x00000000fa400000, 0x00000000fa90a548, 0x00000000fa90a600, 0x00000000fae00000)
- 10compacting perm gen total 21248K, used 4367K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
- 11the space 21248K, 20% used [0x00000000fae00000, 0x00000000fb243dc0, 0x00000000fb243e00, 0x00000000fc2c0000)
- 12No shared spaces configured.
总结一下这次 GC 的结果(由于这篇不是研究内存分配的文章,因此我们只关注结果,至于过程到底为什么就不细究了):
总结起来就是 4M 的 byte 数组并没有被回收(因为总共有 5161K 的对象,虚拟机启动的时候也才加载了 3699K 不到 5161K,那 4M 的 byte 数组肯定是在的),原因是有 bytes 引用指向 4M 的 byte 数组。既然如此,我们把 bytes 置空看看结果如何:
- 1 /**
- 2 * @author 五月的仓颉http://www.cnblogs.com/xrq730/p/7082471.html
- 3 */
- 4 @Test
- 5 public void testStrongReference0() {
- 6System.out.println("**********强引用测试(放一个4M的数组,bytes置空,触发GC)**********");
- 7
- 8 byte[] bytes =new byte[4 * _1MB];
- 9
- 10bytes =null;
- 11
- 12 // 手动触发GC
- 13 System.gc();
- 14}
运行结果为:
- 1**********强引用测试(放一个4M的数组,bytes置空,触发GC)********** 2[Full GC[Tenured: 0K->1064K(10240K), 0.0096213 secs] 7958K->1064K(19456K), [Perm : 4354K->4354K(21248K)], 0.0096644 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
- 3 Heap
- 4parnewgeneration total 9216K, used 284K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
- 5eden space 8192K, 3% used [0x00000000f9a00000, 0x00000000f9a47300, 0x00000000fa200000)
- 6from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
- 7to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
- 8tenured generation total 10240K, used 1064K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
- 9the space 10240K, 10% used [0x00000000fa400000, 0x00000000fa50a368, 0x00000000fa50a400, 0x00000000fae00000)
- 10compacting perm gen total 21248K, used 4367K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
- 11the space 21248K, 20% used [0x00000000fae00000, 0x00000000fb243dc0, 0x00000000fb243e00, 0x00000000fc2c0000)
- 12No shared spaces configured.
从 GC 详情我们可以看到:
很显然 4M 的 byte 数组被回收。
由这个例子我们回顾可以作为 GC Roots 的对象:
这次的回收正是因为第一条。本身有 bytes(在虚拟机栈中)指向 4M 的 byte 数组,由于将 bytes 置空。因此 4M 的 byte 数组此时没有任何一个可以作为 GC Roots 对象的引用指向它,即 4M 的 byte 数组被虚拟机标记为可回收的垃圾,在 GC 时被回收。
稍微扩展一下,这里上面代码的做法是手动将 bytes 置空,其实方法调用结束也是一样的,栈帧消失,栈帧消失意味着 bytes 消失,那么 4M 的 byte 数组同样没有任何一个可以作为 GC Roots 对象的引用指向它,因此方法调用结束之后,4M 的 byte 数组同样会被虚拟机标记为可回收的垃圾,在 GC 时被回收。
软引用的研究
软引用之前说过了,JDK 提供了 SoftReference 类共开发者使用,那我们就利用 SoftReference 研究一下软引用,测试代码为:
- 1 /**
- 2 * @author 五月的仓颉http://www.cnblogs.com/xrq730/p/7082471.html
- 3 */
- 4 @Test
- 5 public void testSoftReference0() {
- 6System.out.println("**********软引用测试**********");
- 7
- 8 byte[] bytes =new byte[4 * _1MB];
- 9SoftReference<byte[]> sr =newSoftReference<byte[]>(bytes);
- 10System.out.println("GC前:" + sr.get());
- 11
- 12bytes =null;
- 13
- 14 System.gc();
- 15System.out.println("GC后:" + sr.get());
- 16}
同样的 new 一个 4M 的 byte 数组,通过 SoftReference 构造方法放到 SoftReference 中。
这段代码最值得注意的是第 9 行 "bytes=null" 这一句,如果不将 bytes 置空,那么 4M 的 byte 数组还与强引用关联着,内存不够虚拟机将抛出异常而不会尝试回收它;将 bytes 置空则不一样,4M 的 byte 数组失去了强引用,但是它又在 SoftReference 中,这意味着这个 4M 的 byte 数组目前仅仅与软引用关联。
运行一下程序,结果为:
- 1**********软引用测试********** 2GC前:[B@76404629 3[Full GC[Tenured: 0K->5161K(10240K), 0.0094088 secs] 7953K->5161K(19456K), [Perm : 4354K->4354K(21248K)], 0.0094428 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
- 4GC后:[B@76404629 5 Heap
- 6parnewgeneration total 9216K, used 284K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
- 7eden space 8192K, 3% used [0x00000000f9a00000, 0x00000000f9a47330, 0x00000000fa200000)
- 8from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
- 9to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
- 10tenured generation total 10240K, used 5161K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
- 11the space 10240K, 50% used [0x00000000fa400000, 0x00000000fa90a778, 0x00000000fa90a800, 0x00000000fae00000)
- 12compacting perm gen total 21248K, used 4367K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
- 13the space 21248K, 20% used [0x00000000fae00000, 0x00000000fb243f10, 0x00000000fb244000, 0x00000000fc2c0000)
- 14No shared spaces configured.
看到 GC 前后,bytes 都是 "[B@76404629",很显然 4M 的 byte 数组并没有被回收。从内存空间来看,老年代中使用了 5161K,和之前强引用测试是一样的,证明了这一点。
那我们怎么能看到弱引用的回收呢?既然弱引用是发生在内存不够之前,那只需要不断实例化 byte 数组,然后将之与软引用关联即可,代码为:
- 1 /**
- 2 * @author 五月的仓颉http://www.cnblogs.com/xrq730/p/7082471.html
- 3 */
- 4 @Test
- 5 public void testSoftReference1() {
- 6System.out.println("**********软引用测试**********");
- 7
- 8SoftReference<byte[]> sr0 =newSoftReference<byte[]>(new byte[4 * _1MB]);
- 9SoftReference<byte[]> sr1 =newSoftReference<byte[]>(new byte[4 * _1MB]);
- 10SoftReference<byte[]> sr2 =newSoftReference<byte[]>(new byte[4 * _1MB]);
- 11SoftReference<byte[]> sr3 =newSoftReference<byte[]>(new byte[4 * _1MB]);
- 12SoftReference<byte[]> sr4 =newSoftReference<byte[]>(new byte[4 * _1MB]);
- 13SoftReference<byte[]> sr5 =newSoftReference<byte[]>(new byte[4 * _1MB]);
- 14
- 15 System.out.println(sr0.get());
- 16 System.out.println(sr1.get());
- 17 System.out.println(sr2.get());
- 18 System.out.println(sr3.get());
- 19 System.out.println(sr4.get());
- 20 System.out.println(sr5.get());
- 21}
运行结果为:
- 1**********软引用测试********** 2[GC[ParNew: 7958K->1024K(9216K), 0.0041103 secs] 7958K->5187K(19456K), 0.0041577 secs] [Times: user=0.05 sys=0.00, real=0.00 secs]
- 3[GC[ParNew: 5203K->331K(9216K), 0.0036532 secs] 9366K->9481K(19456K), 0.0036694 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
- 4[GC[ParNew: 4427K->4427K(9216K), 0.0000249 secs][Tenured: 9149K->9149K(10240K), 0.0054937 secs] 13577K->13246K(19456K), [Perm : 4353K->4353K(21248K)], 0.0055600 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
- 5[Full GC[Tenured: 9149K->783K(10240K), 0.0071252 secs] 13246K->783K(19456K), [Perm : 4353K->4352K(21248K)], 0.0071560 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
- 6[GC[ParNew: 4096K->41K(9216K), 0.0010362 secs] 4879K->4921K(19456K), 0.0010745 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
- 7[GC[ParNew: 4137K->10K(9216K), 0.0009216 secs] 9017K->8986K(19456K), 0.0009366 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
- 8 null
- 9 null
- 10 null
- 11 [B@4783165b
- 12 [B@6f30d50a
- 13 [B@6ef2bc8d
- 14 Heap
- 15parnewgeneration total 9216K, used 4307K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
- 16eden space 8192K, 52% used [0x00000000f9a00000, 0x00000000f9e32560, 0x00000000fa200000)
- 17from space 1024K, 1% used [0x00000000fa200000, 0x00000000fa202978, 0x00000000fa300000)
- 18to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
- 19tenured generation total 10240K, used 8975K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
- 20the space 10240K, 87% used [0x00000000fa400000, 0x00000000facc3f40, 0x00000000facc4000, 0x00000000fae00000)
- 21compacting perm gen total 21248K, used 4366K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
- 22the space 21248K, 20% used [0x00000000fae00000, 0x00000000fb2439e0, 0x00000000fb243a00, 0x00000000fc2c0000)
- 23No shared spaces configured.
从第 8 行~ 第 13 行的结果来看,前三个 4M 的 byte 数组被回收了,后三个 4M 的 byte 数组还在,这就证明了 "被软引用关联的对象会在内存不够时被回收"。
这段代码我们可以做一个对比思考:
所以,很多时候对一些非必需的对象,我们可以将直接将其与软引用关联,这样内存不够时会先回收软引用关联的对象而不会抛出 OutOfMemoryError,毕竟抛出 OutOfMemoryError 意味着整个应用将停止运行。
弱引用的研究
JDK 给我们提供的了 WeakReference 用以将一个对象关联到弱引用,弱引用的测试比较简单,代码为:
- 1 /**
- 2 * @author 五月的仓颉http://www.cnblogs.com/xrq730/p/7082471.html
- 3 */
- 4 @Test
- 5 public void testWeakReference() {
- 6System.out.println("**********弱引用测试**********");
- 7
- 8WeakReference<byte[]> wr =newWeakReference<byte[]>(new byte[4 * _1MB]);
- 9System.out.println("GC前:" + wr.get());
- 10
- 11 System.gc();
- 12System.out.println("GC后:" + wr.get());
- 13}
我这里不定义一个强引用直接关联 4M 的 byte 数组(避免忘了将对象与强引用的关联取消),这也是使用 SoftReference、WeakReference 时我个人比较推荐的做法。程序运行的结果为:
- 1**********弱引用测试********** 2 GC前:[B@21dd63a8
- 3[Full GC[Tenured: 0K->1065K(10240K), 0.0080353 secs] 7958K->1065K(19456K), [Perm : 4353K->4353K(21248K)], 0.0080894 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
- 4GC后:null
- 5 Heap
- 6parnewgeneration total 9216K, used 284K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
- 7eden space 8192K, 3% used [0x00000000f9a00000, 0x00000000f9a47318, 0x00000000fa200000)
- 8from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
- 9to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
- 10tenured generation total 10240K, used 1065K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
- 11the space 10240K, 10% used [0x00000000fa400000, 0x00000000fa50a6e8, 0x00000000fa50a800, 0x00000000fae00000)
- 12compacting perm gen total 21248K, used 4367K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
- 13the space 21248K, 20% used [0x00000000fae00000, 0x00000000fb243dc8, 0x00000000fb243e00, 0x00000000fc2c0000)
- 14No shared spaces configured.
看到 GC 后 bytes 为 null 了,且新生代、老年代中也没见到有 4M 以上的大对象,从两个角度都证明了,GC 之后 4M 的 byte 数组被回收了。
Reference 与 ReferenceQueue
前面用代码验证了强引用、软应用、弱引用三种引用状态,虚引用就不演示了,记住虚引用是用于跟踪对象的回收状态就够了。
下面再讲一个知识点 ReferenceQueue,ReferenceQueue 的作用分点讲解下:
讲完理论,用代码验证一下,还是使用软引用,不过为了显示更清楚把 GC 显示相关参数(-verbose:gc -XX:+PrintGCDetails)去掉,代码为:
- 1 /**
- 2 * @author 五月的仓颉http://www.cnblogs.com/xrq730/p/7082471.html
- 3 */
- 4 @Test
- 5 public void testReferenceQueue() {
- 6System.out.println("**********引用队列测试**********\n");
- 7
- 8ReferenceQueue<byte[]> referenceQueue =newReferenceQueue<byte[]>();
- 9
- 10SoftReference<byte[]> sr0 =newSoftReference<byte[]>(new byte[4 * _1MB], referenceQueue);
- 11SoftReference<byte[]> sr1 =newSoftReference<byte[]>(new byte[4 * _1MB], referenceQueue);
- 12SoftReference<byte[]> sr2 =newSoftReference<byte[]>(new byte[4 * _1MB], referenceQueue);
- 13SoftReference<byte[]> sr3 =newSoftReference<byte[]>(new byte[4 * _1MB], referenceQueue);
- 14SoftReference<byte[]> sr4 =newSoftReference<byte[]>(new byte[4 * _1MB], referenceQueue);
- 15SoftReference<byte[]> sr5 =newSoftReference<byte[]>(new byte[4 * _1MB], referenceQueue);
- 16
- 17System.out.println("**********软引用关联的对象展示**********");
- 18System.out.println(sr0 + "---" + sr0.get());
- 19System.out.println(sr1 + "---" + sr1.get());
- 20System.out.println(sr2 + "---" + sr2.get());
- 21System.out.println(sr3 + "---" + sr3.get());
- 22System.out.println(sr4 + "---" + sr4.get());
- 23System.out.println(sr5 + "---" + sr5.get());
- 24
- 25System.out.println("**********引用队列中的SoftReference展示**********");
- 26 System.out.println(referenceQueue.poll());
- 27 System.out.println(referenceQueue.poll());
- 28 System.out.println(referenceQueue.poll());
- 29 System.out.println(referenceQueue.poll());
- 30 System.out.println(referenceQueue.poll());
- 31 System.out.println(referenceQueue.poll());
- 32}
运行结果为:
- 1**********引用队列测试********** 2
- 3**********软引用关联的对象展示********** 4java.lang.ref.SoftReference@50ed0a5---null
- 5java.lang.ref.SoftReference@fa4033b---null
- 6java.lang.ref.SoftReference@58d01e82---null
- 7java.lang.ref.SoftReference@4783165b---[B@6f30d50a
- 8java.lang.ref.SoftReference@6ef2bc8d---[B@23905e3
- 9java.lang.ref.SoftReference@6db17b38---[B@1f10d1cb
- 10**********引用队列中的SoftReference展示**********11 java.lang.ref.SoftReference@50ed0a5
- 12 java.lang.ref.SoftReference@fa4033b
- 13 java.lang.ref.SoftReference@58d01e82
- 14 null
- 15 null
- 16 null
看到由于内存不够,回收了前三个软引用,且回收的三个软引用内存地址都能对得上,这证明了上面的说法。
来源: http://www.cnblogs.com/xrq730/p/7082471.html