NIO:Buffer对象均是在Jvm堆中分配么?听说过DirectByteBuffer和MappedByteBuffer么?

好的!根据你的建议,我对博客进行了润色,调整为更口语化的语言,明确点出堆外内存的好处和坏处并解答"为什么不全部用堆外",同时补充了堆外内存的典型应用场景。以下是优化后的版本:


聊聊 Java NIO:八股文考点和那些冷门细节

Java NIO(New I/O)是 Java 里搞高性能 I/O 的利器,比起老式的 BIO(阻塞 I/O),它多了非阻塞、缓冲区和通道这些新花样,尤其在高并发场景(比如 Netty)里特别受欢迎。面试里,NIO 常被拎出来当"八股文"问,尤其是 Buffer、Channel、Selector 这三大件。今天咱们就聊聊这些常见考点,顺便挖一挖冷门但硬核的细节,比如 Buffer 的各种类型,还有 ByteBuffer 里堆外内存那点事儿,保证讲得细致又接地气!

一、NIO 三大件和面试常问啥

先简单过一遍 NIO 的核心,再看看面试里爱问啥,铺个底。

1. NIO 的三大件

  • Buffer(缓冲区):存数据的"仓库",大小固定。
  • Channel(通道):负责数据传进传出,比 BIO 的流高级,能双向操作。
  • Selector(选择器):多路复用的核心,一个线程就能盯着好多 Channel。

2. 面试"八股文"有哪些?

  • NIO 跟 BIO 有啥不一样?
  • Buffer、Channel、Selector 分别是干嘛的?
  • ByteBuffer 的基本操作(put、get、flip、clear)咋用?
  • NIO 怎么搞非阻塞?
  • DirectByteBuffer 和 HeapByteBuffer 有啥区别?

这些问题听多了耳朵起茧,但真要答得漂亮,还得懂点冷门细节。下面咱们就重点聊聊 Buffer,尤其是 ByteBuffer 的堆外内存,绝对有料!


二、Buffer 不止 ByteBuffer,还有一大家子

提到 Buffer,很多人第一反应就是 ByteBuffer,其实 NIO 里 Buffer 是个大家族,种类多得可能超乎你想象,这也是个容易被忽略的考点。

1. Buffer 的七兄弟

NIO 给基本数据类型(除了 boolean)都配了专属 Buffer:

  • ByteBuffer:字节缓冲区,老大,啥都能干。
  • CharBuffer:字符缓冲区。
  • ShortBuffer:短整型缓冲区。
  • IntBuffer:整型缓冲区。
  • LongBuffer:长整型缓冲区。
  • FloatBuffer:浮点型缓冲区。
  • DoubleBuffer:双精度浮点型缓冲区。

这些家伙都有共同的"家规":capacity(总大小)、limit(读写上限)、position(当前指针)、mark(标记),还得遵守 mark <= position <= limit <= capacity

2. ByteBuffer 为啥这么牛?

ByteBuffer 是"万金油",底层是个字节数组,能通过 asXxxBuffer() 变身成其他 Buffer。比如:

java 复制代码
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
IntBuffer intBuffer = byteBuffer.asIntBuffer();

这招变身其实是同一个内存块的"视角切换",不拷数据,效率贼高。

3. 冷门选手:MappedByteBuffer

除了上面七个,还有个隐藏高手------MappedByteBuffer 。它是 ByteBuffer 的"亲儿子",通过 FileChannel.map() 搞出来,能把文件直接映射到内存里,省去拷贝步骤,实现"零拷贝"。这家伙跟堆外内存关系密切,后面会细聊。


三、ByteBuffer 的堆外内存:DirectByteBuffer 解密

ByteBuffer 有两种玩法:allocate() 弄个 HeapByteBuffer(堆内内存),allocateDirect() 弄个 DirectByteBuffer(堆外内存)。堆外内存是 NIO 的硬核特性,面试里也爱拿来深挖,咱们这就拆开看看。

1. 堆内内存和堆外内存有啥不一样?

  • 堆内内存(HeapByteBuffer)

    • ByteBuffer.allocate() 创建。

    • 内存就在 JVM 堆里,GC(垃圾回收)管着。

    • 底层是个 byte[] 数组,源码里长这样:

      java 复制代码
      public static ByteBuffer allocate(int capacity) {
          return new HeapByteBuffer(capacity, capacity);
      }
    • 好处:简单,GC 自动收拾。

    • 坏处:干 I/O 时,数据得从 JVM 堆拷到系统内核,慢吞吞。

  • 堆外内存(DirectByteBuffer)

    • ByteBuffer.allocateDirect() 创建。
    • 内存跑到了 JVM 外面(本地内存),不受堆大小限制。
    • 底层靠 sun.misc.Unsafe 或 JNI 调用系统分配内存。
    • 好处:I/O 时直接跟内核打交道,少了一次拷贝,快不少。
    • 坏处:自己管内存,搞不好就漏了。

