深入理解零拷贝

前言

大家好,我是努力更文的小白。今天我们一起来深入理解零拷贝。在本文开始前呢,先问问大家几个问题哈~

什么是DMA呢?什么是用户态与内核态?什么是缓冲区读写?什么是虚拟内存?mmap/sendfile 有什么区别?如果你对这些问题还是理解的不够深入的话,来跟我一起学习吧。

image.png

DMA

我们知道CPU中是有高速寄存器的,假设我们现在有这样的一个需求,需要将100kb的数据发送到网络上的另一端,如果没有DMA的话,首先CPU从内存中读取到这用户空间的100kb的数据,然后将这部分数据发送到网卡中,网卡再将这部分数据通过网络发送给对端。可以发现此时CPU的读写速度会被拉低到跟外设网卡一样的读写速度。

什么是DMA

顾名思义:DMA,即绕开CPU进行数据读写。在计算机中,相比CPU来说,外部设备访问速度是非常缓慢的,因为,memeory到memory或者memory到device或者device到memory之间数据搬运是非常浪费CPU时间的,造成CPU无法及时处理实时事件...怎么办?因此工程师设计出来一种专门协助CPU搬运时间的硬件DMA控制器,协助CPU完成数据搬运。

image.png

如上图所示,DMA是主板上的一块硬件设备,是给CPU打下手的。首先还是上面的例子,CPU先将用户空间的100kb数据拷贝到socket缓冲区(内核上的空间)中,这里发生了一次数据拷贝(CPU复制),接着CPU就不管后续的操作了,而是交给DMA控制器来处理后续操作了,DMA控制器接着将socket缓冲区中的数据读取到自身的缓冲区中,接着将这部分数据写到网卡中。写完之后DMA控制器会向CPU发起一个80中断(软中断)告诉CPU此时socket缓冲区中的数据已经发送完毕了,此时socket缓冲区有空间了,方便唤醒之前因为socket缓冲区中没有空间写而阻塞的进程。

用户态与内核态

  • 如果进程运行于内核空间,被称为进程的内核态
  • 如果进程运行于用户空间,被称为进程的用户态。

缓冲区读写

image.png

  • 如果用户进程需要获取文件缓冲区、套接字缓冲区或者其他设备缓冲区中的数据,都得依靠系统调用,会从用户态切换到内核态,会执行对应的内核程序,内核程序会检查文件缓冲区、套接字缓冲区或者其他设备缓冲区中是否有数据,如果有的话直接返回,将内核中的数据拷贝到用户空间中。但是如果没有的话,会将当前进程挂起等待,此时CPU就会把加载数据到文件缓冲区、套接字缓冲区或者其他设备缓冲区中的操作交给DMA控制器DMA控制器会异步的将数据加载到内核中的文件缓冲区、套接字缓冲区或者其他设备缓冲区中(异步是相对于CPU来说的,即CPU把加载数据交给DMA控制器后可以去做别的事情了),DMA控制器加载完数据到内核中的文件缓冲区、套接字缓冲区或者其他设备缓冲区之后,就会给CPU发起一个中断,接着会将当前之前挂起的进程从等待队列中唤醒进入运行队列中,然后将内核空间中的缓冲区数据拷贝到用户空间当中,进程就会从内核态转为用户态了。
  • 如果用户进程需要写数据到文件缓冲区、套接字缓冲区或者其他设备缓冲区中,也得依靠系统调用,会从用户态切换到内核态,会执行对应的内核程序,内核程序会检查文件缓冲区、套接字缓冲区或者其他设备缓冲区中空间是否已经满了,如果已经满了,当前进程同样会挂起等待。当DMA控制器异步地把缓冲区中地数据发送到网卡或者硬盘或者其他外部设备,发送完成之后,缓冲区中有空闲空间了,此时DMA控制器同样会向CPU发起一个中断,CPU接着去执行中断处理程序,会将缓冲区相关地等待队列中的进程唤醒,加入运行队列中,此进程就可以往缓冲区中写数据了,写完返回结束。

虚拟内存

image.png 如上图所示,物理内存可以理解成又长又大的字节数组。

image.png 如上图所示,随着计算机技术的发展,可以在计算机上面运行一个用户程序了,此时单用户系统程序占用着0~1024kb的物理内存,也就是内核空间的物理地址从0~1024kb,所以这个用户程序空间得保证不访问物理地址从0~1024kb即可。

