How to use virtual reference to manage out of heap memory in NIO

Keywords: Java Back-end

Virtual reference is the weakest reference. How weak is it? That is, you define a virtual reference, and you can't get the object through the virtual reference, let alone affect the life cycle of the object. The only function in a virtual reference is to queue to receive notification that the object is about to die.

1. Characteristics of virtual reference

  • Virtual reference must be used together with ReferenceQueue. When GC prepares to recycle an object, if it finds that it still has virtual reference, it will add the virtual reference to its associated ReferenceQueue before recycling.

  • Cannot get the real object referenced by the virtual reference object through the virtual reference!!!!

    ReferenceQueue queue = new ReferenceQueue();
    PhantomReference<byte[]> reference = new PhantomReference<byte[]>(new byte[1], queue);
    // Call reference.get() to try to get [real object referenced by virtual reference object], and null will be returned here
    System.out.println(reference.get());
    

    The reason is that the reference class overrides the get method

    public class PhantomReference<T> extends Reference<T> {
    
      	// Override the get method and return null directly
        public T get() {
            return null;
        }
    }
    

2. Role of virtual reference

Using virtual reference to manage off heap memory is a typical use of virtual reference. For example, when NIO of JDK allocates off heap memory, it uses the feature of virtual reference to manage off heap memory.

a) . prospect introduction

Question 1: why doesn't Java release memory manually

We know that in Java, we don't need to release the memory manually like C + +, because our memory is allocated in the JVM heap space, and the GC garbage collector will help us automatically manage and recycle the garbage memory. But what if we can manipulate off heap memory?

Question 2: is there any way to manipulate off heap memory in Java

The answer is: Yes

  • Method 1: for example, in NIO of JDK, we can manually allocate a piece of off heap memory through ByteBuffer.allocateDirect (size)
  • Method 2: for example, we can get the Unsafe object through reflection, and then open up a piece of off heap memory by calling the unsafe.allocateMemory(size) method

Question 3: how to free out of heap memory in Java

As we can see from the above, we can allocate out of heap memory through ByteBuffer.allocateDirect and unsafe.allocateMemory. But how do we release the allocated out of heap memory when it is used up?

  • Out of heap memory is not managed by the JVM garbage collector, so the garbage collector cannot reclaim out of heap memory. Therefore, the out of heap memory needs to be released manually. We can manually release the out of heap memory at the specified location through the unsafe.freeMemory(address) method of the unsafe object.

b) Example of operating out of heap memory through unsafe

  • To operate the off heap memory through the unsafe object, you need to release it manually
/**
 * The underlying principle of direct memory allocation: Unsafe
 */
public class Demo1_27 {

    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // Allocate memory
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();

        // Free memory
        unsafe.freeMemory(base);
        System.in.read();
    }

    /**
     * Get Unsafe objects by reflection
     */
    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

c) Example of using out of heap memory through ByteBuffer

/**
 * Add the JVM runtime parameter: - XX:+DisableExplicitGC to disable the impact of explicit recycling on direct memory
 */
public class Demo1_26 {
  
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
      	// ① Allocate 1G memory
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("Allocation complete...");
        System.in.read();
        System.out.println("Start release...");
      	// ② The strong reference of the object referenced by the virtual reference is released
        byteBuffer = null;
        System.gc(); // Explicit garbage collection, Full GC
      	// ③ . block here. Analyze the memory consumption of the current JVM process through the memory analysis tool. Observe whether the 1G memory allocated above is recycled
        System.in.read();
    }
}

Through the memory analysis tool, you can see that 1gb of memory has been recycled. But we didn't manually free the out of heap memory. What is this?

Before memory release

After memory release

3. Managing out of heap memory analysis using virtual references

  • The above describes two methods to use off heap memory. One is to rely on unsafe objects, and the other is ByteBuffer in NIO. It is very difficult for ordinary developers to directly use unsafe objects to operate memory, and memory leakage is easy to occur if memory management is improper. So it's not recommended. ByteBuffer is recommended to operate off heap memory.
  • In the ByteBuffer case above, we did not release the memory manually, but in the end, when the ByteBuffer object is garbage collected, the off heap memory is still released. What is the reason? Is it out of heap memory reclaimed by the garbage collector? Obviously not, because out of heap memory is not managed by the JVM garbage collector.

For the source code of ByteBuffer, I tried to post it at the beginning, but I found that it takes too long to say so. Here, let's talk about how to release the out of heap memory in ByteBuffer in the way of flow!!!

① Review of test code

/**
 * Add the JVM runtime parameter: - XX:+DisableExplicitGC to disable the impact of explicit recycling on direct memory
 */
public class Demo1_26 {
  
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
      	// ① Allocate 1G memory
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("Allocation complete...");
        System.in.read();
        System.out.println("Start release...");
      	// ② The strong reference of the object referenced by the virtual reference is released
        byteBuffer = null;
        System.gc(); // Explicit garbage collection, Full GC
      	// ③ . block here. Analyze the memory consumption of the current JVM process through the memory analysis tool. Observe whether the 1G memory allocated above is recycled
        System.in.read();
    }
}

② . process analysis

1. Figure displaying memory before byteBuffer is not recycled

That is, when ② in the source code has not been executed, the memory diagram is as follows

  • You can see that the object referenced by the virtual reference is actually the byteBuffer object. Therefore, we need to focus on what operations will be triggered after the byteBuffer object is recycled.

2. Figure displaying the memory after byteBuffer is reclaimed

  • After ByteBuffer is recycled, during GC garbage collection, it is found that the virtual reference object Cleaner is an object of phantom reference type, and the object referenced by the object (ByteBuffer object) has been recycled
  • Then he puts the object into the reference queue
  • A thread with low priority in the JVM will fetch the virtual reference object from the queue and call back the clean () method
  • The work done in the clean () method is actually to release this memory according to the memory address (internally or through the unsafe object).

3. Part of the source code display

a) . ByteBuffer creation source code
// Create out of heap memory 
public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
 }

 // This is a virtual reference object
 private final Cleaner cleaner;

// Create out of heap memory logic
DirectByteBuffer(int cap) {

        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 {
          	// Use the unsafe object to allocate a block of off heap memory
            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 = base;
        }
  			// [key] create a virtual reference object that points to this object (that is, to the created byteBuffer object)
  			// Deallocator here can focus on exploring.
  			// In fact, the Deallocator is saved to the virtual reference object cleaner. When the virtual reference object is put into the queue, the clean() method of the Deallocator object is executed to clear the out of heap memory. See the source code below
  			// You can see that the allocated out of heap memory address is passed to the Deallocator object to save. This address will also be used to release the out of heap memory
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
b) How Deallocator releases memory source code
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;
    }

  	// When the virtual reference object is put into the queue, a thread in the JVM will fetch the elements in the queue. This method is then executed to free memory
    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
      	// Free off heap memory by allocating the address saved when off heap memory is allocated
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }

}

Posted by xeno on Wed, 10 Nov 2021 19:34:26 -0800