preface

Hello, everyone. I’m trying to be more literate. Today we’re going to take a closer look at zero copy. Before we begin, let me ask you a few questions

What is DMA? What are user-mode and kernel-mode? What are buffer reads and writes? What is virtual memory? What is the difference between mmap/sendfile? If you don’t know enough about these questions, come and learn with me.

DMA

We know that we have high-speed registers in the CPU, so let’s say we have a requirement to send 100kb of data to the other side of the network. Without DMA, the CPU reads 100kb of user space from memory and then sends that data to the network card. The network adapter sends the data to the peer end over the network. You can see that the CPU read/write speed is lowered to the same speed as the peripheral network card.

What is theDMA?

Just as the name suggests: DMA, or reading and writing data around the CPU. In a computer, the external device access speed is very slow compared to the CPU, because the memeory to memory or memory to device or device to memory data transfer is a waste of CPU time, resulting in the CPU can not process real-time events in a timely manner… How to do? Therefore, engineers designed a dedicated hardware DMA controller to assist CPU handling time to complete data handling.

DMA, as you can see above, is a piece of hardware on the motherboard that works for the CPU. First, the CPU copies 100KB of user space into the socket buffer (kernel space). There is a copy (CPU copy), and then the CPU does not care about the subsequent operations. Instead, the DMA controller handles the subsequent operations. The DMA controller then reads the socket buffer into its own buffer and writes the data to the network card. After the write is complete, the DMA controller issues an 80 interrupt (soft interrupt) to the CPU to tell the CPU that the data in the socket buffer has been sent, and that the socket buffer has space to wake up the process that was blocked because there is no space to write.

User mode and kernel mode

  • If the process is running in kernel space, it is called the kernel state of the process
  • If the process is running in user space, it is called the user state of the process.

Buffer read and write

  • If user processes need access to the file buffer, socket buffer or other equipment data in a buffer, have to rely on a system call, will switch from user mode to kernel mode, performs the corresponding kernel program, the kernel will check the file buffer, socket buffer or other equipment whether there is data in the buffer, if any direct return, Copy data from the kernel into user space. If not, the current process will be suspended and waitCPULoad data into a file buffer, socket buffer, or other device bufferThe DMA controller.The DMA controllerAsynchronously loads data into file buffers, socket buffers, or other device buffers in the kernel (asynchronously as opposed toCPUIn terms of, i.eCPUGive the load data toThe DMA controllerCan move on to other things),The DMA controllerAfter loading data into a file buffer, socket buffer, or other device buffer in the kernel, theCPUAn interrupt is issued, the currently pending process is woken up from the wait queue into the run queue, and the buffer data in kernel space is copied into user space, and the process moves from kernel to user state.
  • If the user process need to write data to the file buffer, the socket buffer buffer or other equipment, also have to rely on a system call, will switch from user mode to kernel mode, executes the corresponding kernel, the kernel will check the file buffer, socket buffer or other equipment if space in the buffer is full, if it is full, The current process also suspends and waits. whenThe DMA controllerAsynchronously send data in the buffer to a network card or hard disk or other external device. After sending data, free space in the buffer is availableThe DMA controllerThe same toCPUInitiate an interrupt,CPUThe interrupt handler is then executed to wake up the process in the buffer waiting queue and join the runqueue. The process can write data to the buffer and return.

Virtual memory

As shown in the figure above, physical memory can be understood as long and large byte arrays.

As shown in the figure above, with the development of computer technology, it is possible to run a user program on the computer, while the single-user system program occupies0~1024kbThe physical memory, that is, the physical address of the kernel space from0~1024kb, so the user program space must be guaranteed not to access physical addresses from0~1024kbCan.

As shown in the preceding figure, the virtual memory is mapped to the corresponding physical memory. The MMU unit uses the virtual memory mapping table in the operating system kernel to quickly query the real physical memory address based on the virtual memory and sends the address back to the CPU for subsequent data processing.

Virtual memory space can be larger than real physical memory space. Assuming 1 GB of physical memory and 1.5 GB of virtual memory, how does this work? The operating system uses the LRU algorithm to place occupied, infrequently accessed memory in the swap area of the disk. And different virtual memory addresses can be mapped to the same physical memory address.

As shown in the figure above, you can map user space and kernel space to the same physical memory, thus saving one COPY of CPU data (user space and kernel space data copy). This is how the MMAP function works.

