【java.lang.ref】PhantomReference & jdk.internal.ref.Cleaner

2023-10-27

目录

  • 零、前情概要
    • ref包内容
    • 系列目录
    • 上一章回顾
  • 一、PhantomReference
    • 适用场景
    • 优雅和提前规避
      • Java中使用native memory
      • 常规做法
      • 借助于Java引用机制
    • 处理流程
  • 二、jdk.internal.ref.Cleaner
    • 源码注释
    • 源码概览
      • dummyQueue
      • add/remove
      • 为什么需要这条双向链表
      • create/thunk/clean
    • java.nio.DirectByteBuffer
    • 为什么不让你使用Cleaner
  • 总结


零、前情概 要

1.java.lang.ref包的内容

  • Reference & ReferenceQueue & ReferenceHandler 
    • SoftReference & WeakReference
    • PhantomReference
      • jdk.internal.ref.Cleaner
    • FinalReference
      • Finalizer & FinalizerThread
  • java.lang.ref.Cleaner
  • # 其中会涉及两个虚拟机线程:ReferenceHandler & FinalizerThread

2.系列目录

3.上一章回顾

  • 弱引用的适用场景和相关案例
    • 理解弱可达
    • 弱引用的处理流程
  • 软引用的适用场景
    • 理解软可达
  • 虚拟机是如何量化软引用的“内存紧张”
    • 软引用的时间戳
    • -XX:SoftRefLRUPolicyMSPerMB
    • 软引用清除策略ReferencePolicy
    • Java引用预处理ReferenceProcessor
    • 模型逻辑:正常情况、GC频繁、异常情况


一、PhantomReference

1.使用场景

API documentation:PhantomReference (Java SE 15 & JDK 15) (oracle.com)

Phantom reference objects, which are enqueued after the collector determines that their referents may otherwise be reclaimed. Phantom references are most often used to schedule post-mortem cleanup actions.

Suppose the garbage collector determines at a certain point in time that an object is phantom reachable. At that time it will atomically clear all phantom references to that object and all phantom references to any other phantom-reachable objects from which that object is reachable. At the same time or at some later time it will enqueue those newly-cleared phantom references that are registered with reference queues.

In order to ensure that a reclaimable object remains so, the referent of a phantom reference may not be retrieved: The get method of a phantom reference always returns null.

如API文档所述,虚引用一般用来做事后清理。为了确保虚引用的referent成为可回收对象之后不被干扰能被顺利清理掉,PhantomReference的get方法直接返回null,而不会返回referent。

这就是虚引用与软引用、弱引用的最大区别。软引用和弱引用的referent变成弱可达之后,通过其get方法还是能够让referent恢复成强可达状态,但是PhantomReference直接堵死了这条路。

除此之外,PhantomReference只有一个构造函数,必须传入ReferenceQueue,以便在referent确定会被回收之后得到一个通知。

public class PhantomReference<T> extends Reference<T> {

    /**
     * Returns this reference object's referent.  Because the referent of a
     * phantom reference is always inaccessible, this method always returns
     * {@code null}.
     *
     * @return {@code null}
     */
    public T get() {
        return null;
    }

