引用计数管理堆外内存对象池的核心是严格绑定借用、归还、销毁与计数增减,杜绝错位、提前释放、重复释放;计数器嵌入内存块头部,用Unsafe原子操作维护,acquire/release成对触发计数变更,归零即回收但不立即free,真正释放仅在池销毁或清理器确认refcnt==0后执行,并通过Cleaner兜底检测泄漏。

直接用引用计数管理堆外内存对象池的生命期,核心不是“计数器放哪”,而是让每次对象借用、归还、销毁都与计数增减严格绑定,且杜绝计数错位、提前释放、重复释放三类致命问题。
对象池结构中嵌入原子引用计数字段
每个堆外内存块(比如一个 128 字节的 buffer)头部预留 4 或 8 字节,存放一个 Unsafe.compareAndSwapInt/Long 可操作的原子计数器。不要用普通 int 字段——它在多线程下读-改-写必然竞态。初始化时用 putInt 或 putLong 写入 0,首次分配后立即 getAndAddInt(addr, 1) 置为 1。
关键细节:
- 计数器地址 = 堆外内存起始地址 + 头部偏移(如 0),必须 4 字节对齐(int)或 8 字节对齐(long)
- 不依赖 JVM 对象头,所有操作走 unsafe.xxx(null, addr + offset, ...),明确指向裸地址
- 避免把计数器和业务数据混排导致误读;建议统一头部结构:[refcnt:int][size:int][payload:...]
借出与归还必须成对触发计数变更
对象池的 acquire() 和 release(buf) 方法是唯一合法修改计数的入口。每次 acquire 返回前执行 getAndAddInt(addr, 1);每次 release 开头先 getAndAddInt(addr, -1),再判断是否归零。
典型安全模式:
- acquire:查空闲链表 → 取块 → 计数从 0→1(首次使用)或 1→2(复用)→ 返回裸地址
- release:接收裸地址 → 计数减 1 → 若结果为 0,则将该块插入空闲链表,**不立即 freeMemory**
- 绝不允许用户绕过池接口直接调用 freeMemory;所有释放必须经由 release 流程
归零即回收,但需规避“伪归零”风险
计数到 0 表示当前无任何线程持有该块,可安全重用。但要注意两类干扰:
- 未初始化就 release:分配后未 acquire 就调 release,计数从 0→-1,后续 acquire 变成 0→1,逻辑错乱。解决方案:acquire 时强制校验计数 ≥ 0,非法值直接抛异常
- 多线程同时 release 同一块:两个线程都读到 1,都减为 0,都执行归还链表操作 → 链表损坏。解决方案:用 compareAndSwapInt 循环重试,仅当旧值为 1 时才允许减为 0 并归还
- 真正 freeMemory 的时机只在池整体销毁时统一调用,或长期闲置块被周期性清理器扫描到且确认 refcnt == 0 后才执行
配合 Cleaner 实现兜底防护
即使应用层忘记 release,也要防止堆外内存永久泄漏。可仿照 DirectByteBuffer,在池创建时注册一个 Cleaner:
- Cleaner 关联的是整个池的生命周期,不是单个 buffer
- clean() 方法中遍历所有已分配块,对每个 refcnt == 0 的块调用 unsafe.freeMemory(addr)
- 对 refcnt > 0 的块打日志告警——说明存在泄漏,但不强行释放,避免 crash
- 该机制不替代正确 release,而是作为开发期检测与线上容错手段


















