JDK 源码阅读 : DirectByteBuffer

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

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

在文章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社区,更多行业相关资讯更有行业相关免费视频教程等待你来学习。完全免费哦! )

<think>好的,我现在要解决用户的问题,用户提到他们的Java程序在运行时突然出现了hs_error_pid错误,也就是通常所说的JVM崩溃,生成了hs_err_pid<pid>.log文件。我需要先理解这个问题的原因,然后给出解决步骤。 首先,我得回忆一下JVM崩溃的常见原因。通常,这涉及到本地代码问题,比如JNI调用、内存不足、硬件或操作系统问题,或者JVM自身的bug。用户可能需要检查这些方面。 接下来,我需要分解解决问题的步骤。第一步应该是查看错误日志,因为里面会有关键信息,比如信号、问题发生的线程和堆栈跟踪。然后用户需要检查是否有堆栈溢出或OOM,不过OOM通常不会导致JVM崩溃,除非本地内存耗尽。 然后要考虑是否有JNI代码,如果有的话,可能是本地代码的问题,比如内存越界。这时候需要检查JNI代码,用Valgrind等工具排查。另外,硬件问题如内存损坏、磁盘故障或者CPU过热也可能导致崩溃,建议用户运行硬件诊断。 还有可能是操作系统的问题,比如资源限制或内核bug,这时候更新操作系统可能有帮助。JVM本身的bug虽然少见,但也不能排除,用户需要检查是否使用了过时的JDK版本,并考虑升级。 最后,如果用户无法自行解决,应该收集日志、核心转储和环境信息,联系Java支持或社区寻求帮助。整个过程中,保持回答结构清晰,分步骤引导用户排查问题,同时确保使用正确的LaTeX格式,比如行内公式用$...$,独立公式用$$...$$,但这个问题中可能不太需要数学公式,所以可能用不上。但需要确保中文回答,语法正确,并且符合用户可见层的指导方针。</think>当Java程序运行中突然出现`hs_error_pid`错误时,表明JVM发生了致命错误并崩溃,通常伴随生成名为`hs_err_pid<pid>.log`的日志文件。以下是逐步分析及解决方法: --- ### 1. **理解错误本质** - **触发原因**:JVM遇到无法恢复的错误(如内存访问冲突、堆栈溢出、本地代码问题等)。 - **日志文件**:生成的`hs_err_pid*.log`包含详细崩溃信息,如寄存器状态、线程堆栈、环境变量等。 --- ### 2. **关键排查步骤** #### **步骤1:查看错误日志** - **定位日志**:找到日志文件(默认在程序运行目录),检查以下部分: ```log # Problematic frame: [C 或 [jvm.dll] 等 # SIGSEGV (0xb) 或 SIGBUS 等信号 ``` - **信号类型**: - `SIGSEGV`:内存访问违规(如空指针、缓冲区溢出)。 - `SIGBUS`:内存对齐错误(常见于JNI代码)。 - `EXCEPTION_ACCESS_VIOLATION`(Windows):类似SIGSEGV。 #### **步骤2:检查代码与资源** - **堆栈溢出**:若日志中线程堆栈显示`java.lang.StackOverflowError`,需优化递归或循环调用。 - **JNI代码**:若使用了本地方法(JNI),检查C/C++代码的内存操作(如野指针、未初始化指针)。 - **资源泄漏**:检查文件句柄、数据库连接等是否未正确释放。 #### **步骤3:内存与硬件问题** - **物理内存不足**:本地内存耗尽可能导致崩溃,需监控内存使用(如`jstat -gc <pid>`)。 - **硬件故障**:运行内存测试工具(如`memtest86`)排除内存损坏。 - **过热或电源不稳**:检查服务器/设备温度及供电状态。 #### **步骤4:环境与依赖** - **JVM版本**:升级至最新稳定版JDK(如OpenJDK 17 LTS),避免已知Bug。 - **操作系统兼容性**:确保JVM与操作系统架构(32/64位)匹配。 - **三方库冲突**:检查依赖库(尤其本地库)版本兼容性。 #### **步骤5:调试工具辅助** - **核心转储分析**(Linux): ```bash gdb /path/to/java /path/to/core_dump bt # 查看崩溃时的调用栈 ``` - **Valgrind检查JNI**: ```bash valgrind --tool=memcheck java -jar YourApp.jar ``` --- ### 3. **典型解决方案** | 场景 | 解决方法 | |---------------------|-------------------------------------| | JNI代码内存越界 | 使用`AddressSanitizer`或重构本地代码 | | 堆外内存泄漏(如DirectByteBuffer) | 限制`-XX:MaxDirectMemorySize` | | JVM自身Bug | 降级或升级JDK版本 | | 操作系统资源限制 | 调整`ulimit`或内核参数 | --- ### 4. **示例日志片段分析** 假设日志中包含: ```log Problematic frame: C [libnative.so+0x1234] ``` 表明崩溃发生在动态库`libnative.so`的某个地址,需优先检查JNI代码或第三方本地库。 --- ### 5. **预防措施** - **压力测试**:使用JMeter模拟高负载场景。 - **启用JVM参数**: ```bash -XX:+ErrorFileToStderr # 输出错误日志到控制台 -XX:+CrashOnOutOfMemoryError # OOM时主动崩溃生成日志 ``` 若问题仍无法解决,建议将日志文件与完整环境信息提交至[JVM Bug数据库](https://bugs.openjdk.org/)或社区寻求帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值