More articles, concerns:liangye-xo.xyz

Interpretation of DirectByteBuffer

Introduction to

NIO is inseparable from DirectByteBuffer. If DirectByteBuffer is properly used at the beginning, it can reduce the first copy between user and kernel data. However, DirectByteBuffer itself is out-of-heap memory, which is not controlled by GC, but out-of-heap memory also needs to be reclaimed, otherwise there will be a memory leak. How does the JVM handle this problem?

Take a look at demo: TestPhantomReference

public class TestPhantomReference {
    public static void main(String[] args) {
        // Create a reference queue
        ReferenceQueue queue = new ReferenceQueue();
        byte[] buf = new byte[1024 * 1024 * 10];
        / / incoming ReferenceQueue
        PhantomReference phantomReference = new PhantomReference(buf, queue);
        // null strong references
        buf = null;
        System.out.println("Was there data in the queue before GC?" + "" + (queue.poll() == null ? "没有" : "有"));
        System.out.println("Objects referenced in ref before GC:" + phantomReference.get());
        // After gc occurs, buF pairs of memory are reclaimed - the JVM itself is aware of Reference
        // If a Reference Reference refers indirectly to a block of heap memory, the JVM will ignore it
        // When a GC occurs, if it should be collected, it will be collected
        System.gc();
        System.out.println("After gc, object referenced in ref:" + phantomReference.get()); Phantomreference.get () always returns null
        System.out.println("Whether the ref and weakRef obtained in queue are consistent:" + (queue.poll() == phantomReference ? true : false)); }}Copy the code

Running results:

Was there data in the queue before GC? Objects referenced in ref before GC:nullAfter gc, objects referenced in ref:nullWhether the ref and weakRef obtained in queue are consistent:true
Copy the code

From the execution result, it is not difficult to analyze the role of ReferenceQueue: When the associated object in Reference is reclaimed, the corresponding Reference of Reference instance will be added to the Reference queue

Therefore, we can check whether the object associated with Reference is gc through the Reference queue. When the Reference object is gc, the Reference will be added to the Reference queue. Based on this, when we find data in the Reference queue, we can go get the Reference and do something else

Interpretation of structure

    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 {
            // base - Stores the virtual memory address corresponding to the out-of-heap memory created
            // allocateMemory local method that actually triggers a system call to allocateMemory
            base = UNSAFE.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        // Initialize the memory address
        UNSAFE.setMemory(base, size, (byte) 0);
        if(pa && (base % ps ! =0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            // Store the address of the pair in and out of the heap
            address = base;
        }
        // Release the memory
        // Parameter 1: DirectByteBuffer instance
        // Parameter 2: Deallocator instance
        cleaner = Cleaner.create(this.new Deallocator(base, size, cap));
        att = null;
    }
Copy the code

Unsafe is used in the constructor to allocate out-of-heap memory, and it can itself be used to free out-of-heap memory

Eventually, native methods will be called accordingly

    private native long allocateMemory0(long bytes);
    private native void freeMemory0(long address);
Copy the code

In the construction, the Cleaner is allocated memory for related initialization

When the Cleaner is created, parameters are passed: this (DirectByteBuffer), Deallocator instance itself – can be understood as Deallocator is a memory release

    // Deallocator - Memory locator
    private static class Deallocator
        implements Runnable
    {
Copy the code

Deallocator implements Runnable, corresponding to its run:

        public void run(a) {
            if (address == 0) {
                // Paranoia
                return;
            }
            // Call the local method freeMemory via Unsafe to free out-of-heap memory
            UNSAFE.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
Copy the code

As you can see, out-of-heap memory is freed in run() by calling unsafe.freememory ()

It is not difficult to guess that Deallocator is used to release the out-of-heap memory corresponding to DirectByteBuffer. How is the logic integrated?

Continue to look at:

Cleaner is a field of DirectByteBuffer that implements virtual references

   // Cleaner inherits virtual references
    private final Cleaner cleaner;
Copy the code
// Inherits the virtual reference - PhantomReference
public class Cleaner
    extends PhantomReference<Object>
{
    // Cleaner creates the reference queue itself
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
Copy the code

The Cleaner structure

    public static Cleaner create(Object ob, Runnable thunk) {
        if (thunk == null)
            return null;
        return add(new Cleaner(ob, thunk));
    }

	private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        // Save the Deallocator instance reference to thunk
        this.thunk = var2;
    }
Copy the code

Save the Deallocator instance reference to thunk

When the Cleaner is created, instances of this and Deallocator are passed, and when DirectByteBuffer is recycled (i.e., the Ref association object), the Cleaner is added to the queue, initially to reference.pending

Light from the construction does not seem to be visible, but you can guess the implementation: rely on Reference!

Interpretation of the Reference

The change of Ref state is as follows:

field

    // Store the real object reference
    private T referent;         /* Treated specially by GC */

    // Reference queue, external can pass the reference queue, convenient to determine whether the Ref associated object referent has been recycled by GC
    volatile ReferenceQueue<? super T> queue;

    // Save the reference to the next element in the reference queue
    @SuppressWarnings("rawtypes")
    volatile Reference next;

    // The vm thread will add the current ref to the pending queue after it determines that the current ref is garbage.
    // Pending queue is a one-way linked list that is connected using discovered
    private transient Reference<T> discovered;
Copy the code
    // Pending header field of a linked list
    // Pending header fields are appended by JVM garbage collector threads
    private static Reference<Object> pending = null;
Copy the code

structure

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null)? ReferenceQueue.NULL : queue; }Copy the code

Initialize the

When Reference is loaded, a piece of static code is executed:

    // When Reference is loaded
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for(ThreadGroup tgn = tg; tgn ! =null;
             tg = tgn, tgn = tg.getParent());
        // Create ReferenceHandler
        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); // Highest priority
        handler.setDaemon(true); // Daemon thread
        / / key
        handler.start();

        // provide access in SharedSecrets
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean waitForReferenceProcessing(a)
                throws InterruptedException
            {
                return Reference.waitForReferenceProcessing();
            }

            @Override
            public void runFinalization(a) { Finalizer.runFinalization(); }}); }Copy the code

