深入理解零拷贝
前言
大家好,我是努力更文的小白。今天我们一起来深入理解零拷贝。在本文开始前呢,先问问大家几个问题哈~
什么是DMA
呢?什么是用户态与内核态?什么是缓冲区读写?什么是虚拟内存?mmap/sendfile 有什么区别?如果你对这些问题还是理解的不够深入的话,来跟我一起学习吧。
DMA
我们知道CPU
中是有高速寄存器的,假设我们现在有这样的一个需求,需要将100kb的数据发送到网络上的另一端,如果没有DMA
的话,首先CPU
从内存中读取到这用户空间的100kb的数据,然后将这部分数据发送到网卡中,网卡再将这部分数据通过网络发送给对端。可以发现此时CPU
的读写速度会被拉低到跟外设网卡一样的读写速度。
什么是DMA
?
顾名思义:DMA
,即绕开CPU
进行数据读写。在计算机中,相比CPU
来说,外部设备访问速度是非常缓慢的,因为,memeory到memory
或者memory到device
或者device到memory
之间数据搬运是非常浪费CPU
时间的,造成CPU
无法及时处理实时事件...怎么办?因此工程师设计出来一种专门协助CPU
搬运时间的硬件DMA控制器
,协助CPU
完成数据搬运。
如上图所示,DMA
是主板上的一块硬件设备,是给CPU
打下手的。首先还是上面的例子,CPU
先将用户空间的100kb数据拷贝到socket缓冲区(内核上的空间)
中,这里发生了一次数据拷贝(CPU
复制),接着CPU
就不管后续的操作了,而是交给DMA控制器
来处理后续操作了,DMA控制器
接着将socket缓冲区
中的数据读取到自身的缓冲区中,接着将这部分数据写到网卡
中。写完之后DMA控制器
会向CPU
发起一个80中断(软中断)
告诉CPU
此时socket缓冲区
中的数据已经发送完毕了,此时socket缓冲区
有空间了,方便唤醒之前因为socket缓冲区
中没有空间写而阻塞的进程。
用户态与内核态
- 如果进程运行于内核空间,被称为进程的内核态
- 如果进程运行于用户空间,被称为进程的用户态。
缓冲区读写
- 如果用户进程需要获取文件缓冲区、套接字缓冲区或者其他设备缓冲区中的数据,都得依靠系统调用,会从用户态切换到内核态,会执行对应的内核程序,内核程序会检查文件缓冲区、套接字缓冲区或者其他设备缓冲区中是否有数据,如果有的话直接返回,将内核中的数据拷贝到用户空间中。但是如果没有的话,会将当前进程挂起等待,此时
CPU
就会把加载数据到文件缓冲区、套接字缓冲区或者其他设备缓冲区中的操作交给DMA控制器
,DMA控制器
会异步的将数据加载到内核中的文件缓冲区、套接字缓冲区或者其他设备缓冲区中(异步是相对于CPU
来说的,即CPU
把加载数据交给DMA控制器
后可以去做别的事情了),DMA控制器
加载完数据到内核中的文件缓冲区、套接字缓冲区或者其他设备缓冲区之后,就会给CPU
发起一个中断,接着会将当前之前挂起的进程从等待队列中唤醒进入运行队列中,然后将内核空间中的缓冲区数据拷贝到用户空间当中,进程就会从内核态转为用户态了。 - 如果用户进程需要写数据到文件缓冲区、套接字缓冲区或者其他设备缓冲区中,也得依靠系统调用,会从用户态切换到内核态,会执行对应的内核程序,内核程序会检查文件缓冲区、套接字缓冲区或者其他设备缓冲区中空间是否已经满了,如果已经满了,当前进程同样会挂起等待。当
DMA控制器
异步地把缓冲区中地数据发送到网卡或者硬盘或者其他外部设备,发送完成之后,缓冲区中有空闲空间了,此时DMA控制器
同样会向CPU
发起一个中断,CPU
接着去执行中断处理程序,会将缓冲区相关地等待队列中的进程唤醒,加入运行队列中,此进程就可以往缓冲区中写数据了,写完返回结束。
虚拟内存
如上图所示,物理内存可以理解成又长又大的字节数组。
如上图所示,随着计算机技术的发展,可以在计算机上面运行一个用户程序了,此时单用户系统程序占用着
0~1024kb
的物理内存,也就是内核空间的物理地址从0~1024kb
,所以这个用户程序空间得保证不访问物理地址从0~1024kb
即可。
如上图所示,虚拟内存最终都会映射到对应的物理内存的,通过MMU
单元通过操作系统内核中的虚拟内存映射表快速根据虚拟内存查询到真实的物理内存地址返回给CPU
进行后续数据处理操作。
虚拟内存空间是可以大于真实物理内存空间的。假设物理内存为1G
,虚拟内存为1.5G
,这个怎么实现的呢?操作系统会使用LRU算法
将已经占用的访问不频繁的内存放到磁盘中的swap
交换区当中。并且不同的虚拟内存地址可以映射到同一物理内存地址上。
如上图所示,可以将用户空间与内核空间映射到同一块物理内存上,就可以减少一次CPU
数据拷贝了(用户空间与内核空间的数据拷贝),这也是mmap
函数的实现原理。
传统IO
咱们模拟的是传统IO
从磁盘中读取数据发送到网络对端的场景,来看看传统IO
的处理流程,如下图所示
- 首先用户进程发起
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
复制(从内核缓冲区复制到用户空间缓冲区)
- 首先用户进程通过
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
表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。
- 首先用户进程通过
sendfile
函数发起一个系统调用,上下文从用户态切换到内核态,接着CPU
通知DMA控制器
进行数据处理。 - 接着
DMA控制器
将磁盘中的数据复制到内核缓冲区
,复制完成之后,通知CPU
(80中断
)。 - 然后
CPU
将内核缓冲区
中的数据复制到socket缓冲区
中,然后通知DMA控制器
进行数据处理。sendfile
函数返回,上下文从内核态切换回用户态。 DMA控制器
异步将socket缓冲区
中的数据复制到网卡,网卡最终将这部分数据通过网络协议发送到网络对端。 从以上可以看出,一共进行了2次上下文切换和3次复制(2次DMA
复制和1次CPU
复制)。
BIO网络通信分析
BIO
网络通信发送数据原理如下图所示
- 首先进程首先向内核发出
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));
中,查看Cleaner
的create
方法,如下:
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
,将指定ReferenceQueue
的Reference
对象移动到ReferenceQueue
队列,变成已入ReferenceQueue
队列状态,而对于那些没有指定ReferenceQueue
的Reference
对象将会变成Inactive
状态。对于ReferenceQueue
队列中的Reference
对象,如果从ReferenceQueue
队列出队的话,即调用ReferenceQueue
的poll
方法出队列的话,也会变成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回收掉next
:ReferenceQueue
是一个单向链表,每个元素都有一个Next
指针指向下一个元素discovered
:vm
线程在判定当前Reference
中的真实对象是垃圾后,会将当前Reference
加入到pending
队列中,pending
队列是一个单向链表,使用discovered
字段连接起来pending
:pending
队列是一个先进后出(栈)的队列,消费线程会从头开始消费,pending
属性指向第一个元素,相当于head
,pending
队列中的元素通过上面的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
中。 - 如果当前出队列的
Reference
是cleaner
实例,则会执行cleaner
的clean
方法。
转载自:https://juejin.cn/post/7048834922596794399