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;
}
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.
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).
maxMemory()
is also a native method, the source code is as follows:
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:
// 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
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
Looking at the maximum memory of the young generation, it actually reduces the size of a survivor. The source code is as follows:
You can see the return value of the max_capacity() method in the debugging process [set -Xmx1g]:
- 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:
The implementation in hotspot is in unsafe.cpp, the source code is as follows:
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:
- new a
DirectByteBuffer
object; -
DirectByteBuffer
object is called when the initialization execution constructor is executedunsafe.allocateMemory(size)
allocate memory, and use the memory address as the return result; - 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; - Save the memory address to
DirectByteBuffer
the member variable of the objectaddress
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:
- Manually call
unsafe.freeMemory()
to release, ByteBuf.release() in netty is this way; - 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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。