Java | 难以置信!Java 居然有第五种引用类型

前言

Java Reference 类型是与虚拟机垃圾回收机制密切相关的知识点,同时也是面试重要考点之一。一般认为 Java 有四种 Reference(强引用 & 软引用 & 弱引用 & 虚引用),但是其实还有隐藏的第五种 Reference,你知道是什么吗?

在这篇文章里,我将总结引用类型的用法 & 区别,并基于 ART 虚拟机分析相关源码。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

提示:本文源码分析基于 Android 9.0 ART 虚拟机。

目录

1. 概述

1.1 什么是引用?

在 Java 中,引用的基本定义是:某一个对象 / 某一块内存的起始地址,这与 C/C++ 中指针的定义是类似的。从 JDK 1.2 开始,Java 扩充了引用的种类,根据引用强度的不同分为四种类型:强引用 & 软引用 & 弱引用 & 虚引用

1.2 引用的作用

不同引用类型的作用不尽相同,这一点很多文章没有明确指出。软引用 & 弱引用提供了更加灵活地控制对象生存期的能力,而虚引用提供了感知对象垃圾回收的能力。除了虚引用之外,Object#finalize() 也提供了感知对象被垃圾回收的能力,在第 5 节我将分析两者的原理与区别。

引用类型Class作用对象 GC 时机(不考虑 GC 策略)

强引用无/GC Root 可达就不会回收

软引用SoftReference灵活控制生存期空闲内存不足以分配新对象时

弱引用WeakReference灵活控制生存期每次GC

虚引用PhantomReference感知对象垃圾回收每次GC

提示:对象是否被 GC,不仅仅取决于引用类型,还取决于当次 GC 采用的策略。

1.3 对象的访问定位方式

根据引用访问对象,分为句柄访问 & 直接指针访问两种方式,你可以看我之前写过的一篇文章:《Java | Object obj = new Object() 占用多少字节?》

2. 引用 & 引用队列

这一节,我们先来分析下引用(Reference)& 引用队列(ReferenceQueue)的源码,以从中梳理出两者基本的依赖关系。

再次提示:本文源码分析基于 Android 9.0 ART 虚拟机。

2.1 Reference 源码分析

Reference 是抽象类,有四个子类:

SoftReference(软引用)

WeakReference(弱引用)

PhantomReference(虚引用)

FinalizerReference(@hide)

前三个相信你都见过,第四个 FinalizerReference 是@hide隐藏类,我在第 4 节再说。首先,我们还是先分析下 Reference 类的源码:

Reference.java

public abstract class Reference {        1、构造器    Reference(T referent) {        this(referent, null);    }    Reference(T referent, ReferenceQueue queue) {        this.referent = referent;        this.queue = queue;    }    2.1 引用指向的对象    private T referent;    2.2 获取引用指向的对象,如果对象被回收,返回 null    public T get() {        return getReferent();    }    2.3 清除引用关系    public void clear() {        clearReferent();    }    3、关联的引用队列    final ReferenceQueue queue;    4、疑问:这两个变量是什么作用呢?    Reference queueNext;    Reference pendingNext;    private final native T getReferent();    native void clearReferent();    ...}复制代码

这段源码并不复杂,主要关注以下几点:

1、创建引用对象的时候可以指定关联的 ReferenceQueue,默认为 null;

2、referent是引用指向的对象;

3、queue是关联的引用队列 ;

4、queueNext & pendingNext我在第 2.2 节讲。

可以看到,获取引用指向的对象和清除引用关系都是调用 native 方法:

java_lang_ref_Reference.cc

static jobject Reference_getReferent(JNIEnv* env, jobject javaThis) {    ScopedFastNativeObjectAccess soa(env);    ObjPtr ref = soa.Decode(javaThis);    通过 ReferenceProcessor 获得对象    ObjPtr const referent = Runtime::Current()->GetHeap()->GetReferenceProcessor()->GetReferent(soa.Self(), ref);    return soa.AddLocalReference(referent);}static void Reference_clearReferent(JNIEnv* env, jobject javaThis) {    ScopedFastNativeObjectAccess soa(env);    ObjPtr ref = soa.Decode(javaThis);    通过 ReferenceProcessor 清除引用关系    Runtime::Current()->GetHeap()->GetReferenceProcessor()->ClearReferent(ref);}复制代码

其中的ReferenceProcessor是 ART 中专门用与处理 Reference 对象的模块,后文我会重新提到。另外,对于 PhantomReference 来说,get()方法永远返回 null。

PhantomReference.java