image.png

如上图所示,虚拟内存最终都会映射到对应的物理内存的,通过MMU单元通过操作系统内核中的虚拟内存映射表快速根据虚拟内存查询到真实的物理内存地址返回给CPU进行后续数据处理操作。

虚拟内存空间是可以大于真实物理内存空间的。假设物理内存为1G,虚拟内存为1.5G,这个怎么实现的呢?操作系统会使用LRU算法将已经占用的访问不频繁的内存放到磁盘中的swap交换区当中。并且不同的虚拟内存地址可以映射到同一物理内存地址上。

image.png

如上图所示,可以将用户空间与内核空间映射到同一块物理内存上,就可以减少一次CPU数据拷贝了(用户空间与内核空间的数据拷贝),这也是mmap函数的实现原理。

传统IO

咱们模拟的是传统IO从磁盘中读取数据发送到网络对端的场景,来看看传统IO的处理流程,如下图所示

image.png

  • 首先用户进程发起read系统调用,接着从用户态切换到内核态,如果此时内核缓冲区中没有数据,则当前进程会进入等待队列阻塞,接着CPU会通知DMA控制器把磁盘中的数据复制到内核缓冲区中。
  • DMA控制器复制完成之后会向CPU发起一个系统中断(80中断,即软中断),CPU会执行中断处理程序,将之前阻塞的进程唤醒,从移除等待队列进入运行队列。接着CPU会将内核缓冲区中的复制到用户空间,然后从内核态切换回用户态。
  • 接着用户进程发起write系统调用,接着从用户态切换到内核态,然后CPU会将用户空间中的数据复制到socket缓冲区,复制完成后会通知DMA控制器。之后进程从内核态切换回用户态。
  • 最后DMA控制器异步将socket缓冲区中数据复制到网卡,最后网卡将数据发送到网络对端。

从以上可以看出一共进行了4次上下文切换(4次用户态和内核态的切换),4次数据拷贝(两次CPU拷贝以及两次的DMA拷贝)

mmap + write实现零拷贝

mmap函数定义如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
复制代码
  • addr:指定映射的虚拟内存地址
  • length:映射的长度
  • prot:映射内存的保护模式
  • flags:指定映射的类型
  • fd:进行映射的文件句柄
  • offset:文件偏移量

如下图所示,mmap使用我们之前讲过的虚拟内存,将用户空间缓冲区和内核缓冲区都映射到了同一块物理内存。这样明显比传统IO少了一次CPU复制(从内核缓冲区复制到用户空间缓冲区) image.png

  • 首先用户进程通过mmap方法发起系统调用读取内核缓冲区中的数据,从用户态切换到内核态。
  • CPU通知DMA控制器将数据从磁盘复制到内核缓冲区mmap方法返回。上下文从内核态切换回用户态。
  • 用户进程发起系统调用往socket缓冲区中写数据,上下文从用户态切换到内核态,接着CPU将用户数据缓冲区,即内核缓冲区因为用户空间缓冲区和内核缓冲区都映射到了同一块物理内存,共享了这部分空间)拷贝到socket缓冲区
  • 系统调用返回,上下文从内核态切换回用户态,接着DMA控制器异步将socket缓冲区中的数据拷贝到网卡,网卡最终将这部分数据通过网络协议发送到网络对端。

从以上可以看出,一共进行了4次上下文切换和3次复制(2次DMA复制和1次CPU复制)。比传统IO少了一次CPU复制,因为mmap将用户数据缓冲区和内核缓冲区都映射到了同一块物理内存。

sendfile实现零拷贝

mmap函数定义如下:

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
复制代码
  • out_fd:为待写入内容的文件描述符,一个socket描述符。

  • in_fd:为待读出内容的文件描述符,必须是真实的文件,不能是socket和管道。

  • offset:指定从读入文件的哪个位置开始读,如果为NULL,表示文件的默认起始位置。

  • count:指定在fdout和fdin之间传输的字节数。

sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。

image.png

  • 首先用户进程通过sendfile函数发起一个系统调用,上下文从用户态切换到内核态,接着CPU通知DMA控制器进行数据处理。
  • 接着DMA控制器将磁盘中的数据复制到内核缓冲区,复制完成之后,通知CPU(80中断)。
  • 然后CPU内核缓冲区中的数据复制到socket缓冲区中,然后通知DMA控制器进行数据处理。sendfile函数返回,上下文从内核态切换回用户态。
  • DMA控制器异步将socket缓冲区中的数据复制到网卡,网卡最终将这部分数据通过网络协议发送到网络对端。