    /**
     * Creates a new phantom reference that refers to the given object and
     * is registered with the given queue.
     *
     * <p> It is possible to create a phantom reference with a {@code null}
     * queue, but such a reference is completely useless: Its {@code get}
     * method will always return {@code null} and, since it does not have a queue,
     * it will never be enqueued.
     *
     * @param referent the object the new phantom reference will refer to
     * @param q the queue with which the reference is to be registered,
     *          or {@code null} if registration is not required
     */
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

ReferenceQueue用在什么地方?或者说怎么用ReferenceQueue。

WeakHashMap用ReferenceQueue,是因为entity是强可达的,形如WeakHashMap -> table[] -> entity。

entity extends WeakReference,key作为referent,当key弱可达时entity会被GC处理,即让entity进入创建时关联的ReferenceQueue,随后在expungeStaleEntries方法中通过ReferenceQueue拿到这个entity,然后断开其强可用,最终key被回收,随后继承弱引用的entity也会不可达被回收。

换言之,WeakHashMap使用ReferenceQueue是因为弱引用entity本身强可达,所以在referent(即key)弱可达之后继承弱引用WeakReference的entity最终被放入ReferenceQueue,随后通过ReferenceQueue拿到继承弱引用WeakReference的entity,接着到map.table[]中找到entity,然后断开map.table[] -> entity这条强引用,释放过期失效的entity。

补充:ThreadLocal没有使用ReferenceQueue,他是在get/set/remove的时候通过遍历map.table[],查找key == null的entity,然后断开这些entity的强引用。

所以这就是ReferenceQueue的使用场景:虽然资源对象referent弱可达(Reference在被GC挂到pending-reference list上的时候还会被clear referent,变成不可达),但如果在应用场景中Reference本身一直强可达,那么当资源对象referent确定会被回收以后,就可以通过队列拿到Reference,然后断开Reference自身的强引用。

对于PhantomReference来说,如果场景中只需要断开PhantomReference的强引用,那确实是ReferenceQueue的使用姿势。但是PhantomReference的主要适用场景往往不是应对Java堆内存资源(SoftReference、WeakReference主要适用场景才是Java堆内存资源),而是什么native memory、文件句柄等。这时候你通过ReferenceQueue拿到PhantomReference是没什么用的。

在之前第二章,谈为什么要Java引用的时候,是希望有一种机制能够让你可以提前规避忘记关闭资源所造成的风险,或者以一种优雅的方式释放资源。

这种“优雅”和“提前规避风险”体现在哪里?

“我写好资源类,你直接用就行了,你不需要关注怎么关闭资源,我连close都不提供。”

回想一下,SoftReference和WeakReference就是这样的,分配给referent的Java堆内存资源,你只要把referent传入SoftReference、WeakReference,GC什么时候回收referent的Java堆内存资源,你需要关注吗?

PhantomReference不同于SoftReference、WeakReference,这俩主要的使用场景是用于回收Java堆内存资源,已经有GC来自动做这个事情。而PhantomReference的主要使用场景并不是用来回收Java堆内存资源,而是堆外的native memory、文件句柄、socket端口等,都是GC不能释放的资源,都需要你去主动close

所以单纯的想要依靠虚引用来做事后清理是有难度的,因为关闭资源的信息在referent中,而你根本拿不到虚引用的资源对象referent,你只能通过ReferenceQueue拿到PhantomReference。但仔细想想,你拿到一堆PhantomReference有个毛用?referent被GC干掉了,但是分配给referent的native memory、socket端口、文件句柄等可都挂在JVM进程上,referent被GC回收了,这些资源释放的入口也被断掉了,这波反向操作就如同打开了资源泄露的大门。

所以,不应该、也不建议直接使用PhantomReference,而是要自己新建一个子类去继承PhantomReference(为什么这么做,请继续往下看)。出于个人朴素的情感,我觉得java.lang.ref.PhantomReference应该定义成抽象类,这样能更大程度上避免误用。

2.优雅和提前规避

假设要释放堆外的native memory,要怎么优雅和提前规避?

Java中使用native memory

在Java中,使用native memory要借助于jdk.internal.misc.Unsafe:

  • 分配native memory:long allocateMemory(long bytes),入参是你要申请多少字节的native memory,返回值是指向申请的这片内存地址(虚拟地址),毫无疑问是在JVM进程的虚拟地址空间分配,等到实际使用的时候才会通过缺页异常分配物理内存;
    • 相当与C里面的malloc:void *malloc(size_t size),入参size是内存块的大小,字节为单位,返回值是一个指针,指向在虚拟地址空间分配的内存;
  • 初始化native memory:void setMemory(long address, long bytes, byte value),将申请的内存初始化为指定值,address是allocateMemory返回的虚拟地址,bytes是指要初始化多少字节,value是指定的初始化值(通常是初始化为零值),调用该函数会通过缺页异常分配实际的物理页;
    • 相当于C里面的mmset:void *memset(void *str, int c, size_t n),入参*str是要初始化的内存指针,c是指定初始化的值,n是要初始化的长度单位字节;
  • 释放native memory:void freeMemory(long address),入参address就是allocateMemory返回的虚拟地址;
    • 相当于C里面的free:void free(void *ptr),入参*ptr是要释放的内存指针,如果是一个空指针,将不会执行任何动作;

分配native memory:

初始化native memory:

释放native memory:

常规做法

常规的做法是是抽象一个NativeMemory类,有一个属性long address,三个方法allocateMemroy、setMemroy、freeMemory。为了读写这片内存,你至少还得有一个属性long offset的偏移地址和read(size, byte[])/write(size, byte[])方法,read(size, byte[])中address+offset等于读操作的起始地址,读size个字节,读完之后offset=offset+size,write方法同理。

调用方拿到NativeMemroy,依次调用allocateMemory、setMmemory,然后read(size, byte[])/write(size, byte[]),操作完native memory之后,一定不要忘记freeMemory,否则就内存泄漏。

这样可以用,但是这不满足要求,调用方是有可能忘记调用freeMemory从而导致native memory泄露。我们希望达到的效果是调用方只管调用,不用关注怎么释放资源,不需要提供freeMemory方法。

借助于Java引用机制的实现

要优雅、要提前规避,这时候你需要PhantomReference,因为Java引用机制能够让你可以提前规避忘记关闭资源所造成的风险,或者以一种优雅的方式释放资源。

同样和上例抽象一个一模一样的NativeMemory,在NativeMemory内部抽象一个Metadata extends PhantomReference,如以下示例的伪代码:

class NativeMemory {
    // 内存基址
    private long address;
    // 当前操作的偏移地址
    private long offset;

    NativeMemory(int size) {
        long address = allocateMemory(size);
        setMemory(address, size, (byte)0);
        // 这里会将继承虚引用的Metadata挂到静态链表head上,避免虚引用Metadata不可达被GC掉,从而导致Java引用的机制中断
        // 其中NativeMemory作为referent
        // address是释放native memory的基址指针
        new Metadata(this, address);
    }

    allocateMemory();
    setMemory();
    read();
    write();

    // -------------------------------------

    static class Metadata extends PhantomReference {
        private static ReferenceQueue queue = new ReferenceQueue();
        // head和next属性是为了保证虚引用Metadata不变成不可达,从而破坏Java引用机制
        private static Metadata head;
        private Metadata next;

        private long address;
        Metadata(object referent, long address){
            super(referent, queue);
            this.address = address;
            add(this);
        }

        static synchronized add(Metadata md) {
            if(null != head) 
                md.next = head;
            head = md;
        }

        // 释放native memory
        public void clean(){
            Unsafe.freeMemory(address);
        }
        
        static class FreeNativeMemroyThread extends Thread {
            public void run(){
                while(true){
                // 把Metadata从head链表上摘下来
                // 然后调用其clean方法
                }
            }
        }
        
        // 启动FreeNativeMemroyThread
        // 该线程的逻辑是拿到queue中的Metadata,并调用其clean方法释放native memory
        static {
            new FreeNativeMemoryThread().setDaemon(true).start();
        }
    }
}

代码逻辑是这样的:

  • 既然不需要调用方关注native memory的释放,那么释放native memory的元数据address就需要保存到其他地方,不能让address随着NativeMemory实例的消亡而消失;
  • 所以,在NativeMemory的构造函数中实例化一个虚引用对象Metadata,将NativeMemroy和address传入,NativeMemory作为虚引用的referent;
  • 当作为referent的NativeMemory实例被GC回收的时候,借助于Java引用机制,虚引用Metadata也会被GC挂到pending-reference list上;
  • 随后ReferenceHandler线程将Metadata从pending-reference list上摘下来,放入关联的ReferenceQueue(本例中是Metadata的静态属性queue);
  • 最后,我们自定义的线程FreeNativeMemoryThread会将虚引用Metadata从queue中取出并调用其clean方法释放native memory,同时也从head链表上摘下来断开MetaData的强可达;

这样一顿操作下来,借助于Java引用机制,调用方就不再需要关注native memory的释放。

这份伪代码能满足要求,遗憾的是并不通用,因为清理资源的逻辑和引用强绑定。如果可以将清理资源的动作封装成规范,让不同调用方按照规范实现自身的清理逻辑,然后传入引用对象,让FreeNativeMemoryThread线程去调用这些自定义的清理逻辑,这就能满足多方需求。

实际上,jdk.internal.ref.Cleaner已经这样做了。

3.处理流程

在介绍jdk.internal.ref.Cleaner之前,我们再回顾一下虚引用的处理流程。

虚引用的处理流程其实和弱引用是一样的,在JVM中不会像软引用那样需要特殊处理。当GC发生的时候,软引用在ReferenceProcessor::discover_reference中会根据配置的软引用清除策略做出是否清理的判断;而虚引用和弱引用一样,只要发生GC,在ReferenceProcessor::discover_reference中直接就往下走处理逻辑,并没有什么特殊判断。

如第二章Reference中Java引用的处理流程所述,虚引用的处理流程:

  • 当GC的时候,垃圾收集器发现referent虚可达
  • GC会将封装该referent的Java引用对象挂到pending-reference list上并clear referent(断开referent的虚引用,即Reference.referent = null)
  • 然后线程间通信,唤醒阻塞在waitForReferencePendingList上的ReferenceHandler线程
  • ReferenceHandler线程拿到pending-reference list,然后循环处理该链表上的所有引用节点
  • 如果是虚引用,则拿到其实例化时关联的队列,并调用queue.enqueue将引用入队列,第一个生产者-消费者模型结束,进入队列作后等待下一步的处理
  • 如果没有关联队列,则流程结束


二、jdk.internal.ref.Cleaner

1.源码注释

General-purpose phantom-reference-based cleaners.

Cleaners are a lightweight and more robust alternative to finalization. They are lightweight because they are not created by the VM and thus do not require a JNI upcall to be created, and because their cleanup code is invoked directly by the reference-handler thread rather than by the finalizer thread. They are more robust because they use phantom references, the weakest type of reference object, thereby avoiding the nasty ordering problems inherent to finalization.

A cleaner tracks a referent object and encapsulates a thunk of arbitrary cleanup code. Some time after the GC detects that a cleaner's referent has become phantom-reachable, the reference-handler thread will run the cleaner. Cleaners may also be invoked directly; they are thread safe and ensure that they run their thunks at most once.

Cleaners are not a replacement for finalization. They should be used only when the cleanup code is extremely simple and straightforward. Nontrivial cleaners are inadvisable since they risk blocking the reference-handler thread and delaying further cleanup and finalization.

从源码类注释可知:

  • Cleaner相比较finalization(下一章才会谈到finalization)是轻量级、更强壮的机制,这住体现在两个方面:
    • 轻量级
      • 不是由VM创建也不需要通过JNI调用;
      • Cleaner释放资源的代码是由ReferenceHandler线程调用,而不是FinalizerThread线程;
    • 更健壮:使用的是引用对象中引用关系最弱的PhantomReference,因此避免了finalization排队的消耗
  • Cleaner追踪referent,封装了一个自定义释放资源代码的thunk
    • 当GC检测到Cleaner的referent变成虚可达之后,Cleaner会挂到pending-reference list上,然后由ReferenceHandler线程调用Cleaner的clean方法,在该方法中会执行thunk自定义释放资源的逻辑
    • 你也可以直接调用Cleaner的clean方法
    • clean方法的调用线程安全,并且确保只执行一次
  • Cleaner不是finalization机制的替代,Cleaner适用于释放资源的代码逻辑简单直接的场景。如果释放资源的代码逻辑过于繁复,有可能会阻塞或延迟ReferenceHandler线程,这样Java引用机制就会出现风险;
    • 在第二章ReferenceHandler的源码中会通过一个静态块预加载并初始化Cleaner,当时只说和Cleaner的适用场景有关系,这里补上更详细一点的解释:pre-load and initialize Cleaner class so that we don't get into trouble later in the run loop if there's memory shortage while loading/initializing it lazily. 之所以要预加载并初始化Cleaner,主要有两个方面的考量:
      • 一是因为Cleaner释放资源的clean方法在ReferenceHandler中调用,也就是说其实是ReferenceHandler线程来执行释放资源的逻辑。所以Cleaner适用于释放资源代码逻辑简单直接的场景,这样就不会导致ReferenceHandler线程过度地被消耗在某一个Cleaner上,从而保证Java引用机制能够不被拖延阻塞;
      • 更重要的是,延迟加载和初始化Cleaner,在内存短缺的场景中可能会陷入以下循环困境:因为内存资源短缺,所以需要加载Cleaner到内存来释放资源;但是正因为内存资源短缺,要加载Cleaner就先要GC;而GC正在加载Cleaner到内存来释放资源。。。

2.源码概览

jdk.internal.ref.Cleaner:

  • 属性
    • static dummyQueue
    • static first
    • prev
    • next
    • thunk
  • 构造函数:Cleaner(Object referent, Runnable thunk)
  • 方法
    • static create(Object ob, Runnable thunk)
    • clean()
    • add/remove

dummyQueue

正如注释所述,dummyQueue的存在只是因为Cleaner extends PhantomReference,而PhantomReference的构造函数中要求必须传入一个ReferenceQueue,实际上dummyQueue永远是一个空队列,不会放入任何Reference节点。

回顾一下reference-handler线程处理pending-reference list的逻辑,如果节点是jdk.internal.ref.Cleaner,那么直接调用其clean方法走自定义的清理逻辑;否则走进队列的逻辑。所以,如果是jdk.internal.ref.Cleaner,那么就不会走到入队列的逻辑。因此dummyQueue只是因为PhantomReference的构造函数要求,而实际上不会有Cleaner进入队列,它始终是一个空队列。

add/remove

通过first、prev、next三个属性,在Cleaner中维护了一条双向链表。

Cleaner机制可以简化成一个多生产单消费的模型,所以add、remove方法操作这条双向链表的时候需要同步。

add:头插法将新节点加入双向链表

remove

  • 新创建的Cleaner cl,其prev、next属性都是null
  • 通过add头插法插入双向链表,此时cl.next指向原头节点,cl.prev的值依旧为null,头节点指针first指向cl
  • 当调用remove将cl移除队列的时候
    • 如果cl已经被移除队列了,此时cl的next、prev都会指向自身,所以方法一开始就判断ck是否已经被删除过了
    • 如果cl还没有移除队列,分成两种情况,cl是头节点和普通节点:
      • cl是头节点,那么要处理头结点指针first:
        • cl.next为null值,链表只有一个节点,那么头节点指针first赋值为null;
        • cl.next不为null,链表中有多个节点,那么头节点指针first指向cl.next;
      • cl是普通节点:处理好头结点指针first,那么就要考虑断开cl节点,此时无论cl是不是头节点,都要把cl前后的双向链断开
      • 最后,将cl的next、prev都指向cl自身,表明cl是被删除的状态

这个过程并不复杂。但是你需要深入思考的是:为什么需要这条双向链表?

为什么需要这条双向链表

这条双向链表看似多余,实则必须,因为需要这条双向链表来保证Java引用机制的正常运行。

有两个原因:

  • 为什么是双向链表:当ReferenceHandler线程调用Cleaner的clean方法的时候,会将Cleaner从链表上摘下来,此时摘除的节点有可能是头节点,有可能是普通节点,如果换成单链表需要遍历,双向链表做摘除操作方便很多;
  • 为什么需要链表:如果Cleaner不可达,那么他走不到Java引用机制,GC阶段直接就被回收了,这一点参看下面的代码示例做验证;
    • 原因分析:垃圾清理通常分成两步,先要找到垃圾,然后才能回收垃圾。GC算法虽然都叫垃圾收集算法,实际上也分成两类,一类是找垃圾算法,例如可达性分析、引用计数,一类是垃圾清理算法,例如标记清理、标记整理、复制算法、分代收集等。在可达性分析中,通过GC roots找到强可达的存活对象,然后在对象头打标,如果发现对象是Reference,那么GC就会调用ReferenceProcessor::discover_reference方法做一些判断,例如referent还有没有强引用、Reference是不是一个软引用等,如果这些判断都通过,那么最后会把这个Reference挂到DiscoveredList上。所以,如果Reference本身都是不可达的,那么就不会有后续的Java引用流程。

将System.gc()换到phantomReference = null之前,通过queue.poll就能拿到phantomReference。两者的区别就是GC的时候,phantomReference是不是强可达的,不是那么会被当做垃圾清理掉,是那么就走Java引用流程。

create\thunk\clean

整个jdk.internal.ref.Cleaner就两个pubic的方法,一个是create,另一个就是clean了。

create是整个Cleaner的入口。入参的object作为虚引用的referent,thunk封装了对资源对象referent的自定义清理逻辑。

在clean方法中,会调用remove将Cleaner从双向链表摘除,然后调用create传入的thunk执行自定义的清理资源代码。

而chunk是一个Runnable,关闭资源所需的元数据都需要封装在其中。

jdk.internal.ref.Cleaner的实现和使用并不复杂,看一个java.nio.DirectByteBuffer的例子基本上就能融会贯通。

3.DirectByteBuffer

  • allocateMemory、setMemory如上所述,都需要借助Unsafe;
  • Bits.reserveMemory、Bits.unreserveMemory:JVM参数-XX:MaxDirectMemorySize用来配置允许使用的native memory的大小,Bits.reserveMemory、Bits.unreserveMemory这两个方法通过几个static AtomicLong在Java中保存native memory的使用情况,如果已经分配超过了-XX:MaxDirectMemorySize则不允许再通过Unsafe.allocateMemory申请native memory,如果通过Unsafe.freeMemory释放native memory,那么通过Bits.unreserveMemory把释放的size加回到AtomicLong中;
  • 最后要重点关注Cleaner.create:这是jdk.internal.ref.Cleaner的入口,在DirectByteBuffer的构造函数中,将创建的DirectByteBuffer实例和自定义资源释放逻辑Deallocator注册到Cleaner的双向链表中,其中Unsafe.allocateMemory返回分配的native memory的基址指针base就需要保存到Deallocator中,因为释放native memory需要这些元数据,也因为不能让base随着DirectByteBuffer实例的消亡而消失;

整个流程很简单:

  • 在DirectByteBuffer的构造函数中将DirectByteBuffer实例和自定义的释放native memory的Deallocator封装成Cleaner,挂到Cleaner的双向链表中,其中DirectByteBuffer实例作为referent,Deallocator作为thunk,thunk封装了自定义资源释逻辑,所以需要保存释放native memory的元数据,例如基址指针base;
  • 等到DirectByteBuffer实例变成不可达被GC回收的时候,Cleaner会被挂到pending-reference list上;
  • 然后ReferenceHandler线程从pending-reference list上摘下节点,发现是一个Cleaner实例,则调用其clean方法;
  • 在clean方法中,通过remove将Cleaner从双向链表上摘下,然后转调自定义的资源释放代码thunk.run,即Deallocator.run
  • Deallocator需要封装释放native memory的元数据base,即申请的native memory的基址地址;
  • 这样自定义释放资源的逻辑借助于Java引用机制,得以调用完成,不需要调用方关注资源的释放;

自定义释放资源的Deallocator的代码也很简单,重点关注run方法:address是释放native memory所需的元数据基址地址,size和capacity是JVM内部用于记录native memory使用情况的数值,最终会受到-XX:MaxDirectMemorySize的配置影响:

    private static class Deallocator
        implements Runnable
    {

        private long address;
        private long size;
        private int capacity;

        private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }

        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            UNSAFE.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

    }

4.为什么不让你使用Cleaner

