[JVM] Detailed explanation of direct memory

1. Overview of direct memory

The following is the description of Java direct memory in section 2.2.7 of "In-depth Understanding of Java Virtual Machine, Third Edition".

Direct memory (Direct Memory) is not part of the virtual machine runtime data area, nor is it a memory area defined in the "Java Virtual Machine Specification" . But this part of memory is also frequently used, and may also cause OutOfMemoryErrorexceptions, so we put it here to explain it together. A new NIO(New Input/Output)class , and an I/O method based on channels (Channel) and buffers (Buffer) was introduced. It can use the Native function library to directly allocate off-heap memory, and then store it in the Java heap through a The DirectByteBufferobject operates as a reference to this memory. This can significantly improve performance in some scenarios, because it avoids copying data back and forth between the Java heap and the Native heap. Obviously, the direct memory allocation of the machine will not be limited by the size of the Java heap, but since it is memory, it will definitely be limited by the total memory of the machine (including physical memory, SWAP partition or paging file) and the addressing space of the processor. Generally, when server administrators configure virtual machine parameters, they will -Xmxset , but often ignore direct memory, so that the sum of each memory area is greater than the physical memory limit (including physical and operating system-level limits). , resulting in OutOfMemoryErroran exception .

Direct memory is often used for NIO operations, for data buffers.

However, the cost of direct memory allocation and recovery is high, but the read and write performance is high.

Finally, it is not subject to JVM memory recovery management


2. Direct memory usage

The following example uses two methods to copy files to another place

  1. Traditional Java buffer
  2. direct memory
static final String FROM = "D:\\BaiduNetdiskDownload\\《MYSQL内核:INNODB存储引擎 卷1》.zip";
static final String TO = "D:\\BaiduNetdiskDownload\\《MYSQL内核:INNODB存储引擎 卷1》(1).zip";
static final int _1Mb = 1024 * 1024;

public static void main(String[] args) {
    
    
    io();
    directBuffer();
}

private static void directBuffer() {
    
    
    long start = System.nanoTime();
    try (FileChannel from = new FileInputStream(FROM).getChannel();
         FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
    
    
        ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
        while (true) {
    
    
            int len = from.read(bb);
            if (len == -1) {
    
    
                break;
            }
            bb.flip();
            to.write(bb);
            bb.clear();
        }
    } catch (IOException e) {
    
    
        e.printStackTrace();
    }
    long end = System.nanoTime();
    System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}

private static void io() {
    
    
    long start = System.nanoTime();
    try (FileInputStream from = new FileInputStream(FROM);
         FileOutputStream to = new FileOutputStream(TO);
        ) {
    
    
        byte[] buf = new byte[_1Mb];
        while (true) {
    
    
            int len = from.read(buf);
            if (len == -1) {
    
    
                break;
            }
            to.write(buf, 0, len);
        }
    } catch (IOException e) {
    
    
        e.printStackTrace();
    }
    long end = System.nanoTime();
    System.out.println("io 用时:" + (end - start) / 1000_000.0);
}

image-20230127135245522


2.1 Java buffer

Java itself does not have the ability to read and write the disk. If you want to use the ability to read and write the disk, you must call the function provided by the operating system.

That is, the local method will be called internally, and the state of the CPU will be switched from user mode to kernel mode at the same time.

insert image description here

At the same time, the memory will also change accordingly. When switching to the kernel state, the disk file will be read into the system buffer first (read in batches). Java cannot use the system buffer, and Java will create a file in the heap memory. Java buffer ( byte[] buf = new byte[_1Mb]), if Java wants to read the system buffer, it needs to indirectly read the data in the system buffer into the Java buffer, and Java can operate on the Java buffer.

image-20230127135942255

The reason why using traditional IO is relatively inefficient is that the disk file needs to be read into the system buffer first, and then the system buffer is read into the Java buffer before Java can process the disk file, which causes unnecessary data copying. Efficiency is thus lower.


2.2 Direct memory

When executed ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);, the operating system will allocate a 1MB memory.

This memory Java code can be directly accessed, and the operating system can also be directly accessed. It is equivalent to a piece of memory shared by Java and the operating system.

At this time, the disk file can be read into the direct memory, and then the Java code can operate on the direct memory, that is, one less copy operation than traditional IO, so the efficiency is higher.