Traditional IO

Let’s simulate the scenario where traditional IO reads data from disk and sends it to the peer end of the network. Let’s take a look at the processing process of traditional IO, as shown in the figure below

  • First the user process initiatesreadIf there is no data in the kernel buffer at this point, the current process will enter the wait queue and blockCPUWill informThe DMA controllerCopy data from disk to kernel buffer.
  • The DMA controllerWhen the copy is done, theCPUInitiate a system outage (80 Interrupt: soft interrupt),CPUAn interrupt handler is executed to wake up a previously blocked process from the remove wait queue to the run queue. Then,CPUCopies the kernel buffer to user space, and then switches from kernel state back to user state.
  • Then the user process initiateswriteSystem call, and then switch from user to kernel, and thenCPUCopies data in user space toThe socket buffer, will be notified when the replication is completeThe DMA controller. The process then switches from kernel state back to user state.
  • The lastThe DMA controllerAsynchronous willThe socket bufferData is copied to the network adapter, and the network adapter sends the data to the peer end.

As you can see from the above, there were 4 context switches (4 user-kernel switches), 4 data copies (2 CPU copies and 2 DMA copies)

Mmap + write implements zero copy

The mmap function is defined as follows:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
Copy the code
  • addr: Specifies the virtual memory address to be mapped
  • length: Mapping length
  • prot: Indicates the protection mode of mapped memory
  • flags: Specifies the type of the mapping
  • fd: File handle for mapping
  • offset: Indicates the file offset

As shown in the picture below,mmapUse what we talked about beforeVirtual memory, maps both user-space and kernel buffers to the same physical memory.This is obviously better thanTraditional IOTook a littleCPUCopy (from kernel buffer to user-space buffer)

  • First the user process passesmmapMethod to initiate a system call readKernel bufferFrom user mode to kernel mode.
  • CPUnoticeThe DMA controllerCopy data from disk toKernel buffer.mmapMethod returns. The context is switched from kernel state to user state.
  • The user process initiates a system call toThe socket bufferWrite data, context from user mode to kernel mode, and thenCPUBuffers user data, i.eKernel buffer(Because user-space buffers and kernel buffers are mapped to the same physical memory, they share this space) copy to theThe socket buffer.
  • The system call returns, the context switches from kernel state back to user state, and thenThe DMA controllerAsynchronous willThe socket bufferThe data is copied to the network adapter, and the network adapter sends the data to the peer end through the network protocol.

As you can see from the above, there were four context switches and three replicates (twoDMACopy and 1CPUReplication). That’s one less IO than traditional IOCPUCopy, becausemmapMap both the user data buffer and the kernel buffer to the same physical memory.

Sendfile implements zero copy

The mmap function is defined as follows:

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
Copy the code
  • out_fd: is the file descriptor of the content to be written, onesocketDescriptor.

  • in_fd: indicates the file descriptor of the content to be read. It must be a real filesocketAnd piping.

  • Offset: specifies the starting position of the read file. If NULL, it indicates the default starting position of the file.

  • Count: Specifies the number of bytes transferred between FDout and fdin.

Sendfile stands for transferring data between two file descriptors. It operates within the operating system kernel to avoid copying data from the kernel buffer to the user buffer, so it can be used for zero-copy.

  • First the user process passessendfileFunction makes a system call, the context switches from user to kernel, and thenCPUnoticeThe DMA controllerData processing.
  • Then,The DMA controllerCopy data from a disk toKernel bufferWhen the copy is complete, notifyCPU(80 interrupt).
  • thenCPUwillKernel bufferTo copy data toThe socket buffer, and then notifyThe DMA controllerData processing.sendfileThe context is switched from kernel state to user state.
  • The DMA controllerAsynchronous willThe socket bufferThe data is copied to the network adapter, and the network adapter sends the data to the peer end through the network protocol.

As you can see from the above, there are two context switches and three copies (twoDMACopy and 1CPUReplication).

BIO Network communication analysis

The BIO network communication works as shown in the following figure

  • First, the process first issues to the kernelwriteSystem call, and then the context switches from user to kernel mode.
  • Then,CPUWill copy the data that user space wants to send to the kernel spaceSocket output bufferIn the.
  • When the copy is done,CPUWill informThe DMA controllerSend this data to the network peer,writeThe system call returns, and the context switches from kernel state to user state.
  • thenThe DMA controllerAsynchronously, the data is copied to the network adapter, which then sends the data to the network peer over the network protocol.

