Java实现的引用

引用的分类

Java 1.2以后,除了普通的引用外,Java还定义了软引用、弱引用、虚引用等概念。

  • 强引用:GC root引用
  • 软引用(Soft Reference):通过java.lang.ref.SoftReference引用的对象,可以通过get操作获取所引用的对象,所引用对象会延迟到在即将OOM时回收
  • 弱引用(Weak Reference):通过java.lang.ref.WeakReference引用的对象,可以通过get操作获取所引用的对象,不会影响垃圾收集器的行为,所引用对象会在下次垃圾收集时回收
  • 虚引用(Phantom Reference):通过java.lang.ref.PhantomReference引用的对象,不能通过get操作获取所引用对象(无论何时都会返回null),不会影响垃圾收集器的行为,会在下次垃圾收集时回收。在PhantomReference实例时,必须要传入一个ReferenceQueue实例用于实现通知。

JDK中的引用(Reference)

Java使用java.lang.ref下的类表示和管理对象的引用状态,如上面提到的三种其他引用,以及finalization在Java语言层上的实现。通过这些类与JVM进行交互,共同实现Java对这些引用的逻辑。除了强引用外,Java通过java.lang.ref.Reference<T>实现其他类型的引用。Reference中定义了引用的状态(State),当发生一次GC后,某些引用的状态会随之发生改变。状态改变后,某些引用可以通过放置到用户指定的java.lang.ref.ReferenceQueue实例,实现被引用的对象失效后对引用实例本身的操作,比如在引用失效后通知给用户。

Java所有的除了强引用之外的引用都通过java.lang.ref.Reference<T>抽象类实现,该类某些逻辑是通过与JVM的操作紧密结合而实现的,所以除了java.lang.ref下继承它的子类可以被JVM识别,自己继承这个抽象类是没有任何意义的。Reference通过一个referent的泛型引用保存被引用的对象,同时也持有一个queue引用保存一个ReferenceQueue<? super T>的实例用于对引用的注册(register)操作,用于在被引用对象失效后将引用注册进队列。Reference本身也被实现成一个链表,当一个Reference作为一个引用时,其next为null,如果作为一个pending引用链出现,next要么是this(链表尾),要么是其它的引用实例。

引用(Reference)的状态

Reference将引用的状态分为有效(Active)、挂起(Pending)、待处理(Enqueued)、不可用(Inactive)。通过判断一个Reference实例是否被注册(is registered)到该Reference实例来影响一个引用被GC后的状态变化。

  • 有效(Active):新创建的引用实例,在其被引用的对象被回收之前是有效的
  • 挂起(Pending):其被引用的对象被回收之后被放到pending-Reference列表中,等待Reference-handler线程处理的引用
  • 待处理(Enqueued):一个在ReferenceQueue队列实例中的引用
  • 无效(Inactive):不再可用的引用。

只有在一个被注册(包含一个ReferenceQueue实例引用)的引用中可能存在挂起(Pending)和待处理(Enqueued)状态。

引用(Reference)的生命周期

一个引用的生命周期通常是这样子的:

首先,当一个引用被创建时,无论有没有被注册,总是有效的(Active)。在GC标记阶段,如果一个referent被标记为不可达(没有GC root),收集器在检测到referent的可达性发生变化(由可达变为不可达)后,如果一个引用是被注册的,那么JVM会将该引用更改为挂起(Pending)状态,否则直接不可用(Inactive)。

怎么判断一个引用是否被注册呢?通过这个引用是否有持有一个非Null的ReferenceQueue实例。如果用户没有在构造引用实例时手动传入一个ReferenceQueue,那么这个引用就是未被注册的。这个Null也是一个类,是一个ReferenceQueue内部状态类,没有别的作用,仅仅作为一个生成空对象的实例使用。

private static class Null extends ReferenceQueue {
    boolean enqueue(Reference r) {
        return false;
    }
}

若一个引用被注册,那么JVM会将该引用实例添加到pending-Reference列表中,并修改其next,该引用正式处于挂起(Pending)状态。所谓的pending-Reference列表,就是Reference中的一个特殊的私有静态引用,“添加到pending-Reference列表中”其实就是一个赋值(set)操作。当一个引用被挂起(Pending)后,唯一的目的就是等待Reference-handler线程将其从pending-Reference列表中移动到ReferenceQueue。

Reference-handler线程将挂起的引用从pending-Reference列表中移动到它被注册的ReferenceQueue后,这个引用的状态就成了待处理(Enqueued)。由于ReferenceQueue是用户指定的,所以用户可以对这个状态的引用进行操作,也可以说,被注册的引用在被GC后,用户可以得到一个通知。ReferenceQueue也可以说是一个消息队列,用户可以对里面的引用进行操作,典型的应用就是WeakHashMap对弱引用的处理。一旦里面的引用被移出队列,那么该引用的状态就会变为最终态——无效(Inactive)状态。无效状态的引用再也不会更改为其他状态,只能等待自身被GC。

