「Java 路线」| 引用类型 & Finalizer 机制

简介: 「Java 路线」| 引用类型 & Finalizer 机制

前言


  • Java Reference 类型 是与虚拟机垃圾回收机制密切相关的知识点,同时也是面试重要考点之一。一般认为 Java 有四种 Reference(强引用 & 软引用 & 弱引用 & 虚引用),但是其实还有隐藏的第五种 Reference,你知道是什么吗?
  • 在这篇文章里,我将总结 引用类型的用法 & 区别,并基于 ART 虚拟机分析相关源码。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。


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


目录

image.png


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<T> {
    1、构造器
    Reference(T referent) {
        this(referent, null);
    }
    Reference(T referent, ReferenceQueue<? super T> 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<? super T> 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<mirror::Reference> ref = soa.Decode<mirror::Reference>(javaThis);
    通过 ReferenceProcessor 获得对象
    ObjPtr<mirror::Object> const referent = Runtime::Current()->GetHeap()->GetReferenceProcessor()->GetReferent(soa.Self(), ref);
    return soa.AddLocalReference<jobject>(referent);
}
static void Reference_clearReferent(JNIEnv* env, jobject javaThis) {
    ScopedFastNativeObjectAccess soa(env);
    ObjPtr<mirror::Reference> ref = soa.Decode<mirror::Reference>(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<T> {
    private Reference<? extends T> head = null;
    private Reference<? extends T> tail = null;
    public ReferenceQueue() { }
    入队
    boolean enqueue(Reference<? extends T> reference) {
        synchronized (lock) {
            if (enqueueLocked(reference)) {
                lock.notifyAll();
                return true;
            }
            return false;
        }
    }
    入队(内部)
    private boolean enqueueLocked(Reference<? extends T> r) {
        ....
    }
    出队
    public Reference<? extends T> poll() {
        ...
    }
}
复制代码


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

image.png

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


  • 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 节 讲。


image.png

  • 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 上等待锁的线程。


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


image.png

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,类图如下:

image.png

引用自 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(...) 。


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


image.png

3.2 FinalizerDaemon 线程


已简化
private static class FinalizerDaemon extends Daemon {
    private static final FinalizerDaemon INSTANCE = new FinalizerDaemon();
    注意:这个队列是 FinalizerReference 的静态变量
    private final ReferenceQueue<Object> 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<mirror::Object>* object) {
    ScopedObjectAccess soa(self);
    ScopedLocalRef<jobject> arg(self->GetJniEnv(), soa.AddLocalReference<jobject>(*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<mirror::Object>(arg.get());
}
复制代码


FinalizerReference.java


public static final ReferenceQueue<Object> queue = new ReferenceQueue<Object>();
private static FinalizerReference<?> head = null;
private FinalizerReference<?> prev;
private FinalizerReference<?> next;
public static void add(Object referent) {
    FinalizerReference<?> reference = new FinalizerReference<Object>(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() 。


image.png

5. 感知对象垃圾回收


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


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

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


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


6. 总结


  • 从 JDK 1.2 开始,Java 扩充了引用的种类,软引用 & 弱引用提供了更加灵活地控制对象生存期的能力,虚引用提供了感知对象垃圾回收的能力;
  • 强引用只有当对象没有到 GC Root 的引用链时可回收;软引用不保证每次 GC 都会被回收,只有当空闲内存不足以分配新对象时被回收;弱引用每次 GC 都会被回收;虚引用跟回收时机没有关系,只是提供了一种感知对象垃圾回收的能力;
  • FinalizerReference 也是一种引用类型,是隐藏类,用于实现在回收对象之前调用 Object#finalize() 的功能;
  • Object#finalize() 也提供了感知对象被垃圾回收的能力,但由于 finalize() 是在守护线程执行的,在 CPU 资源紧张时引用会堆积在引用队列中,增大 OOM 风险,回收时机也不稳定。
目录
相关文章
|
8月前
|
设计模式 人工智能 安全
AQS:Java 中悲观锁的底层实现机制
AQS(AbstractQueuedSynchronizer)是Java并发包中实现同步组件的基础工具,支持锁(如ReentrantLock、ReadWriteLock)和线程同步工具类(如CountDownLatch、Semaphore)等。Doug Lea设计AQS旨在抽象基础同步操作,简化同步组件构建。 使用AQS需实现`tryAcquire(int arg)`和`tryRelease(int arg)`方法以获取和释放资源,共享模式还需实现`tryAcquireShared(int arg)`和`tryReleaseShared(int arg)`。
441 32
AQS:Java 中悲观锁的底层实现机制
|
8月前
|
人工智能 Java 关系型数据库
Java——SPI机制详解
SPI(Service Provider Interface)是JDK内置的服务提供发现机制,主要用于框架扩展和组件替换。通过在`META-INF/services/`目录下定义接口实现类文件,Java程序可利用`ServiceLoader`动态加载服务实现。SPI核心思想是解耦,允许不同厂商为同一接口提供多种实现,如`java.sql.Driver`的MySQL与PostgreSQL实现。然而,SPI存在缺陷:需遍历所有实现并实例化,可能造成资源浪费;获取实现类方式不够灵活;多线程使用时存在安全问题。尽管如此,SPI仍是Java生态系统中实现插件化和模块化设计的重要工具。
312 0
|
6月前
|
人工智能 前端开发 安全
Java开发不可不知的秘密:类加载器实现机制
类加载器是Java中负责动态加载类到JVM的组件,理解其工作原理对开发复杂应用至关重要。本文详解类加载过程、双亲委派模型及常见类加载器,并介绍自定义类加载器的实现与应用场景。
289 4
|
8月前
|
Java 区块链 网络架构
酷阿鲸森林农场:Java 区块链系统中的 P2P 区块同步与节点自动加入机制
本文介绍了基于 Java 的去中心化区块链电商系统设计与实现,重点探讨了 P2P 网络在酷阿鲸森林农场项目中的应用。通过节点自动发现、区块广播同步及链校验功能,系统实现了无需中心服务器的点对点网络架构。文章详细解析了核心代码逻辑,包括 P2P 服务端监听、客户端广播新区块及节点列表自动获取等环节,并提出了消息签名验证、WebSocket 替代 Socket 等优化方向。该系统不仅适用于农业电商,还可扩展至教育、物流等领域,构建可信数据链条。
|
10月前
|
缓存 Dubbo Java
理解的Java中SPI机制
本文深入解析了JDK提供的Java SPI(Service Provider Interface)机制,这是一种基于接口编程、策略模式与配置文件组合实现的动态加载机制,核心在于解耦。文章通过具体示例介绍了SPI的使用方法,包括定义接口、创建配置文件及加载实现类的过程,并分析了其原理与优缺点。SPI适用于框架扩展或替换场景,如JDBC驱动加载、SLF4J日志实现等,但存在加载效率低和线程安全问题。
494 7
理解的Java中SPI机制
|
8月前
|
人工智能 JavaScript Java
Java反射机制及原理
本文介绍了Java反射机制的基本概念、使用方法及其原理。反射在实际项目中比代理更常用,掌握它可以提升编程能力并理解框架设计原理。文章详细讲解了获取Class对象的四种方式:对象.getClass()、类.class、Class.forName()和类加载器.loadClass(),并分析了Class.forName()与ClassLoader的区别。此外,还探讨了通过Class对象进行实例化、获取方法和字段等操作的具体实现。最后从JVM类加载机制角度解析了Class对象的本质及其与类和实例的关系,帮助读者深入理解Java反射的工作原理。
215 0
|
9月前
|
存储 Java 编译器
Java 中 .length 的使用方法:深入理解 Java 数据结构中的长度获取机制
本文深入解析了 Java 中 `.length` 的使用方法及其在不同数据结构中的应用。对于数组,通过 `.length` 属性获取元素数量;字符串则使用 `.length()` 方法计算字符数;集合类如 `ArrayList` 采用 `.size()` 方法统计元素个数。此外,基本数据类型和包装类不支持长度属性。掌握这些区别,有助于开发者避免常见错误,提升代码质量。
896 1
|
10月前
|
缓存 运维 Java
Java静态代码块深度剖析:机制、特性与最佳实践
在Java中,静态代码块(或称静态初始化块)是指类中定义的一个或多个`static { ... }`结构。其主要功能在于初始化类级别的数据,例如静态变量的初始化或执行仅需运行一次的初始化逻辑。
344 4
|
Java 开发者
Java中的异常处理机制深度剖析####
本文深入探讨了Java语言中异常处理的重要性、核心机制及其在实际编程中的应用策略,旨在帮助开发者更有效地编写健壮的代码。通过实例分析,揭示了try-catch-finally结构的最佳实践,以及如何利用自定义异常提升程序的可读性和维护性。此外,还简要介绍了Java 7引入的多异常捕获特性,为读者提供了一个全面而实用的异常处理指南。 ####
219 20
|
运维 Java 编译器
Java 异常处理:机制、策略与最佳实践
Java异常处理是确保程序稳定运行的关键。本文介绍Java异常处理的机制,包括异常类层次结构、try-catch-finally语句的使用,并探讨常见策略及最佳实践,帮助开发者有效管理错误和异常情况。
979 6