public T get() {    return null;}复制代码

2.2 ReferenceQueue 源码分析

引用队列(ReferenceQueue)需要搭配软引用、弱引用和虚引用,源码如下:

ReferenceQueue.java

public class ReferenceQueue {    private Reference head = null;    private Reference tail = null;    public ReferenceQueue() { }    入队    boolean enqueue(Reference reference) {        synchronized (lock) {            if (enqueueLocked(reference)) {                lock.notifyAll();                return true;            }            return false;        }    }        入队(内部)    private boolean enqueueLocked(Reference r) {        ....    }    出队    public Reference poll() {        ...    }}复制代码

从源码可以看出,ReferenceQueue 是基于单链表的队列,其中方法内部的实现细节我就不贴出来了,不重要。

在这里我们主要关注下面几个方法:

ReferenceQueue.add(...)

ReferenceQueue.add(...)是静态方法,源码如下:

public static Reference unenqueued = null;静态方法:添加一个 Reference 对象static void add(Reference list) {    synchronized (ReferenceQueue.class) {        if (unenqueued == null) {            1、如果 unenqueued  为 null,则直接赋值            unenqueued = list;            } else {                            2.1 找到 unenqueued 的队尾            Reference last = unenqueued;            while (last.pendingNext != unenqueued) {                last = last.pendingNext;            }            2.2 将引用追加到 unenqueued  尾部            last.pendingNext = list;            last = list;            while (last.pendingNext != list) {                last = last.pendingNext;            }            last.pendingNext = unenqueued;        }        3、唤醒等待 ReferenceQueue.class 锁的线程        ReferenceQueue.class.notifyAll();    }}复制代码

可以看到,这个方法其实就是把参数 Reference 对象追加到unenqueued尾部。需要注意到,将对象追加到尾部后,还唤醒了等待 ReferenceQueue.class 锁的线程。这个线程在哪里呢?我在第 3 节讲。

ReferenceQueue.enqueuePending(...)

ReferenceQueue.enqueuePending(...)是静态方法,源码如下:

静态方法:引用入队public static void enqueuePending(Reference list) {    Reference start = list;    do {        获取引用关联的引用队列        ReferenceQueue queue = list.queue;        if (queue == null) {            1、如果引用没有关联的 ReferenceQueue,跳过            Reference next = list.pendingNext;            list.pendingNext = list;            list = next;        } else {            2、如果引用有关联的 ReferenceQueue            synchronized (queue.lock) {                2.1 遍历 pendingNext,如果属于该 queue,则执行入队                do {                    Reference next = list.pendingNext;                    list.pendingNext = list;                    入队                    queue.enqueueLocked(list);                    list = next;                } while (list != start && list.queue == queue);                2.2 唤醒在 queue.lock上等待锁的线程                queue.lock.notifyAll();            }        }    } while (list != start);}复制代码

以上源码比较绕,其实这个方法就是将引用对象添加到关联的引用队列中,随后唤醒了在 queue.lock 上等待锁的线程。

2.3 小结

看到这里,我们先来总结这一节的内容以及遇到的疑问:

1、在新建引用对象时,引用与引用队列建立关联,后者是基于单链表的队列;

2、静态方法 ReferenceQueue.add(...) 将参数 Reference 对象追加到 unenqueued 尾部,随后唤醒了等待 ReferenceQueue.class 锁的线程;

3、静态方法 ReferenceQueue.enqueuePending(...) 将引用对象添加到关联的引用队列中,随后唤醒在 queue.lock 上等待锁的线程。

那么,这些等待的线程在哪里呢?

3. 守护线程

在虚拟机启动时,会启动一些守护线程:

runtime.cc

void Runtime::StartDaemonThreads() {  调用 java.lang.Daemons.start()  Thread* self = Thread::Current();  JNIEnv* env = self->GetJniEnv();  env->CallStaticVoidMethod(WellKnownClasses::java_lang_Daemons,                            WellKnownClasses::java_lang_Daemons_start);}复制代码

Daemons.java

public static void start() {    启动四个守护线程    ReferenceQueueDaemon.INSTANCE.start();    FinalizerDaemon.INSTANCE.start();    FinalizerWatchdogDaemon.INSTANCE.start();    HeapTaskDaemon.INSTANCE.start();}private static abstract class Daemon implements Runnable {    private Thread thread;    private String name;    protected Daemon(String name) {        this.name = name;    }    public synchronized void start() {        startInternal();    }    public void startInternal() {        thread = new Thread(ThreadGroup.systemThreadGroup, this, name);        thread.setDaemon(true);        thread.start();    }    public void run() {        runInternal();    }    public abstract void runInternal();    protected synchronized boolean isRunning() {        return thread != null;    }}复制代码

