既然 ByteBlockPool 底层是一个个连续的大 buffer(通常是 32KB),为什么不直接在大 buffer 里顺序往后写,非要搞出“切片“这个中间层? 这是一个直击灵魂的问题。既然ByteBlockPool底层是一个个连续的大 buffer通常是 32KB为什么不直接在大 buffer 里顺序往后写非要搞出切片这个中间层答案的核心在于ByteBlockPool 不是一个单纯的字节流存储器它是一个支持多路并发写入 随机寻址的内存管理器。如果不按 Slice 扩容以下三个核心需求将无法实现1. 多路并发写入互不干扰最核心原因在 Lucene 构建倒排索引时成千上万个 Term 是同时被写入同一个 ByteBlockPool 的。每个 Term 都有自己的 posting list 需要追加数据。如果没有 Slice所有 Term 共享一个写入指针。Term A 写 5 字节Term B 写 3 字节Term C 写 10 字节……它们的数据会像面条一样交织在一起。你根本无法区分哪些字节属于哪个 Term。有了 Slice每个 Term 拥有自己独立的 Slice 链。Term A 的 Slice、Term B 的 Slice 在大 buffer 中各自占据一段空间彼此物理隔离。每个 Slice 有自己的写入游标互不影响。大 Buffer (32KB) 内部的实际布局 [TermA-slice0][TermB-slice0][TermC-slice0][空闲][TermA-slice1][TermD-slice0][TermB-slice1]... ↑ 各 Term 的数据块交错分布但每个块内部是连续的Slice 本质上就是 ByteBlockPool 内部的虚拟独立缓冲区让多个写入者共享同一块物理内存而不冲突。2. 支持非连续增长链表式存储一个 Term 的 posting list 长度是不可预知的可能只有几个字节也可能有几十 MB。如果不用 Slice要为每个 Term 预分配连续空间要么浪费预分配太大要么不够用时需要整体搬迁类似 ArrayList 扩容的Arrays.copyOf。在多线程环境下搬迁数据是灾难性的。用了 Slice每个 Term 的数据以Slice 链表的形式存储。写满了就在大 buffer 的空闲处分配一个新 Slice通过 marker 指过去。不需要搬迁任何已有数据也不需要预分配。Term hello 的数据在物理上不连续但逻辑上是链表 buffer[2].slice(offset100, len200) → marker指向 → buffer[5].slice(offset340, len80) → marker指向 → buffer[5].slice(offset900, len200)读取时沿着 marker 链遍历即可这就是前面讨论的setBytesRef能零拷贝取数据的前提——每个 Slice 内部是连续的所以可以用 BytesRef 直接引用。3. 分级大小是为了极致减少碎片回到你之前问的LEVEL_SIZE_ARRAY。为什么 Slice 不统一大小而要分 10 级因为 Term 的长度分布极度不均匀大量短 Term几个~十几个字节→ 用小 Slice5/14/20避免浪费少量长 Posting List → 用大 Slice80/120/200减少链表节点数和allocSlice()调用开销如果统一用 200 字节的 Slice那些只存 3 字节数据的短 Term 会浪费 98% 的空间。如果统一用 5 字节长 Term 会产生海量链表节点遍历时 cache miss 爆炸。分级就是对这种幂律分布的工程妥协。 总结对比方案多路并发动态增长内存碎片读取性能纯顺序写入无 Slice❌ 数据混杂❌ 需搬迁--每 Term 独立 byte[]✅✅❌ 海量小对象GC 爆炸✅ByteBlockPool Slice✅ 隔离且共享✅ 链表式零搬迁✅ 分级控制✅ 片内连续可零拷贝一句话Slice 是 ByteBlockPool 在一块大内存上模拟出无数个独立可变长缓冲区的最小代价抽象。没有它Lucene 就无法在单池内高效地同时构建数百万个 Term 的倒排数据。