In the initialization block, the ReferenceHandler is created and its start() is executed.

What is ReferenceHandler?

    private static class ReferenceHandler extends Thread {
        / / load
        private static void ensureClassInitialized(Class
        clazz) {
            try {
                // The Cleaner classes have been loaded by the JVM
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) newNoClassDefFoundError(e.getMessage()).initCause(e); }}// execute at initialization time
        static {
            // 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.
            ensureClassInitialized(Cleaner.class);
        }

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, null, name, 0.false);
        }

        public void run(a) {
            while (true) {
                // Consume Pending queue elementsprocessPendingReferences(); }}}Copy the code

When a Reference is initialized, the current thread (ReferenceHandler) consumes elements in the pending queue

    static boolean tryHandlePending(boolean waitForNotify) {
        // Save a reference to the Ref that the current thread wants to consume
        Reference<Object> r;
        / / key
        Cleaner c;
        try {
            // Why is synchronization needed here?
            The JVM garbage collector thread needs to append a ref to the pending queue
            // 2. The current thread consumes pending queues
            synchronized (lock) {
                if(pending ! =null) {
                    // Get the pending queue header element
                    r = pending;
                    // 'instanceof' might throw OutOfMemoryError sometimes
                    // so do this before un-linking 'r' from the 'pending' chain...
                    // c is generally null. When r points to a cleaner instance, c is not null and points to a cleaner object
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // unlink 'r' from 'pending' chain
                    // Queue logic
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // The waiting on the lock may cause an OutOfMemoryError
                    // because it may try to allocate exception objects.
                    // In order to avoid the thread constantly polling - futile
                    if (waitForNotify) {
                        // 1. Release lock
                        / / 2. Block the current thread until the other thread calls the current lock. The notify () | lock. The notifyAll ().
                        lock.wait();
                        // Who wakes up the current consuming thread? - JVM garbage collection thread, after adding ref to pending, will call Lock.notify ()
                    }
                    // 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
        // true - if ref is a clean instance, the enqueue logic of ref will not be executed.
        if(c ! =null) {
            c.clean(); Cleaner.clean()
            return true;
        }
        // Get the reference queue associated with ref
        ReferenceQueue<? super Object> q = r.queue;
        // true - refQueue specified when creating ref - execute queue logic
        if(q ! = ReferenceQueue.NULL) q.enqueue(r);return true;
    }
Copy the code

It can be seen that when the thread consumes the element (Ref reference) in the pending queue, if the element is not a Cleaner instance and is associated with ReferenceQueue, it will be added to the ReferenceQueue and the state is set. Cleaner.clean() is executed directly when the element is a Cleaner instance

    public void clean(a) {
        if(! remove(this))
            return;
        try {
            Deallocator.run() - deallocator.run (
            thunk.run();
        } catch (final Throwable x) {
            AccessController.doPrivileged(new PrivilegedAction<>() {
                    public Void run(a) {
                        if(System.err ! =null)
                            new Error("Cleaner terminated abnormally", x)
                                .printStackTrace();
                        System.exit(1);
                        return null; }}); }}Copy the code

As you can see, clean() calls thunk.run(), and thunk is the Deallocator passed in when the cleaner is created

Free out-of-heap memory is thus achieved: DirectByteBuffer has a field Cleaner. When DirectByteBuffer is gc, the JVM garbage collection thread adds the Cleaner reference to the Pendding queue. When the thread (ReferenceHanlder) consumes the pendding element, it detects that it is an instance of a Cleaner class. Instead of adding the reference to the RefQueue, cleaner.clean () is executed directly. This.chunk.run () is called, which corresponds to deallocator.run (), where the out-of-heap memory is freed!

At this point, DirectByteBuffer memory free reading is complete!