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!