让人心疼的Java虚引用!

开发 后端
在Java的世界里,对象的存在层次,也有三六九等,充满了阶层之间的嘲弄。强软弱虚各种引用,对于熟悉Java的同学一定不会感到陌生,它们随着等级的降低,越来越没存在感。

[[405737]]

本文转载自微信公众号「小姐姐味道」,作者小姐姐养的狗 。转载本文请联系小姐姐味道公众号。

在Java的世界里,对象的存在层次,也有三六九等,充满了阶层之间的嘲弄。强软弱虚各种引用,对于熟悉Java的同学一定不会感到陌生,它们随着等级的降低,越来越没存在感。平常使用的对象,大多数就是强引用的;而软引用和弱引用,则经常在一些堆内缓存框架中用到。

那虚引用呢?传说中的幽灵引用,是不是就如同它的名字一样,一无是处呢?

三种引用

首先,我们来回顾一下其他三种引用的类型和用途。

Strong references

当内存空间不足,系统撑不住了,JVM 就会抛出 OutOfMemoryError 错误。即使程序会异常终止,这种对象也不会被回收。这种引用属于最普通最强硬的一种存在,只有在和 GC Roots 断绝关系时,才会被消灭掉。

这种引用,你每天的编码都在用。例如:new 一个普通的对象。

  1. Object obj = new Object() 

这种方式可能是有问题的。假如你的系统被大量用户(User)访问,你需要记录这个 User 访问的时间。可惜的是,User 对象里并没有这个字段,所以我们决定将这些信息额外开辟一个空间进行存放。

Soft references

软引用用于维护一些可有可无的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。

可以看到,这种特性非常适合用在缓存技术上。比如网页缓存、图片缓存等。

Guava 的 CacheBuilder,就提供了软引用和弱引用的设置方式。在这种场景中,软引用比强引用安全的多。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。

Weak references

弱引用对象相比较软引用,要更加无用一些,它拥有更短的生命周期。

当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。弱引用拥有更短的生命周期,在 Java 中,用 java.lang.ref.WeakReference 类来表示。

怪异的虚引用

以上几个引用级别都很好理解,但是虚引用是个例外。虚引用可以使用下面的代码定义:

  1. Object  object = new Object(); 
  2. ReferenceQueue queue = new ReferenceQueue(); 
  3. // 虚引用,必须与一个引用队列关联 
  4. PhantomReference pr = new PhantomReference(object, queue); 

但是当你想取出其中的值时(get),得到的却总是null。

  1. //JDK源码    
  2. /** 
  3.      * Returns this reference object's referent.  Because the referent of a 
  4.      * phantom reference is always inaccessible, this method always returns 
  5.      * {@code null}. 
  6.      * 
  7.      * @return {@code null
  8.      */ 
  9.     public T get() { 
  10.         return null
  11.     } 

虚引用主要用来跟踪对象被垃圾回收的活动。

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象之前,把这个虚引用加入到与之关联的引用队列中。

程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

桃花源深处

在hotspot的jvm中,有一个叫做cleaner的类,其实就是虚引用典型的应用。可以看到Cleaner是直接简单粗暴的继承了PhantomReference,所以它本质上就是一个虚引用,只不过多了一些便捷的操作。

那么这个类是在什么地方用到的呢?大家手上应该都有jdk的源代码,追踪一下,发现最后竟然是DirectByteBuffer用到了它。

直接内存,一直是一个看起来非常高大上的名词,基本上和高性能挂钩,但也容易产生内存泄漏。由于直接内存,是属于堆外内存的,所以垃圾回收的时候,就不能靠JVM的那一套垃圾回收算法进行清理。

事实上,由于DirectByteBuffer可能会被使用较长时间,熬过了年轻代的各种回收,就会进入老年代。这时候就比较麻烦了,这些引用对象,要在下一轮Old GC或者Full GC才能触发,如果你的老年代空间较大,触发回收的操作就需要等很久很久。问题是,在这段时间内,虽然这些堆外内存不再使用了,但它仍然占用着较大的物理空间,最后造成严重的浪费甚至崩溃。

对堆外内存不是很熟悉的同学,可以看我以前的一张图。或者直接看这篇文章。通过-XX:MaxDirectMemorySize可以限制直接内存的使用上限。

《一图解千愁,jvm内存从来没有这么简单过!》

那么这些堆外内存是如何进行回收的呢?这就是Cleaner的作用。Cleaner通过next和prev构造了一个典型的链表,但它本身是没有任何逻辑的,因为它的清理逻辑都在thunk方法中。

  1. cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 
  2.  
  3. public void clean() { 
  4.         if (remove(this)) { 
  5.             try { 
  6.                 this.thunk.run(); 

也就是Deallocator = De allocator。其中,传入的base,就是靠unsafe类申请的堆外内存地址引用(仅仅是个地址),有了引用和容量,其实我们就能够在回收的时候定位到真正的堆外内存块。就像Deallocator做的一样。

  1. public void run() { 
  2.   if (address == 0) { 
  3.     // Paranoia 
  4.     return
  5.   } 
  6.   unsafe.freeMemory(address); 
  7.   address = 0; 
  8.   Bits.unreserveMemory(size, capacity); 

机制上没什么问题,关键要看它们是怎么联系起来的。这种问题,当然是要靠其他线程完成,这里就是ReferenceHandler。很熟悉的名字,你每次使用jstack命令导出堆栈,都会看到它。

  1. Thread handler = new ReferenceHandler(tg, "Reference Handler"); 
  2. /* If there were a special system-only priority greater than 
  3. * MAX_PRIORITY, it would be used here 
  4. */ 
  5. handler.setPriority(Thread.MAX_PRIORITY); 
  6. handler.setDaemon(true); 
  7. handler.start(); 

真正去工作的方法,是tryHandlePending,然后在这里,调用Cleaner的clean方法,进而调用真正的清理方法,释放堆外内存。它会从虚引用注册的队列里,取出新的对象,然后判断是不是Cleaner类型,如果是,就进行一次清理。

End

这就是虚引用。它存在的唯一目的,就是在回收的时候,能够被感知到,以便进行更深层次的清理。在commons-io包的FileCleaningTracker类中,同样有继承了虚引用的Tracker类,用来跟踪后续文件的一些清理工作。这个没存在感的小小虚引用,默默的承担起最后一道防线,是系统正常运行的有效保证。

不要小看它,它无处不在。因为你的每一个JVM进程,都跑着一个叫做Reference Handler的线程呢。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

 

责任编辑:武晓燕 来源: 小姐姐味道
相关推荐

2020-12-02 09:01:40

Java基础

2009-06-19 16:19:23

Java对象引用

2012-05-09 13:36:30

WP7手机

2021-10-18 15:50:49

Android强引用软引用

2013-02-21 14:20:47

2010-03-09 16:18:25

ArcGIS Expl

2022-01-17 10:18:33

腾讯末位淘汰制员工

2020-02-26 21:57:09

Lambdajava8方法引用

2009-10-09 16:25:00

CCNA考试个人心得CCNA

2009-06-08 10:39:48

腾讯网友魔兽停服

2015-09-21 09:26:15

2010-01-18 17:38:54

C++虚函数表

2023-03-30 07:55:02

2019-10-24 07:42:28

Java引用GC

2015-11-02 17:20:00

Java弱引用

2011-07-22 17:00:14

java

2020-08-03 12:26:22

数据透视函数公式

2011-11-24 16:34:39

Java

2023-09-02 20:05:07

GNOME 45

2011-11-04 16:44:45

iPhone应用
点赞
收藏

51CTO技术栈公众号