If the user process wants to send data and there is no free space in the Socket output buffer, the process will be suspended and wait in the queue associated with the Socket output buffer until the DMA controller copies the Socket output buffer to the nic. When the Socket output buffer is free, the DMA controller issues an interrupt to the CPU, which then executes an interrupt handler to wake up the waiting thread, re-write data, and finally return.

NIO network communication analysis

NIO is non-blocking block-oriented, and NIO proposes that the server and client transfer data through channel Channal. In fact, the bottom layer of channel Channal is the Socket input and output buffer.

When do you block? Simply known as write(buffer), a buffer is equivalent to a piece of data (a byte array, continuous). When copying data, you only need to tell the start address and length of buffer.

What’s wrong with sending this way? When a GC garbage collection occurs, the memory fragment is defragmentation, which means that the starting address of the previous buffer may change. Therefore, NIO definitely needs out-of-heap memory. It copies a copy of the out-of-heap buffer to the out-of-heap memory, and then copies the out-of-heap buffer to the kernel to prevent the GC from defragmentation. Incorrect copy of buffer caused by changing the start address of buffer.

Simple look at the NIO to write data to the kernel source socket output buffer, and entry: Java NIO. Channels. SocketChannel# write (Java. NIO. ByteBuffer)

public abstract int write(ByteBuffer src) throws IOException;
Copy the code

SocketChannelImpl: SocketChannelImpl: SocketChannelImpl: 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 {
                        // This line of code copies block data from the Buffer to the kernel socket output Buffer
                        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 newAsynchronousCloseException(); }}assertIOStatus.check(var3); }}this.writerCleanup();
                this.end(var3 > 0 || var3 == -2);
                Object var28 = this.stateLock;
                synchronized(this.stateLock) {
                    if (var3 <= 0&&!this.isOutputOpen) {
                        throw newAsynchronousCloseException(); }}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 newAsynchronousCloseException(); }}assert IOStatus.check(var3);
            returnvar5; }}}Copy the code

Var3 = ioutil. write(this.fd, var1, -1l, nd); This line of code looks like this:

static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
    // If the current buffer is out of the heap, copy it directly to the kernel socket output buffer
    if (var1 instanceof DirectBuffer) {
        return writeFromNativeBuffer(var0, var1, var2, var4);
    } else {
        // If the current buffer is memory in the heap
        // Get the first address of the buffer in the heap
        int var5 = var1.position();
        // Get the end of the buffer in the heap
        int var6 = var1.limit();
        assert var5 <= var6;
        // Calculate the size of memory in the heap
        int var7 = var5 <= var6 ? var6 - var5 : 0;
        // Apply for off-heap memory the same size as the in-heap Buffer
        ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);

        int var10;
        try {
            // Copy the data from the buffer in the heap to the memory requested outside the heap
            var8.put(var1);
            var8.flip();
            var1.position(var5);
            // Copy the out-of-heap Buffer to the kernel socket output Buffer
            int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
            if (var9 > 0) {
                var1.position(var5 + var9);
            }
            var10 = var9;
        } finally {
            Util.offerFirstTemporaryDirectBuffer(var8);
        }
        returnvar10; }}Copy the code

In the above code, the execution process is as follows:

  • Judge the present firstbufferIs it off-heap memory, and if so, just off-heapbufferCopy to kernelsocketOutput buffer.
  • If you judge the currentbufferIf it is not off-heap memory, it calculates in-heapbufferThe size of the callgetTemporaryDirectBufferApply for off-heap memory, the size of memory is the same as the in-heap memorybufferSame, and will be in the heapbufferThe data is copied to off-heap memory, and eventually to off-heap memorybufferData is copied to the kernelsocketOutput buffer.

fromNIOThe source code can also have its drawbacks: it is actually one more copy job (from within the heapbufferCopy to out-of-heap memorybuffer)

Java off-heap memory

Out-of-heap memory is not managed by the JVM. How to free out-of-heap memory? (Interview highlights)

DirectByteBuffer = 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 {
        // Call the unsafe class to apply for the size of out-of-heap memory and return the virtual memory address of the out-of-heap memory for subsequent operations
        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 {
        // assign the out-of-heap address to address to facilitate subsequent operations on the out-of-heap memory
        address = base;
    }
    // Create Cleaner class objects to free up memory outside the heap
    // The run method in the Deallocator class is used to actually free the out-of-heap memory,
    // unbroadening. FreeMemory (address); This line of code frees up out-of-heap memory
    cleaner = Cleaner.create(this.new Deallocator(base, size, cap));
    att = null;
}
Copy the code

