1

1 Introduction

Since Java has NIO since version 1.4, direct memory has become so common. In NIO, direct memory acts as a buffer, using native memory instead of heap memory. This method reduces the copy operation of data between the Java heap and the native heap, and improves the efficiency of data flow to a certain extent. However, the allocation and recovery performance of direct memory is not high, and it is not recommended to allocate direct memory frequently.

Usually we allocate a piece of direct memory through allocateDirect() . This actually creates a new Java object DirectByteBuffer on the heap, which references the address of a piece of direct memory (the virtual address in the jvm process, and the actual physical address is allocated through a page fault exception). The following will introduce the process of allocating direct memory through allocateDirect() and some details of DirectMemory in the hotspot source code.

The source version of hotspot is: openjdk 11.0.14

2. DirectMemory memory allocation process

1M of direct memory can be allocated by the following methods.

 ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);

Directly enter the allocateDirect() method, you can see that a new DirectByteBuffer object is actually created.

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

The core content is in the construction method of the DirectByteBuffer class. The source code is as follows:

 DirectByteBuffer(int cap) {                   // package-private
        // 使用父类构造方法初始化ByteBuffer指针
        super(-1, 0, cap, cap);
        // 判断是否设置了 内存对齐(默认false)
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        // 如果不设置内存对齐,size和cap值一样
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        // 内存分配的一些检查和回收操作
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            // 分配内存,并返回直接内存地址
            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;



    }

2.1 Check before memory allocation

The core method is in Bits.reserveMemory(size, cap); . The source code is as follows:

 static void reserveMemory(long size, int cap) {


        if (!memoryLimitSet && VM.isBooted()) {
            // 获取最大直接内存大小
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }

        // optimist!
        // 这个方法内检查是否存在剩余直接内存空间
        if (tryReserveMemory(size, cap)) {
            // 如果还有空间进行分配,直接返回
            return;
        }
        // 获取Reference对象(这里需要Reference的一些知识点)
        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();

        // retry while helping enqueue pending Reference objects
        // which includes executing pending Cleaner(s) which includes
        // Cleaner(s) that free direct buffer memory
        // 通过Cleaner尝试释放一部分直接内存
        while (jlra.tryHandlePendingReference()) {
            // 再次检查剩余直接内存容量
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

        // trigger VM's Reference processing
        // 强制Full GC
        // 可以看到,如果直接内存余量检查不通过,就会触发Full GC
        System.gc();

        // a retry loop with exponential back-off delays
        // (this gives VM some time to do it's job)
        // 在循环中多次检查剩余直接内存容量
        boolean interrupted = false;
        try {
            long sleepTime = 1;
            int sleeps = 0;
            while (true) {
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                // MAX_SLEEPS为9
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        // 每次循环 睡眠时间 * 2(单位:ms)
                        Thread.sleep(sleepTime);
                        sleepTime <<= 1;
                        sleeps++;
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }

            // no luck
            throw new OutOfMemoryError("Direct buffer memory");

        } finally {
            if (interrupted) {
                // don't swallow interrupts
                Thread.currentThread().interrupt();
            }
        }
    }

tryReserveMemory(size, cap) The source code for direct memory margin check is as follows:

 private static boolean tryReserveMemory(long size, int cap) {

        // -XX:MaxDirectMemorySize limits the total capacity rather than the
        // actual memory usage, which will differ when buffers are page
        // aligned.
        long totalCap;
        // totalCapacity记录当前已使用直接内存大小
        // 需要分配的大小如果小于  最大直接内存和当前已使用的直接内存的差值,则为true
        // 否则,返回false
        while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
            // 通过CAS将当前已使用直接内存大小 更新为 当前新的值
            if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
                // 将已预留直接内存大小 更新 为当前新的值
                reservedMemory.addAndGet(size);
                // 计数器自增
                count.incrementAndGet();
                return true;
            }
        }

        return false;
    }

2.2 What is the default maximum DirectMemory

When checking the remaining memory above, the maximum direct memory uses the value of the maxMemory variable, and its value in the source code is as follows:

 private static volatile long maxMemory = VM.maxDirectMemory();
/**
* 这里这个directMemory在VM.java中定义了个静态变量,容易误导人,让人
* 以为maxDirectMemory的大小就是64M,其实不是的。
**/
public static long maxDirectMemory() {
        return directMemory;
    }

image.png

