OpenJDK源码分析之DirectMemory大小(一)

发现问题

在使用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的堆外内存,下面看一下不同情况下的执行效果:

OpenJDK源码分析之DirectMemory大小(一)_第1张图片

从上面的执行结果来看:当指定-XX:MaxDirectMemorySize参数时,堆外内存分配时按照这个参数指定的大小来分配的。但是当不指定-XX:MaxDirectMemorySize参数时,默认的堆外内存分配跟JVM的内存有关系。那么堆外内存分配导致是怎么回事呢?

探索

我们先来看看ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);这句话中allocateDirect执行了什么:

OpenJDK源码分析之DirectMemory大小(一)_第2张图片

继续跟下去:

OpenJDK源码分析之DirectMemory大小(一)_第3张图片

这里我们根据上面堆外内存不足报错打印出来的调用关系,可以确定在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)一直不成功,这个方法是干什么的呢?

OpenJDK源码分析之DirectMemory大小(一)_第4张图片

这里它从maxMemory里面预先“支出”需要的内存大小,然后再去真正去占用对应的内存大小。其中maxMemory是多大呢?

OpenJDK源码分析之DirectMemory大小(一)_第5张图片

可以看出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);

OpenJDK源码分析之DirectMemory大小(一)_第6张图片

我们从下而上找到调用关系后,下面我们再从上而下理解一下这个过程:

System这个类负责在系统启动时初始化JVM数据,这里调用saveAndRemoveProperties,是处理启动参数:

OpenJDK源码分析之DirectMemory大小(一)_第7张图片

这里var0.remove("sun.nio.MaxDirectMemorySize");方法,当我们启动JVM没有指定-XX:MaxDirectMemorySize时,返回-1,否则返回我们指定-XX:MaxDirectMemorySize的值。当我们指定-XX:MaxDirectMemorySize时,directMemory变量会被赋值为var1的值。否则会执行directMemory= Runtime.getRuntime().maxMemory();

那么Runtime.getRuntime().maxMemory();的逻辑是什么呢?

OpenJDK源码分析之DirectMemory大小(一)_第8张图片

这里是个native方法,只能从JDK的源码里面去找答案了。这篇文章就先介绍到这里吧,下篇文章我们将详细介绍Runtime.getRuntime().maxMemory();跟GC的关系。

得出结论

【1】使用堆外内存先从JVM里面申请大小限额(注意这里不是说是从JVM里面占用内存,而仅仅是从JVM里面记录一个数据,告诉JVM我已经用了多少堆外内存,下次再有申请不要超额),然后再去占用堆外内存

推荐阅读

【1】JVM源码分析之堆外内存完全解读

你可能感兴趣的:(JDK)