image-20230127140531917


3. Release of direct memory

The following code allocates a block of 1G direct memory

public class Demo1_26 {
    
    
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
    
    
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC
        System.in.read();
    }
}

After the allocation is successful, you can see that the memory of the Java program is more than 1G in the task manager

image-20230127141209833

Then byteBufferset to NULL, start garbage collection

image-20230127141321089

The memory of the Java program directly drops by about 1G, indicating that the direct memory has been released.


3.1 The principle of direct memory release

Didn't it say that direct memory is not managed by JVM memory recycling? Why is direct memory released after garbage collection?

Don't worry, let's introduce the release principle of direct memory first.

static int _1Gb = 1024 * 1024 * 1024;

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

    // 释放内存
    unsafe.freeMemory(base);
    System.in.read();
}

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);
    }
}

UnsafeIt is the class used by the bottom layer of Java to allocate direct memory and release direct memory. However, it is generally not recommended to use this class.

Allocation of direct memory is achieved by the method Unsafeof the class , and its return value is the address of the allocated memoryallocateMemory

The release of memory is achieved by the method Unsafeof the class . freeMemoryThis method needs to pass in the address of the memory that needs to be freed.

In other words, if you want to release direct memory, you need to actively call freeMemorythe method

Next, view ByteBuffer.allocateDirect(_1Gb)the source code of

public static ByteBuffer allocateDirect(int capacity) {
    
    
    return new DirectByteBuffer(capacity);
}

Its interior is new DirectByteBuffer(capacity), then look at this construction method

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类分配直接内存
        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;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;



}

You can see that the constructor is used unsafe.allocateMemory(size)to allocate direct memory.

So when is the method called to release direct memory?

Need to pay attention to this line of code

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

where Deallocatoris a callback task object

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);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }

}

Viewing its source code, you can find that it has been implemented Runnable, and its runmethod calls the method of releasing direct memoryunsafe.freeMemory(address);

After reading this, that is to say, if you want to release direct memory, you must call the method Deallocatorinrun

Then continue to say Clear, Clearin the Java class library is a special type, called the virtual reference type

When the object associated with the virtual reference is recycled, cleanthe method of the virtual reference object will be triggered

private Cleaner(Object var1, Runnable var2) {
    
    
    super(var1, dummyQueue);
    this.thunk = var2;
}

public static Cleaner create(Object var0, Runnable var1) {
    
    
    return var1 == null ? null : add(new Cleaner(var0, var1));
}

public void clean() {
    
    
    if (remove(this)) {
    
    
        try {
    
    
            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;
                }
            });
        }

    }
}

Looking at cleanthe source code, you can find that Cleaner.createwhen the method is executed, it will be new Deallocator(base, size, cap)passed as a parameter Runnable var1, and when createthe method is called new Cleaner(var0, var1), it will this.thunkbe assigned Runnable var1.

That is to say, what is called in cleanthe method is the method in the method. Thus freeing direct memory.this.thunk.run();Deallocatorrun

That is, ByteBuffer inside the implementation class, (virtual reference) is used Cleaner to monitor ByteBuffer the object . Once ByteBuffer the object is garbage collected, ReferenceHandler the thread release the direct memory through the methodCleaner call ofclean freeMemory


4. Effect of disabling explicit recycling on direct memory

Explicit recycling can be disabled with the following parameter

-XX:+DisableExplicitGC 显式的

What is explicit recycling? The following is an example of explicit recycling

System.gc(); // 显式的垃圾回收,Full GC

After disabling explicit recycling, this line of code becomes invalid

This line of code is invalid and may directly affect the release of direct memory

public class Demo1_26 {
    
    
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
    
    
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC
        System.in.read();
    }
}

Also, as far as this example is concerned, System.gc(); garbage collection will not be triggered when the execution is reached.

Therefore, byteBufferalthough the object is NULL, it will not be recycled, so the 1GB of direct memory previously requested will not be released.

This byteBuffercan only be reclaimed when the real garbage collection is triggered, and the direct memory is also released.

The consequence of this is that the direct memory usage is relatively large.

The corresponding solution is to manually Unsaferelease the direct memory.


Guess you like

Origin blog.csdn.net/weixin_51146329/article/details/128770701