【Java源码阅读系列45】深度解读Java DirectByteBuffer 与 HeapByteBuffer 源码

Java NIO 中的 ByteBuffer 是处理字节数据的核心类,其两个重要子类 HeapByteBuffer(堆内存缓冲区)和 DirectByteBuffer(直接内存缓冲区)分别对应 JVM 堆内存和操作系统堆外内存的存储方式。本文将结合源码,深入解析两者的核心实现、关键方法及设计模式的应用,帮助开发者理解其差异与适用场景。

一、核心定位:堆内存 vs 直接内存

HeapByteBufferDirectByteBuffer 均继承自 ByteBuffer,但底层存储方式不同:

  • HeapByteBuffer:基于 JVM 堆内存的 byte[] 数组存储,数据在用户空间(JVM 堆)中,创建和回收由 JVM 自动管理(GC 回收)。
  • DirectByteBuffer:基于操作系统堆外内存(Off-Heap),数据存储在 JVM 堆外,需手动管理或通过 Cleaner 机制回收,避免了用户空间与内核空间的拷贝(零拷贝)。

二、构造方法:内存分配的底层逻辑

1. HeapByteBuffer 的构造

HeapByteBuffer 的构造方法直接初始化 byte[] 数组,数据存储在 JVM 堆中。

// HeapByteBuffer.java(部分源码)
// 构造方法 1:分配新的 byte 数组
HeapByteBuffer(int cap, int lim) {
    super(-1, 0, lim, cap, new byte[cap], 0);
}

// 构造方法 2:基于现有 byte 数组
HeapByteBuffer(byte[] buf, int off, int len) {
    super(-1, off, off + len, buf.length, buf, 0);
}
  • 逻辑:通过 new byte[cap] 创建堆内存数组,或直接包装现有数组。
  • 特点:内存分配由 JVM 完成,GC 自动回收,无需手动管理。

2. DirectByteBuffer 的构造

DirectByteBuffer 的构造通过 UnsafeJNI 直接分配堆外内存,底层使用指针操作。

// DirectByteBuffer.java(伪代码,实际通过 Unsafe 实现)
DirectByteBuffer(int capacity) {
    super(-1, 0, capacity, capacity);
    this.address = unsafe.allocateMemory(capacity); // 分配堆外内存
    unsafe.setMemory(this.address, capacity, (byte) 0); // 初始化内存为 0
    cleaner = Cleaner.create(this, new Deallocator(address, capacity)); // 注册 Cleaner 回收内存
}
  • 逻辑:通过 Unsafe.allocateMemory 分配堆外内存,记录内存地址(address),并通过 Cleaner 注册内存释放任务(避免内存泄漏)。
  • 特点:内存分配在堆外,需手动或通过 Cleaner 回收(Cleaner 在对象被 GC 时触发内存释放)。

三、关键方法对比:数据读写与内存操作

1. 基础读写操作:get()put()

HeapByteBufferDirectByteBufferget()/put() 方法均基于 Buffer 类的状态变量(positionlimit),但底层实现差异显著。

(1) HeapByteBuffer 的实现

// HeapByteBuffer.java
public byte get() {
    return hb[ix(nextGetIndex())]; // 直接访问 byte 数组
}

public ByteBuffer put(byte x) {
    hb[ix(nextPutIndex())] = x; // 直接修改 byte 数组
    return this;
}
  • 逻辑:通过 ix(i) 计算数组索引(i + offset),直接读写 byte[] 数组。
  • 特点:操作堆内存数组,速度快但需用户空间与内核空间的拷贝(IO 时)。

(2) DirectByteBuffer 的实现

// DirectByteBuffer.java(伪代码)
public byte get() {
    return unsafe.getByte(address + nextGetIndex()); // 直接读取堆外内存地址
}

public ByteBuffer put(byte x) {
    unsafe.putByte(address + nextPutIndex(), x); // 直接写入堆外内存地址
}
  • 逻辑:通过 Unsafe 操作堆外内存地址(address + position),直接读写操作系统内存。
  • 特点:避免了用户空间与内核空间的拷贝(零拷贝),适合高频 IO 操作。

2. 缓冲区压缩:compact()

compact()方法用于将未读取的数据移动到缓冲区头部,以便继续写入新数据。

(1) HeapByteBuffer 的实现

