JVM原理与实现——Reference

本文出自:【InTheWorld的博客】 (欢迎留言、交流)

duke_beer1. Reference的基本介绍

reference的中文含义是“引用”。由于本文所基于的HotSpot虚拟机主要使用C++开发,因此我担心有人会把C++的引用和这里的reference混为一谈。所以,我会尽量使用reference(首字母小写)来表述”引用“这个概念。通常我们写下如下的语句: Object obj; 其实就是定义了一个reference。我可以很直白的说出一个结论——在32位机器上HotSpot的reference就是一个32bit的指针。如下图所示,reference指向一个堆空间的实际对象。这种常用的reference,我们给它起个名字——StrongReference。之所以这样叫,是为了体现它和其他Reference(即将登场)的联系与区别。

image

在java.lang.ref包下,有这样几个类:SoftReference、WeakReference以及PhantomReference。它们之间的继承关系图如下所示:

reference_class

以WeakReference为例,我们来看看Reference的用法。有如下的代码片段:

   SoftReference sr = new SoftReference (new Employee ());
    Employee em = sr.get();

第一行就定义了一个关于Employee的软引用,换言之sr和new Employee()产生的匿名对象之间形成了软引用关系。第二行代码展示了软引用的使用方式,也很简单使用get()方法。而软应用的要点在于这个get()方法可能返回null,换言之软引用所指向的东西是可以被gc清理掉的。下面这张图展示了软引用的内存布局:

soft_reference

其中,SoftReference实例也是一个堆内存中的对象,它被sr这个普通的StrongReference所引用,同时它还通过软引用指向图中的Emplyee对象。其实在内存布局方面,WeakReference、PhantomReference与SoftReference是非常相似的,它们主要的区别在于gc的时机和get()方法返回值不同。比如,软引用会在内存紧张的时候被释放,而弱引用会在gc的时候立即被释放。

引用类型 取得目标对象方式 垃圾回收条件 是否可能内存泄漏
强引用 直接调用 不回收 可能
软引用 通过 get() 方法 视内存情况回收 不可能
弱引用 通过 get() 方法 永远回收 不可能
虚引用 无法取得 不回收 可能

上面这张表展示了不同种类的Reference的特性对比。此外还有一个知识点值得注意,就是Reference.get()方法拿到的是一个强引用,所以不会出现get到一个非null值却在之后被gc掉的情况(在强引用作用域内)。另外,Reference中指向the referent的内部引用域其实是会被多个读、多个写的。Reference.get()是读的情况,它没有加锁,gc对引用域会存在写操作,但gc的操作都是在锁内完成的,这个锁就是全局的gc锁。因此,Reference是不存在同步错误的。

2. Reference语义的实现

前面用不少篇幅来介绍了Reference的基本工作方式,下面我们来研究下HotSpot VM是如何实现Reference的基本语义的。话不多说,先看看java.lang.ref包里面的代码。、

public abstract class Reference {
    //Reference指向的对象
    private T referent;         /* Treated specially by GC */
    //Reference所指向的队列
    volatile ReferenceQueue queue;

    @SuppressWarnings("rawtypes")
    Reference next;

    transient private Reference discovered;  /* used by VM */

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
}

Reference的queue的作用是为了监视引用的状态,不算是一种常用的模式,第一个构造函数更为常用。这java.lang.ref的包里面,并看不出Reference和gc是如何协同作用的,而且也看不到任何native方法。然而”Treated specially by GC ”这句注释还是暴露了不少信息的。不同类型的Reference语义其实其实是由GC直接实现的。听起来有点悬。还是根据源代码来分析吧!下面根据g1垃圾收集器分析Reference的语义实现:

    openjdk\hotspot\src\share\vm\gc_implementation\g1\g1CollectedHeap.cpp

g1ColloctedHeap.cpp是g1垃圾收集器的重要实现部分。在g1ColloctedHeap类中有下面这个方法,简化的代码如下:

void G1CollectedHeap::process_discovered_references(uint no_of_gc_workers) {
  double ref_proc_start = os::elapsedTime();
  //获取ReferenceProcessor
  ReferenceProcessor* rp = _ref_processor_stw;
  ReferenceProcessorStats stats;
  if (!rp->processing_is_mt()) {
    // 串行Reference处理...
    stats = rp->process_discovered_references(&is_alive,
                                              &keep_alive,
                                              &drain_queue,
                                              NULL,
                                              _gc_timer_stw);
  } else {
    // 并行Reference处理
    G1STWRefProcTaskExecutor par_task_executor(this, workers(), _task_queues, no_of_gc_workers);
    stats = rp->process_discovered_references(&is_alive,
                                              &keep_alive,
                                              &drain_queue,
                                              &par_task_executor,
                                              _gc_timer_stw);
  }
}

这个函数会调用ReferenceProcessor的process_discovered_references()方法。ReferenceProcessor,顾名思义就是处理Reference的。到ReferenceProcessor的代码里面一看究竟吧!process_discovered_references()方法看起来非常直接了当,可以非常明显看出它完成了几种Reference的处理。