Daemon 是Runnable 的抽象子类,它的四个实现类分别是 ReferenceQueueDaemon、FinalizerDaemon、FinalizerWatchdogDaemon 和 HeapTaskDaemon,类图如下:

引用自weread.qq.com/web/reader/…—— 邓凡平 著

3.1 ReferenceQueueDaemon 线程

private static class ReferenceQueueDaemon extends Daemon {    private static final ReferenceQueueDaemon INSTANCE = new ReferenceQueueDaemon();    ReferenceQueueDaemon() {        super("ReferenceQueueDaemon");    }    @Override    public void runInternal() {        while (isRunning()) {            Reference list;            1、同步            synchronized (ReferenceQueue.class) {                2、检查 - 等待                while (ReferenceQueue.unenqueued == null) {                    ReferenceQueue.class.wait();                }                list = ReferenceQueue.unenqueued;                ReferenceQueue.unenqueued = null;            }            3、将对象加入引用队列            ReferenceQueue.enqueuePending(list);        }    }}复制代码

可以看到,ReferenceQueueDaemon 线程的主要作用是轮询判断 ReferenceQueue.unenqueued 是否为空,如果不为空则调用上一节讲的  ReferenceQueue.enqueuePending(...) 。

提示:「检查 - 等待」「设置 - 唤醒」,这是典型的守卫暂停模式。

3.2 FinalizerDaemon 线程