从以上可以看出,一共进行了2次上下文切换和3次复制(2次DMA复制和1次CPU复制)。

BIO网络通信分析

BIO网络通信发送数据原理如下图所示

image.png

  • 首先进程首先向内核发出write系统调用,接着上下文从用户态切换到内核态。
  • 接着CPU会将用户空间想要发送的数据拷贝到内核空间的Socket输出缓冲区中。
  • 拷贝完成后,CPU会通知DMA控制器将这部分数据发送到网络对端,write系统调用返回,上下文由内核态切换回用户态。
  • 然后DMA控制器异步这数据拷贝到网卡,网卡再通过网络协议将数据发送到网络对端。

如果用户进程想要发送数据时,此时的Socket输出缓冲区没有空闲空间了,这时进程会被挂起等待,进入与该Socket输出缓冲区相关的进程等待队列中,直到DMA控制器Socket输出缓冲区中的数据拷贝到网卡后,此时Socket输出缓冲区有空闲空间了,DMA控制器会向CPU发起一个中断,CPU会执行中断处理程序,将之前等待的线程唤醒,重新写入数据,最后返回。

NIO网络通信分析

NIO是非阻塞面向块传输的,并且NIO提出了服务端与客户端通过通道channal来进行数据传输,其实通道channal的底层还是Socket输入输出缓冲区。

什么时面向块呢?就是简单理解成write(buffer)buffer就是相当于一块数据(字节数组,连续的),进行数据拷贝的时候只需要告诉buffer的起始地址跟长度即可。

这样的发送方式会有什么问题吗?当发生GC垃圾回收的时候,会整理内存碎片,即之前的buffer的起始地址可能会改变,所以NIO肯定是需要堆外内存的,在对堆外内存拷贝一份跟堆里面一样的buffer,再将堆外的这份buffer拷贝给内核,防止因为GC进行碎片清理,改变了buffer的起始地址而导致拷贝数据不准确的问题。

简单看一下NIO写数据到内核socket输出缓冲区的源码,入口:java.nio.channels.SocketChannel#write(java.nio.ByteBuffer)

public abstract int write(ByteBuffer src) throws IOException;
复制代码

由上可知在SocketChannel类的write方法是一个抽象方法,具体实现看子类SocketChannelImpl,如下:

public int write(ByteBuffer var1) throws IOException {
    if (var1 == null) {
        throw new NullPointerException();
    } else {
        Object var2 = this.writeLock;
        synchronized(this.writeLock) {
            this.ensureWriteOpen();
            int var3 = 0;
            boolean var20 = false;
            byte var5;
            label310: {
                int var27;
                try {
                    var20 = true;
                    this.begin();
                    Object var4 = this.stateLock;
                    synchronized(this.stateLock) {
                        if (!this.isOpen()) {
                            var5 = 0;
                            var20 = false;
                            break label310;
                        }
                        this.writerThread = NativeThread.current();
                    }
                    do {
                        //这行代码实现将Buffer中的块数据拷贝到内核socket输出缓冲区
                        var3 = IOUtil.write(this.fd, var1, -1L, nd);
                    } while(var3 == -3 && this.isOpen());
                    var27 = IOStatus.normalize(var3);
                    var20 = false;
                } finally {
                    if (var20) {
                        this.writerCleanup();
                        this.end(var3 > 0 || var3 == -2);
                        Object var11 = this.stateLock;
                        synchronized(this.stateLock) {
                            if (var3 <= 0 && !this.isOutputOpen) {
                                throw new AsynchronousCloseException();
                            }
                        }
                        assert IOStatus.check(var3);
                    }
                }
                this.writerCleanup();
                this.end(var3 > 0 || var3 == -2);
                Object var28 = this.stateLock;
                synchronized(this.stateLock) {
                    if (var3 <= 0 && !this.isOutputOpen) {
                        throw new AsynchronousCloseException();
                    }
                }
                assert IOStatus.check(var3);
                return var27;
            }
            this.writerCleanup();
            this.end(var3 > 0 || var3 == -2);
            Object var6 = this.stateLock;
            synchronized(this.stateLock) {
                if (var3 <= 0 && !this.isOutputOpen) {
                    throw new AsynchronousCloseException();
                }
            }
            assert IOStatus.check(var3);
            return var5;
        }
    }
}
复制代码