当你看到这里迫不及待的想要写个demo调用jdk.internal.ref.Cleaner来感受一下的时候,你发现JDK不让你调用Cleaner。就像Unsafe一样,因为JDK的安全机制,你不能直接调用。

原因有二:

  • jdk.internal.ref.Cleaner的适用场景:自定义释放资源的thunk.clean方法应该简单而直接,否则会导致ReferenceHandler线程延迟甚至阻塞,从而影响Java引用机制的正常运行。所以,jdk.internal.ref.Cleaner只给JDK内部使用;
  • 允许用户程序直接影响Java引用核心流程的正常运行,这样设计不允许也不应该出现;

因此我建议别想着像Unsafe那样通过反射调用,而是应该仿照jdk.internal.ref.Cleaner的实现,在自己的应用中写一个Cleaner出来释放应用资源。


总结

  • 理解虚引用PhantomReference的适用场景
  • 理解Java引用机制的“优雅”和“提前规避”的含义
  • 参看jdk.internal.ref.Cleaner的源码辅助验证Java引用机制的“优雅”和“提前规避”的实现
  • 参看java.nio.DirectByteBuffer的构造函数和静态内部类Deallocator中是如何使用Cleaner来释放native memory
    • 理解资源释放逻辑Runnable thunk和引用jdk.internal.ref.Cleaner分离(非强绑定)带来通用的优点
  • 理解虚引用的流程
  • 理解Cleaner中双线链表的存在:看似冗余,实则必须
  • 理解为什么不让你使用Cleaner
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【java.lang.ref】PhantomReference & jdk.internal.ref.Cleaner 的相关文章

