在使用javaNIO时,会使用到buffer。那么JDK里面如何分配堆外内存到大小呢?下面根据一系列猜想,整理了一个求证的过程:
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import sun.misc.VM;
import sun.nio.ch.DirectBuffer;
public class DirectByteBufferTest {
public static void main(String[] args) throws InterruptedException{
System.out.println("直接内为:"+VM.maxDirectMemory()/1024/1024+"M");
//分配128MB直接内存
ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);
System.out.println("分配完成");
TimeUnit.SECONDS.sleep(10);
//清除直接缓存
System.out.println("开始清理数据");
((DirectBuffer)bb).cleaner().clean();
TimeUnit.SECONDS.sleep(10);
System.out.println("ok");
}
}
上面代码使用ByteBuffer对象通过JVM去申请128M的堆外内存,下面看一下不同情况下的执行效果:
从上面的执行结果来看:当指定-XX:MaxDirectMemorySize参数时,堆外内存分配时按照这个参数指定的大小来分配的。但是当不指定-XX:MaxDirectMemorySize参数时,默认的堆外内存分配跟JVM的内存有关系。那么堆外内存分配导致是怎么回事呢?
我们先来看看ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);这句话中allocateDirect执行了什么:
继续跟下去:
这里我们根据上面堆外内存不足报错打印出来的调用关系,可以确定在Bits.reserveMemory(size, cap);方法里面会有堆外内存分配的策略
我们来看Bits.reserveMemory(size, cap);这个方法的实现过程:
// These methods should be called whenever direct memory is allocated or
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
static void reserveMemory(long size, int cap) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// optimist!
if (tryReserveMemory(size, cap)) {
return;
}
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
while (jlra.tryHandlePendingReference()) {
if (tryReserveMemory(size, cap)) {
return;
}
}
// trigger VM's Reference processing
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;
}
if (sleeps >= MAX_SLEEPS) {
break;
}
if (!jlra.tryHandlePendingReference()) {
try {
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方法一直返回false,导致在while(true)里面重试了几次都不满足tryReserveMemory(size, cap),然后sleeps >= MAX_SLEEPS超时后break出来直接 throw new OutOfMemoryError("Direct buffer memory");
那么为什么tryReserveMemory(size, cap)一直不成功,这个方法是干什么的呢?
这里它从maxMemory里面预先“支出”需要的内存大小,然后再去真正去占用对应的内存大小。其中maxMemory是多大呢?
可以看出VM.maxDirectMemory()决定了JVM堆外内存的分配大小,那么VM.maxDirectMemory()又是什么决定的呢?
那我们需要看看directMemory这个变量有没有在某个地方被赋值了,在VM这个类里面我们找到了saveAndRemoveProperties(Properties var0)方法修改了directMemory变量。到现在为止,我们跟着ByteBuffer一路找到了创建一个ByteBuffer的过程链条。这个探索之旅,我们获得了一个很重要的结论:
使用堆外内存先从JVM里面申请大小限额(注意这里不是说是从JVM里面占用内存,而仅仅是从JVM里面记录一个数据,告诉JVM我已经用了多少堆外内存,下次再有申请不要超额),然后再去占用堆外内存
上面我们从上而下探索了byteBuffer的过程,下面我们接着上面的saveAndRemoveProperties方法来看看是什么决定了VM.maxDirectMemory()。先看看谁调用了saveAndRemoveProperties方法,我们看到java.lang.System类调用了sun.misc.VM.saveAndRemoveProperties(props);
我们从下而上找到调用关系后,下面我们再从上而下理解一下这个过程:
System这个类负责在系统启动时初始化JVM数据,这里调用saveAndRemoveProperties,是处理启动参数:
这里var0.remove("sun.nio.MaxDirectMemorySize");方法,当我们启动JVM没有指定-XX:MaxDirectMemorySize时,返回-1,否则返回我们指定-XX:MaxDirectMemorySize的值。当我们指定-XX:MaxDirectMemorySize时,directMemory变量会被赋值为var1的值。否则会执行directMemory= Runtime.getRuntime().maxMemory();
那么Runtime.getRuntime().maxMemory();的逻辑是什么呢?
这里是个native方法,只能从JDK的源码里面去找答案了。这篇文章就先介绍到这里吧,下篇文章我们将详细介绍Runtime.getRuntime().maxMemory();跟GC的关系。
【1】使用堆外内存先从JVM里面申请大小限额(注意这里不是说是从JVM里面占用内存,而仅仅是从JVM里面记录一个数据,告诉JVM我已经用了多少堆外内存,下次再有申请不要超额),然后再去占用堆外内存
【1】JVM源码分析之堆外内存完全解读