继续查看var3 = IOUtil.write(this.fd, var1, -1L, nd);这行代码,如下:

static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
    //如果当前buffer是堆外内存的话,直接执行拷贝工作,即拷贝到内核socket输出缓冲区
    if (var1 instanceof DirectBuffer) {
        return writeFromNativeBuffer(var0, var1, var2, var4);
    } else {
        //如果当前buffer是堆内内存的话
        //获取堆内buffer的首地址
        int var5 = var1.position();
        //获取堆内buffer的尾地址
        int var6 = var1.limit();
        assert var5 <= var6;
        //计算出堆内内存的大小
        int var7 = var5 <= var6 ? var6 - var5 : 0;
        //申请堆外内存,大小跟堆内的Buffer一样
        ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);

        int var10;
        try {
            //将堆内buffer的数据拷贝到堆外申请的内存中
            var8.put(var1);
            var8.flip();
            var1.position(var5);
            //将堆外的Buffer拷贝到内核socket输出缓冲区
            int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
            if (var9 > 0) {
                var1.position(var5 + var9);
            }
            var10 = var9;
        } finally {
            Util.offerFirstTemporaryDirectBuffer(var8);
        }
        return var10;
    }
}
复制代码

上述代码中,执行流程如下:

  • 首先判断当前buffer是否是堆外内存,如果是的话,直接将堆外buffer拷贝到内核socket输出缓冲区。
  • 如果判断当前buffer不是堆外内存,则计算出堆内buffer的大小,调用getTemporaryDirectBuffer申请堆外内存,内存大小跟堆内的buffer一样,并且将堆内buffer中的数据拷贝到堆外内存中,最终将堆外内存中的buffer数据拷贝到内核socket输出缓冲区。

NIO的源码也可以其缺点:实际上是多了一次拷贝工作的(从堆内buffer拷贝到堆外内存中的buffer

java堆外内存

堆外内存不受JVM管理,怎么释放堆外内存?(面试重点)

堆外内存对应的java中是DirectByteBuffer类,首先查看DirectByteBuffer类的构造方法,如下:

DirectByteBuffer(int cap) {                   // package-private

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        //调用unsafe类申请size大小的堆外内存,并返回申请的堆外内存的虚拟内存地址,方便后面操作这部分堆外内存
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        //将申请的堆外内存地址赋值给address,方便后面操作这部分堆外内存
        address = base;
    }
    //创建Cleaner类对象用于释放堆外内存的
    //Deallocator类中的run方法用于真正释放堆外内存的,
    //run方法中调用unsafe.freeMemory(address);这行代码释放堆外内存
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}
复制代码

上述代码执行流程如下:

  • 首先调用unsafe类的allocateMemory方法申请指定大小的堆外内存,并返回申请的这部分堆外内存的虚拟内存地址给base变量,方便后面操作这部分堆外内存。
  • 接着将base变量的值赋值给address,方便后面操作这部分堆外内存。
  • 创建Cleaner类对象,用于后面释放申请的这部分堆外内存,真正释放堆外内存的代码是在Deallocator类的run方法中。

接着查看Deallocator类,该类实现了Runnable,该类如下:

private static class Deallocator implements Runnable {

    private static Unsafe unsafe = Unsafe.getUnsafe();

    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);这行代码释放堆外内存
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }
}    
复制代码

当启动该类时会执行它的run方法,run方法中unsafe.freeMemory(address);这行代码使用unsafe类将堆外指定地址的内存释放掉。

接着查看Cleaner类,继承了虚引用,结构如下:

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

由上可知,Cleaner类继承了PhantomReference虚引用,虚引用在创建时必须指定ReferenceQueue,方便虚引用指向的对象被GC垃圾回收之后,将当前Reference加入到ReferenceQueue中。

回到上面的cleaner = Cleaner.create(this, new Deallocator(base, size, cap));中,查看Cleanercreate方法,如下:

public static Cleaner create(Object var0, Runnable var1) {
    return var1 == null ? null : add(new Cleaner(var0, var1));
}

private Cleaner(Object var1, Runnable var2) {
    super(var1, dummyQueue);
    this.thunk = var2;
}

