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、大文件、数据库等例子,接地气又有说服力。

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

相关推荐
梨涡泥窝1 分钟前
JavaWeb——基于 Spring Boot + Vue 的图书管理系统的设计与实现
vue.js·spring boot·后端
jsl_jsl_jsl37 分钟前
LLM工具调用速记
后端
YIAN37 分钟前
手撸社区后端:一套可落地的用户 - 文章 - 互动体系 MySQL 表设计与优化思路
后端·mysql
ThinkerQAQ_44 分钟前
并发编程(一):先谈硬件——从 count++ 到原子性、可见性与有序性
后端
岁月如歌77861 小时前
分布式锁完全指南:从数据库到 Redisson 的演进
java·后端·架构
技术猫1 小时前
记一次因线程池不合理使用导致生产拨测告警
后端
写后端的胖头鱼1 小时前
【高频面试题】spring事物失效场景(带原理 + 代码示例)
java·后端·spring·事务·事物失效
JohnCarter20211 小时前
双卡 Atlas 300I Duo 部署 Qwen3-VL-32B:血泪实录
后端
码外生活1 小时前
🎥 手搓一套直播高并发环境!一个后端小白从 0 到 1 的搭建笔记
后端·spring cloud
Lyra_Infra2 小时前
记一次麒麟 Linux 下达梦数据库 (DM8) 部署与 MySQL 命令行无界面迁移踩坑指南
数据库·后端·dba