JDK 源码阅读 : DirectByteBuffer

本文分析了DirectByteBuffer的实现,它是堆外ByteBuffer,是NIO高性能的核心设计之一。介绍了其使用方法、实例化流程、回收流程、读写逻辑,还探讨了默认可申请的堆外内存大小,指出默认值为Java最大堆大小,最后说明了与之相关的JVM选项。

在文章JDK源码阅读-ByteBuffer中,我们学习了ByteBuffer的设计。但是他是一个抽象类,真正的实现分为两类:HeapByteBuffeDirectByteBufferHeapByteBuffer是堆内ByteBuffer,使用byte[]存储数据,是对数组的封装,比较简DirectByteBuffer是堆外ByteBuffer,直接使用堆外内存空间存储数据,是NIO高性能的核心设计之一。本文来分析一下DirectByteBuffer的实现。

如何使用DirectByteBuffer

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

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

DirectByteBuffer实例化流程

我们来看一下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方法分配内存
        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)) {
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    // 使用Cleaner机制注册内存回收处理函数
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

申请内存前会调用java.nio.Bits#reserveMemory判断是否有足够的空间可供申请:

// 该方法主要用于判断申请的堆外内存是否超过了用例指定的最大值
// 如果还有足够空间可以申请,则更新对应的变量
// 如果已经没有空间可以申请,则抛出OOME
// 参数解释:
//     size:根据是否按页对齐,得到的真实需要申请的内存大小
//     cap:用户指定需要的内存大小(<=size)
static void reserveMemory(long size, int cap) {
    // 因为涉及到更新多个静态统计变量,这里需要Bits类锁
    synchronized (Bits.class) {
        // 获取最大可以申请的对外内存大小,默认值是64MB
        // 可以通过参数-XX:MaxDirectMemorySize=<size>设置这个大小
        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }
        // -XX:MaxDirectMemorySize限制的是用户申请的大小,而不考虑对齐情况
        // 所以使用两个变量来统计:
        //     reservedMemory:真实的目前保留的空间
        //     totalCapacity:目前用户申请的空间
        if (cap <= maxMemory - totalCapacity) {
            reservedMemory += size;
            totalCapacity += cap;
            count++;
            return; // 如果空间足够,更新统计变量后直接返回
        }
    }
 
    // 如果已经没有足够空间,则尝试GC
    System.gc();
    try {
        Thread.sleep(100);
    } catch (InterruptedException x) {
        // Restore interrupt status
        Thread.currentThread().interrupt();
    }
    synchronized (Bits.class) {
        // GC后再次判断,如果还是没有足够空间,则抛出OOME
        if (totalCapacity + cap > maxMemory)
            throw new OutOfMemoryError("Direct buffer memory");
        reservedMemory += size;
        totalCapacity += cap;
        count++;
    }
}

在java.nio.Bits#reserveMemory方法中,如果空间不足,会调用System.gc()尝试释放内存,然后再进行判断,如果还是没有足够的空间,抛出OOME。

如果分配失败,则需要把预留的统计变量更新回去:

static synchronized void unreserveMemory(long size, int cap) {
           if (reservedMemory > 0) {
               reservedMemory -= size;
               totalCapacity -= cap;
               count--;
               assert (reservedMemory > -1);
           }
       }

从上面几个函数中我们可以得到信息:

  1. 可以通过-XX:+PageAlignDirectMemor参数控制堆外内存分配是否需要按页对齐,默认不对齐。
  2. 每次申请和释放需要调用调用Bits的reserveMemory或unreserveMemory方法,这两个方法根据内部维护的统计变量判断当前是否还有足够的空间可供申请,如果有足够的空间,更新统计变量,如果没有足够的空间,调用System.gc()尝试进行垃圾回收,回收后再次进行判断,如果还是没有足够的空间,抛出OOME。
  3. Bits的reserveMemory方法判断是否有足够内存不是判断物理机是否有足够内存,而是判断JVM启动时,指定的堆外内存空间大小是否有剩余的空间。这个大小由参数-XX:MaxDirectMemorySize=设置。
  4. 确定有足够的空间后,使用sun.misc.Unsafe#allocateMemory申请内存
    申请后的内存空间会被清零
  5. DirectByteBuffer使用Cleaner机制进行空间回收
  6. 可以看出除了判断是否有足够的空间的逻辑外,核心的逻辑是调用