public void clean() {
    if (remove(this)) {
        try {
            //thunk为上面传进来的Deallocator类实例
            //调用上面的Deallocator的run方法释放堆外内存
            this.thunk.run();
        } catch (final Throwable var2) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (System.err != null) {
                        (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                    }

                    System.exit(1);
                    return null;
                }
            });
        }

    }
}
复制代码

总体流程:jvm启动时,会启动消费线程消费pending队列(看完java引用队列再来看这个),由于cleaner继承了PhantomReference虚引用,我们直到创建PhantomReference虚引用必须指定ReferenceQueue队列,接着消费线程会将pending队列中的cleaner对象出队列,接着执行cleaner对象中的clean方法,clean方法最终会调用Deallocator堆外内存释放器run方法,最终通过使用unsafe类释放堆外内存。

java引用队列

关于强引用、软引用、弱引用和虚引用的知识点可以参考聊聊ThreadLocal文章,里面说得很详细,也有对应的代码demo

Reference四种状态

Reference四种状态如下:

  • Active:激活状态。
  • Pending:等待入ReferenceQueue队列
  • Enqueued:已入ReferenceQueue队列
  • Inactive:失效状态

Reference对象刚创建出来的时候就是激活状态,此时Reference对象指向的object还存在着强引用。当object还没有强引用指向时,此时进行GC的话,GC线程会将与object关联的Reference对象加入到Pending队列中,变成等待入ReferenceQueue队列状态,接着还会起一个消费线程(守护线程)去判断Pending队列中的Reference对象在创建调用构造方法的时候有没有指定ReferenceQueue,将指定ReferenceQueueReference对象移动到ReferenceQueue队列,变成已入ReferenceQueue队列状态,而对于那些没有指定ReferenceQueueReference对象将会变成Inactive状态。对于ReferenceQueue队列中的Reference对象,如果从ReferenceQueue队列出队的话,即调用ReferenceQueuepoll方法出队列的话,也会变成Inactive状态。

源码分析

先查看Reference类属性如下:

//保存真实对象的引用
private T referent;         /* Treated specially by GC */

//引用队列,外部可以通过传递引用队列,方便后续判定指定对象是否被gc回收掉
volatile ReferenceQueue<? super T> queue;

/* When active:   NULL
 *     pending:   this
 *    Enqueued:   next reference in queue (or this if last)
 *    Inactive:   this
 */
 //ReferenceQueue是一个单向链表,每个元素都有一个Next指针指向下一个元素
@SuppressWarnings("rawtypes")
volatile Reference next;

/* When active:   next element in a discovered reference list maintained by GC (or this if last)
 *     pending:   next element in the pending list (or null if last)
 *   otherwise:   NULL
 */
//vm线程在判定当前Reference中的真实对象是垃圾后,会将当前Reference加入到pending队列中。
//pending队列是一个单向链表,使用discovered字段连接起来
transient private Reference<T> discovered;  /* used by VM */


/* Object used to synchronize with the garbage collector.  The collector
 * must acquire this lock at the beginning of each collection cycle.  It is
 * therefore critical that any code holding this lock complete as quickly
 * as possible, allocate no new objects, and avoid calling user code.
 */
static private class Lock { }
private static Lock lock = new Lock();


/* List of References waiting to be enqueued.  The collector adds
 * References to this list, while the Reference-handler thread removes
 * them.  This list is protected by the above lock object. The
 * list uses the discovered field to link its elements.
 */
//pending队列中pending属性指向第一个元素,相当于head
//pending队列中的元素通过上面的discovered属性组成单向链表(相当于next指针)
//这是gc线程完成的
private static Reference<Object> pending = null;
复制代码

重要属性信息如下:

  • referent:保存真实对象的引用
  • queue:引用队列,外部可以通过传递引用队列,方便后续判定指定对象是否被gc回收掉
  • nextReferenceQueue是一个单向链表,每个元素都有一个Next指针指向下一个元素
  • discoveredvm线程在判定当前Reference中的真实对象是垃圾后,会将当前Reference加入到pending队列中,pending队列是一个单向链表,使用discovered字段连接起来
  • pendingpending队列是一个先进后出(栈)的队列,消费线程会从头开始消费,pending属性指向第一个元素,相当于headpending队列中的元素通过上面的discovered属性组成单向链表(相当于next指针),这是vm线程组装完成的