再谈pending-Reference列表

pending-Reference列表,就是Reference中的一个特殊的私有静态引用:

private static Reference pending = null;

与之类似的还有discovered

transient private Reference<T> discovered;

为什么说这两个变量特殊,是因为Java中没有任何对该引用赋值的定义,那么如何将引用实例放入pending字段中呢?这由VM对字节码的调用完成。openjdk中的hotspot源码中,hotspot/src/share/vm/memory/referenceProcessor.cpp这个文件中有一个ReferenceProcessor::discover_reference方法,根据此方法的注释由了解到虚拟机在对Reference的处理有ReferenceBasedDiscovery和RefeferentBasedDiscovery两种策略。这两个策略的实现不在讨论范围内,此处省略不提。总之,VM通过对Reference的操作,实现了引用状态的变更,由于这些类都是在java.lang下,所以这也是用户手动继承实现一个引用类可能会无效的原因了。

Reference-handler线程

在Reference内部有一个类叫ReferenceHandler,它继承了Thread,是Reference-handler的实现。这个类主要的作用就是将pending中的链表节点逐个移动到Reference实例的ReferenceQueue中,最终将pending还原为null,如果pending为null,这个线程将会无限期挂起。

这个类是在Reference的静态代码块中实例化并运行的,由类加载的知识可以知道类的初始化在第一次使用这个类的时候在其之前完成,所以当用户决定使用一个Reference子类时,就会开始这个线程。线程默认的优先级就是最高的优先级MAX_PRIORITY,如果某个系统拥有比MAX_PRIORITY还要高得等级,该线程也会和内核线程同等优先级运行。总之,开始这个线程之前,Reference会保证这个线程以最高优先级运行。同时,这也是一个守护线程。

如果用户线程已经全部退出运行了,只剩下守护线程存在了,那么虚拟机也会退出,即退出程序。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

  • thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
  • 在Daemon线程中产生的新线程也是Daemon的。
  • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

enqueued 操作

Reference-handler线程的enqueued操作是通过调用Reference的ReferenceQueue实现的。本质就是调用ReferenceQueue的enqueued方法,传入需要enqueued的引用。enqueued方法将该引用的状态更改为ENQUEUED,此时这个引用的ReferenceQueue被替换成Null类,然后使用头插法把这个引用插入这个队列的队头里。如果已经是ENQUEUED状态的引用会直接退出方法。

这个方法,如果enqueued操作成功,即成功将一个引用插入队列,则返回true,其他情况返回false。

enqueued操作会锁定传入的引用对象,所以是同步的,而且入队时会进一步锁定队列,防止并发情况下插入失败。

引用锁

上文提到,如果pending为null,Reference-handler线程将会无限期挂起。那么总是要唤醒这个线程的,在哪里唤醒这个线程呢?要聊到这个话题,就要聊到Reference的锁。java.lang.ref.Reference<T>java.lang.ref.ReferenceQueue<T>中,各有一个自定义的锁类,上文提到的对象状态变更需要的同步操作,都需要持有这两个锁类的锁才能完成。两个类对锁的定义都很简单,就是一个空的类。

java.lang.ref.Reference<T>的锁

static private class Lock { };
private static Lock lock = new Lock();

java.lang.ref.ReferenceQueue<T>的锁

static private class Lock { };
private Lock lock = new Lock();

唯一的区别就是Reference的锁类引用带有static,带有static是因为对象用于与垃圾收集器同步。收集器必须在每个收集周期的开始处获取此锁。因此任何持有此锁的代码尽可能快地完成,不应该在持有这个锁的时候分配新对象,而且应避免调用用户代码。

可是代码中并没有Reference中锁的任何类似调用nolify方法等的唤醒操作,所以笔者认为,唤醒操作应该也是在JVM内部实现的。至于时机,可能是当一次GC结束后。

而ReferenceQueue的锁相对简单。当某个线程执行remove操作时,如果是空队列,则挂起这个线程,仅当达到Timeout或者执行enqueue操作才会被唤醒。由于只通过引用类调用,所以只有当状态更改时才会唤醒。Finalize线程会调用remove方法,这里不再详述。

Finalizer和FinalReference

finalize的执行也大同小异,都是通过static语句块启动一个线程,只是这里启动的是低优先级的线程。,而且最终的调用逻辑是通过sun.misc.JavaLangAccess类完成的。当然Runtime.runFinalization()方法和java.lang.Shutdown类通过调用native方法,再通过native中回调Finalizer中的runAllFinalizers方法也能执行finalize的调用。至于finalize是如何调用的,网上有博客,我就不再赘述了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容