// HeapByteBuffer.java
public ByteBuffer compact() {
    System.arraycopy(hb, ix(position()), hb, ix(0), remaining()); // 数组拷贝
    position(remaining()); // position = limit - position(未读数据长度)
    limit(capacity());     // limit = capacity(恢复写模式)
    discardMark();         // 丢弃标记
    return this;
}
  • 逻辑:使用 System.arraycopy 将未读数据(positionlimit-1)拷贝到数组头部(索引 0 开始)。
  • 特点:基于堆内存数组的拷贝,时间复杂度 O(n),适合小数据量场景。

(2) DirectByteBuffer 的实现

// DirectByteBuffer.java(伪代码)
public ByteBuffer compact() {
    int pos = position();
    int lim = limit();
    int rem = lim - pos;
    // 堆外内存拷贝(通过 Unsafe 或 JNI)
    unsafe.copyMemory(address + pos, address, rem);
    position(rem);
    limit(capacity());
    discardMark();
    return this;
}
  • 逻辑:通过 Unsafe.copyMemory 直接拷贝堆外内存块,避免了堆内存数组的中间拷贝。
  • 特点:内存拷贝效率更高(底层由操作系统优化),适合大数据量高频压缩场景。

3. 视图缓冲区:asCharBuffer()

两者均支持通过视图方法(如 asCharBuffer())创建其他类型的缓冲区,共享底层字节数据。

// HeapByteBuffer.java
public CharBuffer asCharBuffer() {
    int size = remaining() >> 1; // 每个 char 占 2 字节
    int off = offset + position();
    // 根据字节序(大端/小端)创建不同视图
    return (bigEndian 
            ? new ByteBufferAsCharBufferB(this, -1, 0, size, size, off)
            : new ByteBufferAsCharBufferL(this, -1, 0, size, size, off));
}
  • 逻辑:根据当前缓冲区的 positionlimit 和字节序(bigEndian),创建 CharBuffer 视图,共享底层字节数据。
  • 特点:视图与原缓冲区共享数据,修改视图会影响原缓冲区(适合数据格式转换)。

四、设计模式解析:模板方法与工厂方法的协同

1. 模板方法模式(Template Method Pattern)

ByteBuffer 抽象类定义了缓冲区的通用骨架(如 position()limit()flip()等方法),并声明了抽象方法(如 isDirect()isReadOnly()),具体实现由 HeapByteBufferDirectByteBuffer 完成。这种模式将公共逻辑(状态管理)封装在基类,子类只需关注具体数据操作,符合“开闭原则”。

// ByteBuffer.java(抽象基类)
public abstract boolean isDirect(); // 抽象方法,由子类实现

// HeapByteBuffer.java(子类实现)
public boolean isDirect() {
    return false; // 堆缓冲区非直接内存
}

// DirectByteBuffer.java(子类实现)
public boolean isDirect() {
    return true; // 直接缓冲区
}

2. 工厂方法模式(Factory Method Pattern)

ByteBuffer 通过静态工厂方法 allocate()allocateDirect() 创建实例,隐藏了具体子类(HeapByteBufferDirectByteBuffer)。客户端只需调用工厂方法,无需关心底层存储方式,体现了“依赖倒置原则”。

// ByteBuffer.java
public static ByteBuffer allocate(int capacity) {
    return new HeapByteBuffer(capacity, capacity); // 返回堆缓冲区
}

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity); // 返回直接缓冲区
}

五、核心差异与适用场景

特性HeapByteBufferDirectByteBuffer
存储位置JVM 堆内存(byte[] 数组)操作系统堆外内存(指针地址)
内存管理GC 自动回收手动或通过 Cleaner 回收(易泄漏)
IO 效率需用户空间 ↔ 内核空间拷贝(效率低)零拷贝(效率高)
创建/回收成本低(JVM 直接分配数组)高(涉及系统调用和 Cleaner 注册)
适用场景小数据量、短生命周期的 IO 操作大数据量、高频 IO(如网络通信、文件读写)

六、总结

HeapByteBufferDirectByteBuffer 是 Java NIO 中两种核心的字节缓冲区实现,分别适用于不同的 IO 场景。HeapByteBuffer 依赖 JVM 堆内存,管理简单但 IO 效率较低;DirectByteBuffer 依赖堆外内存,IO 效率高但管理复杂。理解两者的源码差异与设计模式(模板方法、工厂方法),有助于开发者根据实际需求选择合适的缓冲区类型,优化 IO 性能。

在实际开发中,建议:

  • 小数据量、短生命周期的场景使用 HeapByteBuffer(如日志输出);
  • 大数据量、高频 IO 场景使用 DirectByteBuffer(如网络框架、文件读写);
  • 注意 DirectByteBuffer 的内存泄漏问题(及时调用 clean() 或依赖 Cleaner 回收)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值