The above code execution process is as follows:

  • First callunsafeOf the classallocateMemoryThe out-of-heap () method allocates a specified size of out-of-heap memory and returns the virtual memory address of the requested out-of-heap memory tobaseVariable to facilitate subsequent operations on this part of the out-of-heap memory.
  • thenbaseThe value of a variable is assigned toaddressTo facilitate the subsequent operation of this part of the out-of-heap memory.
  • createCleanerClass object, used later to free the requested part of the out-of-heap memory, the actual release of out-of-heap memory code is inDeallocatorOf the classrunMethods.

Next look at the Deallocator class, which implements Runnable and looks like this:

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(a) {
        if (address == 0) {
            // Paranoia
            return;
        }
        / / call the unsafe. FreeMemory (address); This line of code frees up out-of-heap memory
        unsafe.freeMemory(address);
        address = 0; Bits.unreserveMemory(size, capacity); }}Copy the code

It will be executed when the class is startedrunMethod,runIn the methodunsafe.freeMemory(address);This line of code usesunsafeClass to free memory outside the heap at the specified address.

Then look at the Cleaner class, inherit the virtual reference, the structure is as follows:

public class Cleaner extends PhantomReference<Object> {}Copy the code

It can be seen from the above that the Cleaner class inherits the PhantomReference virtual Reference, and the ReferenceQueue must be specified when the virtual Reference is created, so that the current Reference can be added to the ReferenceQueue after the object pointed to by the virtual Reference is collected by GC garbage.

Cleaner = cleaner. create(this, new Deallocator(base, size, cap)); , view Cleaner create method, as follows:

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(a) {
    if (remove(this)) {
        try {
            //thunk is the Deallocator instance passed in above
            // Call Deallocator's run method above to free the off-heap memory
            this.thunk.run();
        } catch (final Throwable var2) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run(a) {
                    if(System.err ! =null) {(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                    }

                    System.exit(1);
                    return null; }}); }}}Copy the code

Overall process: When the JVM starts, the consuming thread starts consuming the pending queue (see this after the Java reference queue), and since the Cleaner inherits the PhantomReference virtual reference, The ReferenceQueue queue must be specified until the PhantomReference virtual reference is created. Then the consumer thread will queue the Cleaner object out of the pending queue and execute the Clean method in the Cleaner object. The clean method eventually calls the run method of the Deallocator out-of-heap memory release, which ultimately frees out-of-heap memory using the unsafe class.

Java Reference queue

For more information on strong, soft, weak, and virtual references, see the ThreadLocal article, which covers this in detail, as well as a code demo

Reference Four states

The four states of Reference are as follows:

  • Active: Indicates the active state.
  • Pending: waiting for theReferenceQueueThe queue
  • Enqueued: already into theReferenceQueueThe queue
  • Inactive: Failure state

whenReferenceThe object is active when it is createdReferenceObject directedobjectThere are strong references. whenobjectWhen there is no strong reference to point toGCWords,GCThe thread will connect withobjectThe associatedReferenceObject joins toPendingIn the queue, it becomes wait inReferenceQueueThe queue state is then determined by a consuming thread (daemon thread)PendingIn the queueReferenceObject is specified when the call constructor is createdReferenceQueueThat will be specifiedReferenceQueuetheReferenceObject move toReferenceQueueQueue becomes inboundReferenceQueueQueue status, and for those not specifiedReferenceQueuetheReferenceThe object will becomeInactiveState. forReferenceQueueIn the queueReferenceObject if fromReferenceQueueWhen a queue is unqueued, it is calledReferenceQueuethepollMethod out of the queue will also becomeInactiveState.

Source code analysis

The Reference class attributes are as follows:

// Save a reference to the real object
private T referent;         /* Treated specially by GC */

// The reference queue can be passed externally to determine whether the specified object is collected by gc
volatile ReferenceQueue<? super T> queue;

/* When active: NULL * pending: this * Enqueued: next reference in queue (or this if last) * Inactive: this */
 //ReferenceQueue is a one-way linked list with each element having a Next pointer to the Next element
