直接用 byte[] 做网络缓存易出错,因其是可变对象且无边界控制,多线程读写或未消费完即重置会导致数据覆盖、乱码、粘包等;根本在于缺乏所有权管理和生命周期标记。
为什么直接用
做网络缓存容易出错
因为
是可变对象,且没有内置边界控制——多个线程同时读写同一数组、或一个
尚未消费完就重置位置,都会导致数据覆盖或读取错位。常见现象是:客户端收到乱码、协议头校验失败、偶发性粘包/丢包,但日志里查不到明确异常。
根本原因不是数组本身有问题,而是缺乏「所有权管理」和「生命周期标记」。比如用
复制数组看似安全,但若原始数组被上游复用,复制前的数据可能已被覆盖。
避免直接暴露裸
给业务逻辑层,尤其跨线程场景
不要在
后立刻调用
—— 必须等
返回实际写出字节数且为正,再重置
如果使用
,注意其内存不归 JVM GC 管理,需手动调用
或复用池防止 OOM
用
替代
的关键姿势
不是“更高级的数组”,它是带状态机的二进制容器:有
、
、
三重约束。网络传输中真正起作用的是
和
,而多数 bug 来自对这两者的误操作。
典型错误:调用
后没调用
就传给
,结果写入 0 字节;或
后又
导致
,触发
。
写入阶段:用
系列方法填充数据 → 调用
(把
设为当前
,
归零)
写出阶段:传给
→ 检查返回值,若
,说明未写完,需注册
事件继续
读取阶段:
后必须调用
才能用
读取,否则
在末尾,读不到数据
如何安全复用
避免频繁分配
高频小包场景下,每收发一次都新建
会触发大量 GC,尤其
还可能引发堆外内存泄漏。正确做法是用线程本地池 + 容量分级。
例如:预分配 32KB、1MB、4MB 三档缓冲区,按包大小就近匹配;每个线程独占一个
,避免锁竞争。切记不能把
放进全局静态池并随意
——
只重置指针,不擦除数据,残留内容可能被下次误读。
复用前必须调用
(适用于读取未完成的场景)或
(适用于全新写入),而非仅设
对
,优先用
+ 自定义池,别依赖
手动 malloc,JDK 17+ 已限制该 API
如果协议要求固定包头(如 4 字节长度字段),可在池化时预留 header space,用
切出 payload 区域,避免越界计算
遇到粘包/半包时,
缓存怎么配合协议解析
纯
无法表达“当前已收多少、还差多少”,所以必须搭配状态机。常见错误是把整个 socket 接收缓冲当成一个包处理,或者每次只取固定长度,忽略协议本身的分帧规则。
推荐做法:维护一个
作为滚动接收缓冲(如 8KB),用两个整型变量
和
标记有效数据范围。每次
后更新
,然后循环解析:检查包头长度字段是否可读 → 若可读,计算完整包长 → 若
,则切出子数组交给解码器,再将剩余数据前移并更新
/
。
前移开销大?改用环形缓冲区(
)实现,避免
不要在解析中途修改
内容(如填充值),会导致后续包校验失败
如果协议支持流式解码(如 Protobuf Delimited),优先用
包装
,它内部已处理半包逻辑
最易被忽略的点是:缓冲区的「语义所有权」转移时机。比如从网络层传给业务层时,是传递副本、视图(
)、还是移交引用?一旦选错,轻则数据污染,重则 JVM 崩溃(DirectBuffer 被提前释放)。这事关设计契约,不是写个
就能糊弄过去的。
byte[]byte[]ByteBufferArrays.copyOf()byte[]SocketChannel.write()clear()write()DirectByteBuffercleaner()ByteBufferbyte[]ByteBufferpositionlimitcapacitypositionlimitput(byte[])flip()channel.write()flip()put()position > limitBufferOverflowExceptionput()flip()limitpositionpositionchannel.write(buffer)buffer.remaining() > 0OP_WRITEchannel.read(buffer)flip()get()positionByteBufferByteBufferDirectByteBufferThreadLocalByteBufferclear()clear()compact()clear()position = 0DirectByteBufferByteBuffer.allocateDirect()Unsafeslice()byte[]byte[]byte[]offsetlengthread()lengthlength >= 完整包长offsetlengthRingBufferSystem.arraycopy()byte[]CodedInputStreamByteBufferslice()copyOf()