随机推荐

  • C++内存泄露检测器(库注入方法)

    C 内存泄露检测器 库注入方法 2012 06 18 15 55 04 分类 C C codeproject上的一篇文章 翻译过来共享 C Memory Leak Finder C 内存泄露检测器 leakfinder zip 作者 Fre
  • GPT专业应用:早晚安问候语生成

    正文共 725 字 阅读大约需要 3 分钟 社群运营必备技巧 您将在3分钟后获得以下超能力 自动生成早晚安问候语 Beezy评级 B级 经过简单的寻找 大部分人能立刻掌握 主要节省时间 推荐人 nanako 编辑者 Linda 此图片由Le
  • 【Java 微服务架构 Dubbo篇】-1-Zookeeper

    课程回顾 微服务架构需要解决的问题 分布式协调框架Zookeeper 什么是分布式协调技术 分布式协调技术主要用来解决分布式环境当中多个进程之间的同步控制 让他们有序的去访问某种临界资源 防止造成 脏数据 的后果 在这图中有三台机器 每台机
  • LSTM神经网络详解

    LSTM 长短时记忆网络 Long Short Term Memory Network LSTM 是一种改进之后的循环神经网络 可以解决RNN无法处理长距离的依赖的问题 目前比较流行 长短时记忆网络的思路 原始 RNN 的隐藏层只有一个状态
  • Java学习笔记(02_1Java语言基础)

    知识点总结于毕向东Java基础视频教程25天版本 侵权请联系删除 第二章 Java语言基础 Java语言基础组成 关键字 标识符 注释 常量与变量 常量 变量 类型转换 运算符 算术运算符 赋值运算符 比较运算符 逻辑运算符 位运算符 三元
  • ajax后台返回的数据为空前台显示出现undefined的解决方法

    之前自己做的一个图书管理系统 显示图书借阅排行榜 因为翻译在数据库中有为空的字段 故前台显示会显示undefined 以下贴上部门代码 document ready function rankTable tbody html var id
  • chromium源码的下载与编译

    这篇文章主要记录在chromium源码下载以及编译过程中遇到的问题 一直都对chromium的源码感兴趣 在没有封闭外网之前 下载了一个版本 很老了 重新进行更新又不得行 再加上公司的产品线路需要了解chromium的相关知识 又加上疫情封
  • Ctfshow web入门 PHP特性篇 web89-web151 全

    web入门 PHP特性篇的wp都一把梭哈在这里啦 有点多 师傅们可以收藏下来慢慢看 写的应该挺全面的叭 有错误敬请斧正 CTFshow PHP web89 看题目 有个flag php文件 题目要求get有个num 是数字但是不包含0 9
  • 吴恩达深度学习笔记-单层神经网络(第2课)

    深度学习笔记 1 神经网络概览 2 神经网络表示 3 计算神经网络的输出 4 多个样本的向量化 5 向量化实现的解释 6 激活函数 7 为什么需要非线性激活函数 8 激活函数的导数 9 神经网络的梯度下降法 10 直观理解反向传播 11 随
  • SQL-万能密码

    打开目标站点 随便输入用户名密码 查看返回结果 提示错误 后台管理http 10 225 91 25 知识点 这题需要用到万能密码 首先了解一下万能密码 一般的 库验证登录注册查询数据会用到以下的句型 如果用户名与密码匹配正确则返回真值通过
  • Webpack配置

    Webpack 文章目录 Webpack 一 Webpack的基本功能 二 Webpack的核心概念 三 Webpack常用的Loader 四 Webpack常见的Plugins 五 Loader和Plugin的区别 以及如何自定义Load
  • Ehcache是现在最流行的纯Java开源缓存框架

    Ehcache是现在最流行的纯Java开源缓存框架 配置简单 结构清晰 功能强大 最初知道它 是从Hibernate的缓存开始的 网上中文的EhCache材料以简单介绍和配置方法居多 如果你有这方面的问题 请自行google 对于API 官
  • java枚举和数值的相互转换

    枚举简介 enum 的全称为 enumeration 是 JDK 1 5 中引入的新特性 存放在 java lang 包中 在实际编程中 往往存在着这样的 数据集 它们的数值在程序中是稳定的 而且 数据集 中的元素是有限的 此时枚举可以很方
  • BH1750 光照传感器文档详解 及 驱动设计

    前言 最近接触到一个应用 需要在低功耗的产品上加上光照度采集 正好最近有接触到一款光照传感器 BH1750 性能价格都合适 那么今天就抽空来好好测试一下 那么要写一篇测试文章 我会尽量以新手的角度从资料的获取 资料的阅读理解 以及根据资料进
  • Linux网络编程(7)本地套接字通信

    TCP本地套接字通信 为了实现没有血缘关系的进程之间通信 通常会采用本地套接字进行通信 在两个进程分别绑定好了套接字文件 sock 运行程序后将产生两个套接字文件 这两个文件共享同一片内核缓冲区 内核将完成两个进程之间的数据传输 在不同通信
  • 递推方程求解方法

    总结一下递推方程的求解方法 主要介绍六种方法 迭代法 差消法 递归树 主定理 特征根法 母函数法 欢迎大家批评指正 1 迭代法 不断用递推方程的右部替换左部 下面以汉诺塔为例进行求解 有时候直接迭代可能不太方便 可以使用换元迭代 下面以二分
  • Nginx 安全配置

    Nginx 是一个高性能的 HTTP 和反向代理服务 使用非常广泛 目前很大一部分网站均使用了 Nginx 作为 WEB 服务器 Nginx 虽然非常强大 但是安全防护的配置及恶意访问默认是没用做基础配置的 一 nginx 版本信息隐藏 s
  • STM32学习记录——使用蓝牙点亮LED

    文章目录 前言 一 学习目的 二 模块介绍 三 代码记录 四 实际操作 前言 今天记录一个蓝牙模块的简单应用 有关蓝牙的AT指令模式的设置在前面的记录中已经详细记录过 如果忘记了可以看看下面的文章 HC 05蓝牙模块的使用 KAIs32的博
  • 所有的raft算法

    https raft github io
  • 【java.lang.ref】PhantomReference & jdk.internal.ref.Cleaner

    目录 零 前情概要 ref包内容 系列目录 上一章回顾 一 PhantomReference 适用场景 优雅和提前规避 Java中使用native memory 常规做法 借助于Java引用机制 处理流程 二 jdk internal re