sun.misc.Unsafe#allocateMemory申请内存,我们看一下这个函数是如何申请对外内存的:

// 申请一块本地内存。内存空间是未初始化的,其内容是无法预期的。
// 使用freeMemory释放内存,使用reallocateMemory修改内存大小
public native long allocateMemory(long bytes);

————————————————————————————————————

// openjdk8/hotspot/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
  UnsafeWrapper("Unsafe_AllocateMemory");
  size_t sz = (size_t)size;
  if (sz != (julong)size || size < 0) {
    THROW_0(vmSymbols::java_lang_IllegalArgumentException());
  }
  if (sz == 0) {
    return 0;
  }
  sz = round_to(sz, HeapWordSize);
  // 调用os::malloc申请内存,内部使用malloc函数申请内存
  void* x = os::malloc(sz, mtInternal);
  if (x == NULL) {
    THROW_0(vmSymbols::java_lang_OutOfMemoryError());
  }
  //Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
  return addr_to_java(x);
UNSAFE_END

可以看出sun.misc.Unsafe#allocateMemory使用malloc这个C标准库的函数来申请内存。

DirectByteBuffer回收流程

在DirectByteBuffer的构造函数的最后,我们看到了这样的语句:

// 使用Cleaner机制注册内存回收处理函数
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

这是使用Cleaner机制进行内存回收。因为DirectByteBuffer申请的内存是在堆外,DirectByteBuffer本身支持保存了内存的起始地址而已,所以DirectByteBuffer的内存占用是由堆内的DirectByteBuffer对象与堆外的对应内存空间共同构成。堆内的占用只是很小的一部分,这种对象被称为冰山对象。

堆内的DirectByteBuffer对象本身会被垃圾回收正常的处理,但是对外的内存就不会被GC回收了,所以需要一个机制,在DirectByteBuffer回收时,同时回收其堆外申请的内存。

Java中可选的特性有finalize函数,但是finalize机制是Java官方不推荐的,官方推荐的做法是使用虚引用来处理对象被回收时的后续处理工作,可以参考JDK源码阅读-Reference。同时Java提供了Cleaner类来简化这个实现,Cleaner是PhantomReference的子类,可以在PhantomReference被加入ReferenceQueue时触发对应的Runnable回调。

DirectByteBuffer就是使用Cleaner机制来实现本身被GC时,回收堆外内存的能力。我们来看一下其回收处理函数是如何实现的:

private static class Deallocator
    implements Runnable
    {
 
        private static Unsafe unsafe = Unsafe.getUnsafe();
 
        private long address;
        private long size;
        private int capacity;
 
        private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }
 
        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            // 使用unsafe方法释放内存
            unsafe.freeMemory(address);
            address = 0;
            // 更新统计变量
            Bits.unreserveMemory(size, capacity);
        }
 
    }

sun.misc.Unsafe#freeMemory方法使用C标准库的free函数释放内存空间。同时更新Bits类中的统计变量。

DirectByteBuffer读写逻辑

public ByteBuffer put(int i, byte x) {
    unsafe.putByte(ix(checkIndex(i)), ((x)));
    return this;
}
 
public byte get(int i) {
    return ((unsafe.getByte(ix(checkIndex(i)))));
}
 
private long ix(int i) {
    return address + (i << 0);
}

DirectByteBuffer使用sun.misc.Unsafe#getByte(long)和sun.misc.Unsafe#putByte(long, byte)这两个方法来读写堆外内存空间的指定位置的字节数据。不过这两个方法本地实现比较复杂,这里就不分析了。

默认可以申请的堆外内存大小

上文提到了DirectByteBuffer申请内存前会判断是否有足够的空间可供申请,这个是在一个指定的堆外大小限制的前提下。用户可以通过-XX:MaxDirectMemorySize=这个参数来控制可以申请多大的DirectByteBuffer内存。但是默认情况下这个大小是多少呢?

DirectByteBuffer通过sun.misc.VM#maxDirectMemory来获取这个值,可以看一下对应的代码:

// A user-settable upper limit on the maximum amount of allocatable direct
// buffer memory.  This value may be changed during VM initialization if
// "java" is launched with "-XX:MaxDirectMemorySize=<size>".
//
// The initial value of this field is arbitrary; during JRE initialization
// it will be reset to the value specified on the command line, if any,
// otherwise to Runtime.getRuntime().maxMemory().
//
private static long directMemory = 64 * 1024 * 1024;
 
// Returns the maximum amount of allocatable direct buffer memory.
// The directMemory variable is initialized during system initialization
// in the saveAndRemoveProperties method.
//
public static long maxDirectMemory() {
    return directMemory;
}

这里directMemory默认赋值为64MB,那对外内存的默认大小是64MB吗?不是,仔细看注释,注释中说,这个值会在JRE启动过程中被重新设置为用户指定的值,如果用户没有指定,则会设置为Runtime.getRuntime().maxMemory()。

这个过程发生在sun.misc.VM#saveAndRemoveProperties函数中,这个函数会被java.lang.System#initializeSystemClass调用:

public static void saveAndRemoveProperties(Properties props) {
    if (booted)
        throw new IllegalStateException("System initialization has completed");
 
    savedProps.putAll(props);
 
    // Set the maximum amount of direct memory.  This value is controlled
    // by the vm option -XX:MaxDirectMemorySize=<size>.
    // The maximum amount of allocatable direct buffer memory (in bytes)
    // from the system property sun.nio.MaxDirectMemorySize set by the VM.
    // The system property will be removed.
    String s = (String)props.remove("sun.nio.MaxDirectMemorySize");
    if (s != null) {
        if (s.equals("-1")) {
            // -XX:MaxDirectMemorySize not given, take default
            directMemory = Runtime.getRuntime().maxMemory();
        } else {
            long l = Long.parseLong(s);
            if (l > -1)
                directMemory = l;
        }
    }
 
    //...
}

所以默认情况下,可以申请的DirectByteBuffer大小为Runtime.getRuntime().maxMemory(),而这个值等于可用的最大Java堆大小,也就是我们-Xmx参数指定的值。

所以最终结论是:默认情况下,可以申请的最大DirectByteBuffer空间为Java最大堆大小的值。

和DirectByteBuffer有关的JVM选项

根据上文的分析,有两个JVM参数与DirectByteBuffer直接相关:

  • -XX:+PageAlignDirectMemory:指定申请的内存是否需要按页对齐,默认不对其
  • -XX:MaxDirectMemorySize=,可以申请的最大DirectByteBuffer大小,默认与-Xmx相等

(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程等待你来学习。完全免费哦! )

