【Java】DirectByteBuffer 堆外内存源码解读

目录

一、DirectByteBuffer直接缓冲区介绍

1.1 如何使用DirectByteBuffer

1.2 DirectByteBuffer对象实例化过程

1.2.1 构造器

1.2.2 判断是否有足够的空间可供申请java.nio.Bits#reserveMemory

1.2.3 尝试申请内存

1.2.4 为什么要手动调用System.gc()

1.2.5 DirectByteBuffer实现内存分配

1.2.6 内存分配失败回收内存计数:java.nio.Bits#unreserveMemory 

1.2.7 内存分配小结

1.3 DirectByteBuffer读写逻辑 

 二、堆外内存回收

2.1 DirectByteBuffer及对应的堆外内存回收机制Cleaner

2.1.1 Cleaner对象的创建

2.2. 释放堆外内存

2.2.1 守护线程处理对象引用Reference以及ReferenceQueue

 2.2.2 如何触发Cleaner中的Deallocator线程释放内存

2.3 DirectByteBuffer有关的JVM选项

三、JVM使用堆外内存的原因

3.1 降低垃圾回收停顿提升性能

3.2 避免JVM大堆常驻无法回收

3.3 提升内存I/O减少内存数据拷贝

四、本节总结


作为Java程序开发者我们都知道编译好的程序需要在Java虚拟机中运行,需要分配内存空间指定堆的大小,通过-Xms2G -Xmx2G参数指定JVM启动堆内存的大小,这是堆内内存的分配。而Java程序往往在开发中需要通过JNI调用lib库,以及使用第三方开发框架,诸如mina,netty Nio框架,这些框架为了提高通信效率,减少内存拷贝减轻Jvm GC压力使用了堆外内存,我们从下图了解一下Java运行时所需内存的分配:

根据上图很直观的发现Java程序运行需要的内存分堆内内存(Heap)和堆外内存(No Heap),本次介绍的DirectByteBuffer只是堆外内存的其中一部分,除此之外还有字节码、本地native方法调用、JVM运行本身所需内存等。

一、DirectByteBuffer直接缓冲区介绍

        DirectByteBuffer是ByteBuffer的一个实现,ByteBuffer有两个类实现,一类是HeapBuffer(堆内内存),另一类是DirectBuffer(堆外内存)

        

1.1 如何使用DirectByteBuffer

如果需要实例化一个DirectByteBuffer,可以使用java.nio.ByteBuffer#allocateDirect这个方法

public static ByteBuffer allocateDirect(int capacity) {

    return new DirectByteBuffer(capacity);

}

1.2 DirectByteBuffer对象实例化过程

1.2.1 构造器

我们来看一下DirectByteBuffer是如何构造,如何申请与释放内存的,先看看DirectByteBuffer的构造器

    DirectByteBuffer(int cap) {                   // package-private
        //初始化Buffer的四个核心属性
        super(-1, 0, cap, cap);
        //判断是否需要页面对齐,通过参数-XX:+PageAlignDirectMemory控制,默认为false
        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.allocateMemory分配堆外内存,并返回堆外内存的基地址
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            // 分配失败,释放内存
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        // 初始化内存空间为0
        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对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,堆外内存也会被释放
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;

    }

1.2.2 判断是否有足够的空间可供申请java.nio.Bits#reserveMemory

// 该方法主要用于判断申请的堆外内存是否超过了用例指定的最大值

// 如果还有足够空间可以申请,则更新对应的变量

// 如果已经没有空间可以申请,则抛出OutOfMemoryError
// 参数解释:

//     size:根据是否按页对齐,得到的真实需要申请的内存大小

//     cap:用户指定需要的内存大小(<=size)
static void reserveMemory(long size, int cap) {
        // 获取最大可以申请的对外内存大小,默认值是64MB
        // 可以通过参数-XX:MaxDirectMemorySize=<size>设置这个大小
        // -XX:MaxDirectMemorySize限制的是用户申请的大小,而不考虑对齐情况
        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
        // 如果已经没有足够空间,则尝试手动触发一次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;
                }
                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();
            }
        }
    }

其中,如果系统中内存( 即,堆外内存 )不够的话:

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

jlra.tryHandlePendingReference()会触发一次非堵塞的Reference#tryHandlePending(false)。该方法会将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放。

1.2.3 尝试申请内存

java.nio.Bits#tryReserveMemory方法尝试计算可以分配的真实内存大小,如何可以申请,则更新以下参数:

  • toalCapacity=toalCapacity+cap,目前用户已经申请的总空间大小;
  • reservedMemory=reservedMemory+size,目前保留堆外内存总空间的大小;
  • count:申请次数加1;
    // 参数解释:
    //     size:根据是否按页对齐,得到的真实需要申请的内存大小
    //     cap:用户指定需要的内存大小(<=size)
    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.
        //     totalCapacity:目前用户已经申请的总空间
        //     maxMemory:堆外内存设置最大值
        //     reservedMemory:真实的目前保留的堆外内存空间
        long totalCap;
        while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
            if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
                reservedMemory.addAndGet(size);
                count.incrementAndGet();
                return true;
            }
        }

        return false;
    }

调用AtmoticLong#compareAndSet(long expect, long update),最终实现通过Unsafe更新用户申请的总内存空间大小totalCapacity+cap,即unsafe.compareAndSwapLong(valueOffset,expect,update)


    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
 
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NettyBoy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值