ByteBuffer简要分析

本文详细介绍了Java NIO中ByteBuffer的两种分配方式:堆内存分配和直接内存分配,并重点解析了DirectByteBuffer的工作原理,包括如何使用Unsafe类进行直接内存的申请与释放。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ByteBuffer

allocate(int capacity)

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

​ HeapByteBuffer堆内存

allocateDirect(int capacity)

	在Java中,把内核空间的内存称之为直接内存,区别于堆内存,或者不归jvm管的内存,为了避免频繁的在用户空间与内核空间拷贝数据,通常会直接从内核空间中申请内存,nio包中的`ByteBuffer`的`allocateDirect`方法,就是帮助我们申请直接内存的,代码如下所示:
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

​ 在上述代码片段中,返回的是一个DirectByteBuffer对象,其是ByteBuffer的子类,对于直接内存的分配,就是在这个类中实现的。

java的内存申请和释放

​ 在java中,直接内存的申请与释放是通过Unsafe类的allocateMemory方法和freeMemory方法来实现的,且对于直接内存的释放,必须手工调用freeMemory方法,因为JVM只能帮我们管理堆内存,直接内存不在其管理范围之内

public native long allocateMemory(long var1);
public native void freeMemory(long var1);

DirectByteBuffer

​ DirectByteBuffer帮我们简化了直接内存的使用,我们不需要直接操作Unsafe类来进行直接内存的申请与释

申请

DirectByteBuffer(int cap)

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) {                   // package-private

    super(-1, 0, cap, cap);
    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 {
        base = unsafe.allocateMemory(size);//分配直接内存,base表示的是直接内存的开始地址
    } 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;
}

​ 构造方法中的确是用了unsafe.allocateMemory方法帮我们分配了直接内存,另外,在构造方法的最后,通过 Cleaner.create方法注册了一个钩子函数,用于清除直接内存的引用。

​ 因为直接内存不归JVM管控,所以设置一个钩子函数,在必要的时候释放内存

Bits

​ 在DirectByteBuffer实例创建的时候,分配内存之前调用了Bits.reserveMemory方法,如果分配失败调用了Bits.unreserveMemory,同时在Deallocator释放完直接内存的时候,也调用了Bits.unreserveMemory方法。

Bits.reserveMemory(size, cap);

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) {
                // 刚才执行了GC,所以再次看看内存是不是够分配
                if (tryReserveMemory(size, cap)) {
                    // 够分配了,直接返回
                    return;
                }
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                if (!jlra.tryHandlePendingReference()) {
                    try {
                //1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s)      
                        Thread.sleep(sleepTime); //休眠sleepTime秒,等待垃圾回收完成
                        sleepTime <<= 1;
                        sleeps++;
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }

            // no luck 没有足够的内存分配,所以OOM了
            throw new OutOfMemoryError("Direct buffer memory");

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

MAX_SLEEPS

// max. number of sleeps during try-reserving with exponentially
    // increasing delay before throwing OutOfMemoryError:
    // 1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s)
    // which means that OOME will be thrown after 0.5 s of trying
   // 九次之后就发生OOM了,亲
    private static final int MAX_SLEEPS = 9;

tryReserveMemory

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;
       //维护已经使用的直接内存的数量
        while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
            if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
                //如果直接有足够多的直接内存可以用,直接增加直接内存引用的计数
                reservedMemory.addAndGet(size);
                count.incrementAndGet();
                return true;
            }
        }

        return false;
    }

Bits.unreserveMemory(size, cap);

释放内存时,减少引用直接内存的计数

static void unreserveMemory(long size, int cap) {
    long cnt = count.decrementAndGet();
    long reservedMem = reservedMemory.addAndGet(-size);
    long totalCap = totalCapacity.addAndGet(-cap);
    assert cnt >= 0 && reservedMem >= 0 && totalCap >= 0;
}

总结

​ 我们事实上可以认为Bits类是直接内存的分配担保,当有足够的直接内存可以用时,增加直接内存应用计数,否则,调用System.gc,进行垃圾回收,需要注意的是,System.gc只会回收堆内存中的对象,但是我们前面已经讲过,DirectByteBuffer对象被回收时,那么其引用的直接内存也会被回收,试想现在刚好有其他的DirectByteBuffer可以被回收,那么其被回收的直接内存就可以用于本次DirectByteBuffer直接的内存的分配

释放

​ DirectByteBuffer本身是一个Java对象,其是位于堆内存中的,JDK的GC机制可以自动帮我们回收,但是其申请的直接内存,不在GC范围之内,无法自动回收。好在JDK提供了一种机制,可以为堆内存对象注册一个钩子函数(其实就是实现Runnable接口的子类),当堆内存对象被GC回收的时候,会回调run方法,我们可以在这个方法中执行释放DirectByteBuffer引用的直接内存,即在run方法中调用Unsafe 的freeMemory 方法。注册是通过sun.misc.Cleaner类来实现的

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

System.gc

​ 我们经常看到,有一些文章讲解在使用Nio的时候,不要禁用System.gc,也就是启动JVM的时候,不要传入-XX:+DisableExplicitGC参数,因为这样可能会造成直接内存溢出。道理很明显,因为直接内存的释放与获取比堆内存更加耗时,每次创建DirectByteBuffer实例分配直接内存的时候,都调用System.gc,可以让已经使用完的DirectByteBuffer得到及时的回收。

​ 虽然System.gc只是建议JVM去垃圾回收,可能JVM并不会立即回收,但是频繁的建议,JVM总不会视而不见。因为System.gc导致的是FullGC,可能会暂停用户线程,也就是JVM不能继续响应用户的请求,对于一些要求延时比较短的应用,是不希望JVM频繁的进行FullGC的,所以笔者的建议是:禁用System.gc,调大最大可以使用的直接内存

-XX:+DisableExplicitGC -XX:MaxDirectMemorySize=256M

本文主要参考博客:https://www.jianshu.com/p/d1b8fa9f3207 ,诸如侵权,请及时联系

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值