分析并解决问题: WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by org.apache.rocketmq.store.MappedFile (file:/usr/local/rocketmq/lib/rocketmq-store-4.9.6.jar) to method java.nio.DirectByteBuffer.attachment() WARNING: Please consider reporting this to the maintainers of org.apache.rocketmq.store.MappedFile WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release Exception in thread "ShutdownHook" java.lang.IllegalStateException: java.lang.reflect.InaccessibleObjectException: Unable to make public void jdk.internal.ref.Cleaner.clean() accessible: module java.base does not "exports jdk.internal.ref" to unnamed module @710636b0 at org.apache.rocketmq.store.MappedFile.lambda$invoke$0(MappedFile.java:116) at java.base/java.security.AccessController.doPrivileged(Native Method) at org.apache.rocketmq.store.MappedFile.invoke(MappedFile.java:110) at org.apache.rocketmq.store.MappedFile.clean(MappedFile.java:106) at org.apache.rocketmq.store.StoreCheckpoint.shutdown(StoreCheckpoint.java:69) at org.apache.rocketmq.store.DefaultMessageStore.shutdown(DefaultMessageStore.java:335) at org.apache.rocketmq.broker.BrokerController.shutdown(BrokerController.java:766) at org.apache.rocketmq.broker.BrokerStartup$1.run(BrokerStartup.java:237) at java.base/java.lang.Thread.run(Thread.java:834) Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make public void jdk.internal.ref.Cleaner.clean() accessible: module java.base does not "exports jdk.internal.ref" to unnamed module @710636b0 at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:340) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:280) at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:198) at java.base/java.lang.reflect.Method.setAccessible(Method.java:192) at org.apache.rocketmq.store.MappedFile.lambda$invoke$0(MappedFile.java:113) ... 8 more
最新发布
08-02
JDK 9及更高版本中,Java引入了模块系统(JPMS),对反射访问进行了更严格的限制,导致了一些库(如RocketMQ)在使用某些内部API时出现`IllegalStateException`或`InaccessibleObjectException`等异常。这类问题通常与非法反射访问有关,尤其是在尝试访问`DirectByteBuffer`的`cleaner`字段时。 ### 问题分析 RocketMQ在某些版本中使用了Java NIO的`DirectByteBuffer`对象,并通过反射访问其内部的`cleaner`字段,以便手动触发内存回收。在JDK 9之前,这种访问可以通过`setAccessible(true)`绕过访问控制检查。然而,从JDK 9开始,模块系统引入了更强的封装机制,使得对`java.base`模块中类的反射访问受到限制,从而引发类似以下的异常: ``` java.lang.IllegalStateException: java.lang.reflect.InaccessibleObjectException: Unable to make field private java.nio.DirectByteBuffer$Deallocator java.nio.DirectByteBuffer.attachment accessible ``` 该异常表明尝试访问`DirectByteBuffer`中的`attachment`字段失败,因为模块系统不允许外部代码访问该字段[^3]。 ### 解决方案 #### 1. 使用JVM启动参数放宽访问限制 可以通过在JVM启动时添加`--add-opens`参数,允许特定模块对内部类进行反射访问。例如: ```shell --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED ``` 这些参数将允许未命名模块中的代码访问`java.nio`和`java.lang`包中的类,从而避免`InaccessibleObjectException`异常。该方法适用于RocketMQ等依赖反射访问的旧有代码库,无需修改源码即可运行在JDK 9+环境中。 #### 2. 升级RocketMQ版本 检查是否使用的是支持JDK 11及以上版本的RocketMQ版本。从4.5.0版本开始,社区逐步增强了对JDK 11的支持,部分反射访问问题已在后续版本中修复。建议升级至4.9.0或更高版本以获得更好的兼容性。 #### 3. 替代方式释放DirectByteBuffer内存 如果希望避免使用反射操作`DirectByteBuffer`,可以考虑通过JNA调用本地库(如`LibC`接口)进行内存管理。例如,RocketMQ使用了`net.java.dev.jna:jna:4.2.2`库,并定义了`LibC`接口来调用`mlock`、`munlock`、`madvise`等函数[^2]。这种方式可以替代反射机制,减少对JDK内部API的依赖。 示例代码如下: ```java import com.sun.jna.Native; import com.sun.jna.Pointer; public class MemoryUtil { public static void lockMemory(Pointer ptr, long size) { LibC.INSTANCE.mlock(ptr, new NativeLong(size)); } public static void unlockMemory(Pointer ptr, long size) { LibC.INSTANCE.munlock(ptr, new NativeLong(size)); } } ``` #### 4. 使用Java API替代反射访问 在某些情况下,可以使用`java.lang.ref.Cleaner`类(JDK 9+)替代直接访问`DirectByteBuffer`的`cleaner`字段。通过注册一个`Cleaner`实例,可以在对象被垃圾回收时自动执行清理逻辑,而无需依赖反射。 ```java import java.lang.ref.Cleaner; import java.nio.ByteBuffer; public class BufferCleaner { private static final Cleaner cleaner = Cleaner.create(); public static void cleanDirectBuffer(ByteBuffer buffer) { if (buffer.isDirect()) { cleaner.register(buffer, () -> { // 执行清理逻辑,如调用JNI方法释放内存 System.out.println("Cleaning direct buffer..."); }); } } } ``` ### 小结 上述方法可以有效解决RocketMQ在JDK 9及以上版本中因非法反射访问导致的运行时异常。推荐优先使用JVM参数或升级至兼容版本,若需深度定制内存管理逻辑,可结合JNA或`Cleaner`类实现更安全的替代方案。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值