已简化private static class FinalizerDaemon extends Daemon {    private static final FinalizerDaemon INSTANCE = new FinalizerDaemon();    注意:这个队列是 FinalizerReference 的静态变量    private final ReferenceQueue queue = FinalizerReference.queue;    FinalizerDaemon() {        super("FinalizerDaemon");    }    @Override public void runInternal() {        while (isRunning()) {            1、从引用队列中取出引用            FinalizerReference finalizingReference = (FinalizerReference)queue.poll();            2、执行引用所指向对象 Object#finalize()            doFinalize(finalizingReference);        }    @FindBugsSuppressWarnings("FI_EXPLICIT_INVOCATION")    private void doFinalize(FinalizerReference reference) {        2.1 移除 FinalizerReference 对象        FinalizerReference.remove(reference);        2.2 取出引用所指向的对象        Object object = reference.get();        2.3 清除引用关系        reference.clear();        2.4 调用 Object#finalize()        object.finalize();    }}复制代码

可以看到,FinalizerDaemon线程 的主要作用是轮询从引用队列中取出引用,并执行 Object#finalize() 。需要留意到这个队列其实是 FinalizerReference 的静态变量。FinalizerReference 就是第 2.1 节提到的 Reference 的子类之一(@hide),我在第 4 节再说。

3.3 FinalizerWatchdogDaemon 线程

用于监听 Object#finalize() 的执行耗时,如果执行时间超过MAX_FINALIZE_NANOS,则会退出虚拟机

private static final long MAX_FINALIZE_NANOS = 10L * NANOS_PER_SECOND;Os.kill(Os.getpid(), OsConstants.SIGQUIT);复制代码

3.4 小结

看到这里,我们先来总结这一节的内容以及遇到的疑问:

1、ReferenceQueueDaemon 守护线程等待 ReferenceQueue.class 的锁,轮询判断 ReferenceQueue.unenqueued 是否为空,如果不为空则调用 ReferenceQueue.enqueuePending(...) ;

2、FinalizerDaemon 守护线程等待 queue.lock 锁,并轮询从 FinalizerReference.queue 中取出引用,执行 Object#finalize() 。

那么,FinalizerReference.queue 中的引用是从哪里来的呢?

4. finalize() 函数执行原理分析

4.1 finalizable 标记位

ClassLinker 在加载类时,用于解析其成员方法的函数 LoadMethod(),会检查方法名是否为 finalize(),是则标记该类为 finalizable。

4.2 新建 FinalizerReference 对象

如果一个类被标记为 finalizable,在新建对象时,ART 虚拟机会调用Heap:AddFinalizerReference(...):

heap.cc

void Heap::AddFinalizerReference(Thread* self, ObjPtr* object) {    ScopedObjectAccess soa(self);    ScopedLocalRef arg(self->GetJniEnv(), soa.AddLocalReference(*object));    jvalue args[1];    args[0].l = arg.get();    调用 java.lang.ref.FinalizerReference.add(...)    InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_FinalizerReference_add, args);    *object = soa.Decode(arg.get());}复制代码

FinalizerReference.java

public static final ReferenceQueue queue = new ReferenceQueue();private static FinalizerReference head = null;private FinalizerReference prev;private FinalizerReference next;public static void add(Object referent) {    FinalizerReference reference = new FinalizerReference(referent, queue);    synchronized (LIST_LOCK) {        头插法        reference.prev = null;        reference.next = head;        if (head != null) {            head.prev = reference;        }        head = reference;    }}复制代码

可以看到,每创建一个标记为finalizable 类实例的对象,ART 虚拟机还创建一个指向它的 FinalizerReference 对象,并将 FinalizerReference 对象加入 FinalizerReference 静态成员变量 queue。

4.3 垃圾回收

虚拟机在即将回收对象时,会调用第 2.2 节提到的ReferenceQueue.add(...):

reference_processor.cc

class ClearedReferenceTask : public HeapTask {    ...    InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_ReferenceQueue_add, args);    ...};复制代码

4.4 执行 finalize() 方法

执行 finalize() 方法的源码我们在第 3.2 节讲了,要点是:FinalizerDaemon 线程等待 queue.lock 锁,并轮询从 FinalizerReference.queue 中取出引用,执行 Object#finalize() 。

4.5 小结

看到这里,我们先来总结这一节的内容:

1、重写了 Object#finalize() 的类,在新建对象同时会新建关联的 FinalizerReference;

2、在对象即将被 GC 时,会调用 ReferenceQueue.add(...),将引用对象追加到 unenqueued 尾部,并唤醒等待 ReferenceQueue.class 锁的线程;

3、ReferenceQueueDaemon 守护线程被唤醒,判断 ReferenceQueue.unenqueued 是否为空,如果不为空则调用 ReferenceQueue.enqueuePending(...),并唤醒等待 queue.lock 锁的线程;

4、FinalizerDaemon 守护线程被唤醒,从 FinalizerReference.queue 中取出引用,执行 Object#finalize() 。

5. 感知对象垃圾回收

除了虚引用之外,Object#finalize() 也提供了感知对象被垃圾回收的能力,但是虚引用更加优雅,性能更高。

主要原因是 Object#finalize() 排队在 FinalizeDaemon 守护线程中执行的,由于守护线程的优先级低于其他线程。在 CPU 资源紧张的情况,守护线程竞争到的 CPU 时间片少,这个时候引用对象就会堆积在队列里,增大 OOM 的风险,回收时机也不稳定。

相比之下,使用虚引用的话,可以根据情况使用多个线程来处理。或者直接使用 PhantomReference 的子类 Cleaner 更为简便。

public class Cleaner extends PhantomReference {    ...}复制代码

6. 总结

从 JDK 1.2 开始,Java 扩充了引用的种类,软引用 & 弱引用提供了更加灵活地控制对象生存期的能力,虚引用提供了感知对象垃圾回收的能力;

强引用只有当对象没有到 GC Root 的引用链时可回收;软引用不保证每次 GC 都会被回收,只有当空闲内存不足以分配新对象时被回收;弱引用每次 GC 都会被回收;虚引用跟回收时机没有关系,只是提供了一种感知对象垃圾回收的能力;

FinalizerReference 也是一种引用类型,是隐藏类,用于实现在回收对象之前调用 Object#finalize() 的功能;

Object#finalize() 也提供了感知对象被垃圾回收的能力,但由于 finalize() 是在守护线程执行的,在 CPU 资源紧张时引用会堆积在引用队列中,增大 OOM 风险,回收时机也不稳定。

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

推荐阅读更多精彩内容

  • 点赞关注,不再迷路,你的支持对我意义重大!🔥 Hi,我是丑丑。本文 「Java 路线」| 导读 —— 他山之石,可...
    彭旭锐阅读 603评论 0 1
  • Java引用概述 StrongReference(强引用) 不存在这个类 默认实现 Java.lang.ref提供...
    Gxgeek阅读 522评论 0 2
  • 1 Java中的四种引用 在Java中提供了四个级别的引用:强引用,软引用,弱引用和虚引用。在这四个引用类型中,只...
    爱健身的兔子阅读 2,170评论 0 1
  • 原始地址:Android 中的引用类型初探 引用种类 强引用:在 GC 中如果发现一个对象是可达的,那么 GC 在...
    轻微阅读 1,533评论 3 51
  • 强引用 ( Strong Reference ) 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝...
    tomas家的小拨浪鼓阅读 2,810评论 1 4