In fact, the value of maxMemory is not 64M. If it is not set by default, the value of maxMemory is similar to the maximum heap memory size (the value set by -Xmx). If we set the runtime parameters of the JVM -XX:MaxDirectMemorySize=xxx , maxMemory is our custom value. Look directly at the hotspot source code:

In the jvm, the attribute of -XX:MaxDirectMemorySize will be converted into the attribute of sun.nio.MaxDirectMemorySize , if not set, it will be set to -1 by default.
image.png

When the jvm starts, the value set above will be read. If it is -1 , directMemory will be set to the maximum memory at runtime (that is, almost the value of -Xmx).
image.png

maxMemory() is also a native method, the source code is as follows:
image.png

As for why it is almost equal to the value of the maximum heap memory, it is actually one less survivor space size. Or look at the hotspot source code (how maxMemory is calculated):
The corresponding method for obtaining runtime memory in hotspot is max_capacity() . The size calculation of this method is closely related to the garbage collector:

image.png

 // The particular choice of collected heap.
  static CollectedHeap* heap() { return _collectedHeap; }

You can see that there are 8 garbage collectors in this version of hotspot
image.png

The following is based on the CMS garbage algorithm <br>You can see that the heap size under the CMS garbage algorithm is actually: the maximum memory of the young generation ➕ the maximum memory of the old generation
image.png

Looking at the maximum memory of the young generation, it actually reduces the size of a survivor. The source code is as follows:
image.png

You can see the return value of the max_capacity() method in the debugging process [set -Xmx1g]:
image.png

  • It can be seen that the default maximum size of DirectMemory is (Xmx - 1 survivor);
  • Insufficient DirectMemory will cause Full GC;
In the case of the G1 garbage collector, the calculation method of max_capacity() (high-order address - status address) does not have one survivor's statement, and the -Xmx setting is as much as possible.

2.3 Memory allocation of DirectByteBuffer

The real way to allocate memory is actually unsafe.allocateMemory(size) , which is a native method:
image.png

The implementation in hotspot is in unsafe.cpp, the source code is as follows:
image.png

In fact, the bottom layer is to call the operating system malloc function to allocate memory, and then return a memory address to java.

2.3.1 Summarize the approximate allocation process of direct memory:

  1. new a DirectByteBuffer object;
  2. DirectByteBuffer object is called when the initialization execution constructor is executed unsafe.allocateMemory(size) allocate memory, and use the memory address as the return result;
  3. The jvm calls the operating system malloc function to allocate virtual memory (and then allocates actual physical memory through page fault exception in actual use), and returns the memory address to java;
  4. Save the memory address to DirectByteBuffer the member variable of the object address for reference;

So DirectByteBuffer itself exists in the jvm heap as a java object, but holds a reference to a memory address of native memory.
DirectByteBuffer occupies a small amount of memory on the heap, but is likely to hold a large native memory reference.

3. How the native memory associated with DirectMemory is cleaned up

Since the direct memory is not part of the jvm heap memory, the GC must not be able to directly manage this memory area, so how does the direct memory perform memory recovery?

As we have learned earlier, the direct memory is actually allocated through the malloc function of the operating system, so the memory release also needs to call the free function of the operating system. In java, the underlying free function can be called through unsafe.freeMemory() ---.

Based on this idea, there are only two ways to release direct memory:

  1. Manually call unsafe.freeMemory() to release, ByteBuf.release() in netty is this way;
  2. Use the GC mechanism to automatically call unsafe.freeMemory() to release the referenced direct memory during the GC process;

Today I mainly want to share the second recycling method, that is, how to release the direct memory that is no longer referenced during the GC process.

Before starting, you need to understand some prerequisite knowledge about Reference. Because the indirect recovery of direct memory through GC is completely based on PhantomReference virtual reference.

Here I will directly paste the blog of a big guy: "[java.lang.ref] PhantomReference & jdk.internal.ref.Cleaner" address: https://blog.csdn.net/reliveIT/article/details/116157523

This article comprehensively introduces the relevant knowledge of PhantomReference virtual references, and clearly describes the direct memory recovery process through interaction with GC in the DirectByteBuffer chapter. Give the big guy a thumbs up 👍.

At this point, you also know why "2.1 Check before memory allocation" is called in the Bits.reserveMemory(size, cap) method to display the call System.gc() for Full GC. This is to recycle the unreachable DirectByteBuffer object as much as possible, and only the GC will automatically trigger the call of unsafe.freeMemory() to release the direct memory.



开翻挖掘机
225 声望26 粉丝

不忘初心❤️,且行且思考