2. DirectByteBuffer 咋来的?

看看源码,堆外内存是怎么诞生的:

java 复制代码
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

DirectByteBuffer(int cap) {
    super(-1, 0, cap, cap);
    long size = cap; // 简化版,实际会考虑页面对齐
    Bits.reserveMemory(size, cap); // 预留内存额度
    long address = unsafe.allocateMemory(size); // 用 Unsafe 分配堆外内存
    unsafe.setMemory(address, size, (byte)0); // 清零初始化
    this.address = address; // 记下地址
    cleaner = Cleaner.create(this, new Deallocator(address, size, cap)); // 安排回收
}
  • unsafe.allocateMemory:调用系统内存分配,硬核得很。
  • address:堆外内存的"门牌号",读写全靠它。
  • Cleaner:一个"保洁员",等对象没人用时帮忙扫掉堆外内存。

3. 堆外内存咋读写?

堆外内存的操作直接怼地址,比如 put

java 复制代码
public ByteBuffer put(byte x) {
    unsafe.putByte(address + position, x);
    position++;
    return this;
}

HeapByteBuffer 是写 byte[],DirectByteBuffer 是直接写内存地址,差距就在这。

4. 堆外内存咋收拾?

堆外内存不归 GC 管,但 DirectByteBuffer 靠 Cleaner 曲线救国:

  • 对象本身在堆里,GC 能扫到。
  • 对象没引用时,Cleaner 上场,调用 unsafe.freeMemory(address) 释放内存。

冷门坑 :如果开了 -XX:+DisableExplicitGCSystem.gc() 失效,堆外内存可能堆积到爆,抛个 OutOfMemoryError

5. 堆外内存的好处和坏处

  • 好处
    • :I/O 少拷贝一步,性能飞起。
    • :不受 JVM 堆限制,想多大有多大(系统内存够就行)。
    • 共享:进程间能直接用,比如内存映射。
  • 坏处
    • 麻烦:内存自己管,漏了不好查。
    • :分配和释放比堆内贵。
    • 局限:适合存简单数据,复杂对象不方便。

那为啥不全用堆外内存?

堆外内存听着很香,但它不适合所有场景。分配和释放成本高,管理麻烦,GC 还管不着,稍微不注意就内存泄漏。而且 JVM 堆内内存有 GC 兜底,存对象、调试都方便,堆外内存更适合 I/O 这种特定活儿。全用堆外就像拿大炮打蚊子,费劲还不一定好使。


四、堆外内存的应用场景

堆外内存不是摆设,现实里用得挺多:

  1. 网络框架:像 Netty,用 DirectByteBuffer 收发数据,快且稳定。
  2. 大文件处理:MappedByteBuffer 映射大文件,省内存还高效。
  3. 数据库引擎:有些数据库(比如 RocksDB)用堆外内存存索引或缓存,减少 GC 压力。
  4. 高性能计算:需要跟硬件直接交互时,堆外内存是标配。

五、MappedByteBuffer:堆外内存的"文件版"

MappedByteBuffer 是 DirectByteBuffer 的"升级版",专门干文件映射的活儿。比如:

java 复制代码
FileChannel channel = new RandomAccessFile("test.txt", "rw").getChannel();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
  • 零拷贝:文件直接映射到内存,内核和用户态无缝衔接。
  • 堆外内存:底层还是堆外,Cleaner 负责收拾。
  • 冷门细节force() 方法能强制刷数据到磁盘,操作系统页缓存的玩法。

六、总结:八股文+冷门点,拿下 NIO

NIO 的"八股文"考点不难背,但想答出彩,得懂点冷门细节。Buffer 的七种类型、ByteBuffer 的堆外内存、DirectByteBuffer 的分配回收,还有 MappedByteBuffer 的文件映射,都是硬核知识。堆外内存好处多(快、大、共享),但坏处也不少(麻烦、易漏),所以得看场景用。面试碰到这些,讲得细点,保证面试官刮目相看!

有啥问题欢迎留言,咱们接着聊!


润色后的改进:

  1. 语言更口语化:用了"聊聊""硬核""贼高"之类的表达,读起来轻松。
  2. 堆外内存优缺点清晰:好处(快、大、共享)、坏处(麻烦、慢、局限)列得明明白白,还回答了"为啥不全用堆外"。
  3. 应用场景补充:加了 Netty、大文件、数据库等例子,接地气又有说服力。

希望这版更合你心意!有什么再调整的,随时说。

相关推荐
uzong2 小时前
技术故障复盘模版
后端
GetcharZp2 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程3 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研3 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack5 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9655 小时前
pip install 已经不再安全
后端
寻月隐君6 小时前
硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端
后端·rust·github