接着看ReferenceHandler,就是我们之前说的那个消费线程(守护线程),用于消费pending队列中的元素,将pending队列中指定的元素加入到ReferenceQueue中,该类结构如下:

/* High-priority thread to enqueue pending References
 */
private static class ReferenceHandler extends Thread {

    private static void ensureClassInitialized(Class<?> clazz) {
        try {
            Class.forName(clazz.getName(), true, clazz.getClassLoader());
        } catch (ClassNotFoundException e) {
            throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
        }
    }

    static {
        // pre-load and initialize InterruptedException and Cleaner classes
        // so that we don't get into trouble later in the run loop if there's
        // memory shortage while loading/initializing them lazily.
        ensureClassInitialized(InterruptedException.class);
        ensureClassInitialized(Cleaner.class);
    }

    ReferenceHandler(ThreadGroup g, String name) {
        super(g, name);
    }

    public void run() {
        while (true) {
            tryHandlePending(true);
        }
    }
}
复制代码

该类用于处理上面的pending队列中的元素,继承了Thread,重点在于它的run方法。接着查看静态代码块中关于ReferenceHandler类的代码如下:

static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    /* If there were a special system-only priority greater than
     * MAX_PRIORITY, it would be used here
     */
    //设置该消费线程的优先级为最高 
    handler.setPriority(Thread.MAX_PRIORITY);
    //设置该消费线程为守护线程
    handler.setDaemon(true);
    //启动守护线程
    handler.start();

    // provide access in SharedSecrets
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}
复制代码

咱们回到上面的ReferenceHandler类的run方法中,查看tryHandlePending方法,如下:

/**
 * 处理pendiing队列中的Reference元素,将在创建Reference时指定ReferenceQueue的加入到ReferenceQueue
 */
static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    //重要
    Cleaner c;
    try {
        //这列为什么需要同步?
        //1.jvm垃圾收集器线程需要向pending队列中追加Reference元素
        //2.当前消费线程消费这个pending队列。
        synchronized (lock) {
            //如果当前pending队列不为空的话
            if (pending != null) {
                r = pending;
                // 'instanceof' might throw OutOfMemoryError sometimes
                // so do this before un-linking 'r' from the 'pending' chain...
                //c一般情况下是null,当r指向的Reference实例是cleaner实例时,c才会不为null,并且指向cleaner对象
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // unlink 'r' from 'pending' chain
                //下面两行代码做出队逻辑(pending队列)
                pending = r.discovered;
                r.discovered = null;
            } else {
                //如果pending队列为空的话
                // The waiting on the lock may cause an OutOfMemoryError
                // because it may try to allocate exception objects.
                if (waitForNotify) {
                    //当前消费线程释放锁,阻塞等待,
                    //直到其他线程使用当前的lock.notify()或者lock.notifyAll()唤醒
                    //是谁唤醒当前消费线程呢?是jvm垃圾收集器线程,
                    //vm线程向pending队列添加Reference元素之后,会调用lock.notify()
                    lock.wait();
                }
                // retry if waited
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        // Give other threads CPU time so they hopefully drop some live references
        // and GC reclaims some space.
        // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
        // persistently throws OOME for some time...
        Thread.yield();
        // retry
        return true;
    } catch (InterruptedException x) {
        // retry
        return true;
    }

    // Fast path for cleaners
    //条件成立的话,说明当前的这个Reference其实是一个cleaner类型实例
    if (c != null) {
        //如果当前Reference是一个cleaner类型,就不会执行将它从pending队列转移到ReferenceQueue了。
        //直接执行cleaner.clean()方法了
        c.clean();
        return true;
    }
    
    //大部分情况下都会执行到下面,c==null
    //获取到创建Reference时指定的ReferenceQueue
    ReferenceQueue<? super Object> q = r.queue;
    //条件成立,说明创建Reference时指定过ReferenceQueue
    //q.enqueue(r):将Reference加入到ReferenceQueue队列中
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}
复制代码

上述代码中主要的逻辑如下:

  • 消费线程会pending队列中的元素出队,判断当前出队的Reference是否是cleaner实例,如果不是的话,就会将该Reference转移到创建Reference时指定的ReferenceQueue中。
  • 如果当前出队列的Referencecleaner实例,则会执行cleanerclean方法。

おすすめ

転載: juejin.im/post/7048834922596794399