@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 */
// The vm thread adds the current Reference to the pending queue after deciding that the actual object in the current Reference is garbage.
// Pending queue is a one-way linked list, connected using discovered fields
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. */
The pending attribute in the pending queue points to the first element, which is equivalent to head
// Elements in the pending queue form a unidirectional list using the above discovered attribute (equivalent to the next pointer)
// This is done by the GC thread
private static Reference<Object> pending = null;
Copy the code

Important attribute information is as follows:

  • referent: Saves a reference to the real object
  • queue: reference queue, which can be passed externally to determine whether a specified object is collected by gc
  • next:ReferenceQueueIt’s a one-way linked list, one for each elementNextThe pointer points to the next element
  • discovered:vmThe thread is judging the currentReferenceAfter the real object in is garbage, it will be currentReferenceTo join thependingIn the queue,pendingA queue is a one-way linked list that is useddiscoveredJoin fields
  • pending:pendingA queue is a first in, last out (stack) queue, the consuming thread will consume from the beginning,pendingProperty refers to the first element, equivalent tohead.pendingThe element in the queue passes the abovediscoveredAttributes form a one-way linked list (equivalent tonextPointer), this isvmThread assembly is complete

ReferenceHandler, which consumes pending elements and adds elements specified in the pending queue to the ReferenceQueue, has the following structure:

/* 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) newNoClassDefFoundError(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(a) {
        while (true) {
            tryHandlePending(true); }}}Copy the code

This class is used to process elements in the pending queue above and inherits Thread, with the emphasis on its run method. Then look at the static code block for the ReferenceHandler class as follows:

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 */
    // Sets the priority of the consuming thread to the highest
    handler.setPriority(Thread.MAX_PRIORITY);
    // Set the consuming thread to the daemon thread
    handler.setDaemon(true);
    // Start the daemon thread
    handler.start();

    // provide access in SharedSecrets
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference(a) {
            return tryHandlePending(false); }}); }Copy the code

TryHandlePending = tryHandlePending = tryHandlePending ();

/** * The Reference element in the pendiing queue is added to the ReferenceQueue */ that was specified when the Reference was created
static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    / / important
    Cleaner c;
    try {
        // What needs to be synchronized?
        The JVM garbage collector thread needs to append Reference elements to the pending queue
        //2. The current consuming thread consumes the pending queue.
        synchronized (lock) {
            // If the pending queue is not empty
            if(pending ! =null) {
                r = pending;
                // 'instanceof' might throw OutOfMemoryError sometimes
                // so do this before un-linking 'r' from the 'pending' chain...
                // C is null in general. When r points to a cleaner Reference instance, c is not null and points to a Cleaner object
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // unlink 'r' from 'pending' chain
                // The next two lines make the queue logic (pending queue)
                pending = r.discovered;
                r.discovered = null;
            } else {
                // If pending queue is empty
                // The waiting on the lock may cause an OutOfMemoryError
                // because it may try to allocate exception objects.
                if (waitForNotify) {
                    // The current consuming thread releases the lock, blocks the wait,
                    // until another thread wakes up with the current lock.notify() or lock.notifyall ()
                    Who wakes up the current consuming thread? Is the JVM garbage collector thread,
                    // After the VM thread adds a Reference element to the pending queue, it calls Lock.notify ()
                    lock.wait();
                }
                // retry if waited
                returnwaitForNotify; }}}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
    // If the Reference is true, the current Reference is a cleaner instance
    if(c ! =null) {
        // If the current Reference is a Cleaner type, it will not be transferred from the pending queue to the ReferenceQueue.
        Cleaner.clean () ¶
        c.clean();
        return true;
    }
    
    // In most cases, the following is executed, c==null
    // Get the ReferenceQueue specified when the Reference was created
    ReferenceQueue<? super Object> q = r.queue;
    // ReferenceQueue was specified when Reference was created
    Q.enqueue (r) : adds Reference to the ReferenceQueue queue
    if(q ! = ReferenceQueue.NULL) q.enqueue(r);return true;
}
Copy the code

The main logic in the above code is as follows:

  • The consuming thread willpendingThe element in the queue is queued and determines the current queued elementReferenceWhether it iscleanerInstance, if it is not, theReferenceMove to CreateReferenceWhen the specifiedReferenceQueueIn the.
  • If currently out of the queueReferenceiscleanerInstance, is executedcleanerthecleanMethods.