ReferenceProcessorStats ReferenceProcessor::process_discovered_references(/* */) {
  // Soft references
  size_t soft_count = 0;
  {
    soft_count =
      process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true,
                                 is_alive, keep_alive, complete_gc, task_executor);
  }
  // Weak references
  size_t weak_count = 0;
  /*  */
  // Phantom references
  size_t phantom_count = 0;
  /*  */
  return ReferenceProcessorStats(soft_count, weak_count, final_count, phantom_count);
}

由于几种Reference的处理其实比较类似,我就只贴了SoftReference的代码。process_discoverd_reflist()方法实现了真正的Reference处理工作。这个方法会调用三个阶段的代码,其中第三个阶段会去gc非Strong Reference的referent引用。这个第三阶段的函数就是process_phase3()。它的大致代码如下:

void
ReferenceProcessor::process_phase3(DiscoveredList&    refs_list,
                                   bool               clear_referent,
                                   BoolObjectClosure* is_alive,
                                   OopClosure*        keep_alive,
                                   VoidClosure*       complete_gc) {
  ResourceMark rm;
  DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
  while (iter.has_next()) {
    iter.update_discovered();
    if (clear_referent) {
      // NULL out referent pointer
      iter.clear_referent();
    } else {
      // keep the referent around
      iter.make_referent_alive();
    }
    assert(iter.obj()->is_oop(UseConcMarkSweepGC), "Adding a bad reference");
    iter.next();
  }
  // Remember to update the next pointer of the last ref.
  iter.update_discovered();
  // Close the reachable set
  complete_gc->do_void();
}

不知道为什么,我很喜欢看到循环。大概是觉得它活干的比较多吧!process_phase3()的这个while循环,就完成了对Reference的处理。iter.clear_referenct()就是把Reference的referent域赋值为null,然后referent所指向的堆上对象就要面临被gc的命运了。

写到这里,可以看出gc对Reference的大致处理逻辑。然而,仍然有很多Reference的内容没有分析到,比如几种Reference的差异化处理,没有分析。WeakReference和SoftRefence的处理逻辑差异,主要体现在Reference发现的阶段,简单来讲SoftReference要在内存吃紧的时候才会被加到DiscoverList里面。而PhantomReference的作用是跟踪对象被垃圾回收器回收的活动,它需要和ReferenceQueue配合使用,绝大多数开发者都不需要使用它。还有就是ReferenceQueue的内容,这里也没有涉及。如果有同学感兴趣,可以按着上面的代码去跟跟。

~~~The End~~~

 

参考资料:

[1]. http://www.javaworld.com/article/2073891/java-se/java-se-trash-talk-part-2.html

[2]. https://www.ibm.com/developerworks/cn/java/j-lo-langref/

已有3条评论 发表评论

  1. wf /

    博主你好,这篇文章已经发表一年多了,不知道你后续还有没有继续了解Java引用这一块。
    我觉得这篇文章里对引用的理解可能有一些错误。首先并不是弱引用类型引用的对象总是会被回收,对象是否回收取决于它的可达性(Reachability),而弱引用类型引用的对象并不一定就是弱可达的。弱可达的对象很可能会在下一次GC时被回收,但也不是绝对的。所以严谨一点的话,文章里的那个表格最左边不是强引用、软引用等,而是强可达、软可达等。
    虚引用(如果表示的是虚可达的意思的话)也不是不回收,我认为恰恰相反,而是已经被回收。虚引用的作用主要就是用来判断被引用的对象是否已经被回收。具体可以看虚引用类型的类注释:Phantom reference objects, which are enqueued after the collector determines that their referents may otherwise be reclaimed。
    关于是否可能引起内存泄漏,我认为使用弱引用和虚引用最起码不会引起新的内存泄漏,已存在的内存泄漏应该也和它们无关,强引用不用说的,而软引用如果使用不当的话是可能会引起内存泄漏的。因为软可达对象并不能保证会被及时回收,根据内存情况而定其实是最不确定的情况,能确定的只有:在虚拟机发生OOM之前一定会保证先释放软可达的对象,至于真实情况下什么时候才会释放取决于各回收器的实现。
    我说了这么多其实是在纠结于文章中的那个表格,但正是这一点可能会影响文章的严谨性。另外,博主最后给的参考资料的第二个我建议还是不要参考为好,这篇虽是发表在IBM开发社区上,但是其中的错误还是蛮多的,不值得参考。

    1. swliu / 本文作者

      是的,那个表格的确是有一些歧义,弱引用的对象也可能被强引用。可达性是最标准的判据,这点我同意。写这篇文章的时候大致看了下jvm的引用实现,但基本还是停留在脉络上,不够深入。JVM已经有一段时间没看了,工作比较忙,要学的东西太多了。。。

  2. 引用通知: 一次ThreadLocal源码解析之旅 – IT汇 /

发表评论