并发标记阶段将nio buffer视为普通java对象纳入可达性分析,依据gc roots引用、attachment引用链及写屏障机制确保不漏标;堆外内存释放依赖buffer对象不可达后cleaner触发,而非标记过程直接处理。

并发标记阶段扫描和穿透NIO Buffer的引用,不是靠特殊规则,而是把Buffer对象当作普通Java对象纳入标准可达性分析流程。关键在于:Buffer本身是否被GC Roots引用、其attachment字段是否构成有效引用链、以及并发修改时如何避免漏标。
Buffer对象按常规GC Roots规则参与标记
DirectByteBuffer是堆内普通对象,只要它被线程栈局部变量、静态字段、或其它存活对象持有,就会被标记为灰色并继续遍历。它的内部字段(如address、capacity、attachment)都会被递归扫描:
- address字段虽指向堆外内存,但不参与引用遍历,仅作地址记录;
- attachment字段若为Java对象(比如自定义回调上下文),会作为一条有效引用链加入遍历路径;
- Cleaner虚引用由ReferenceQueue支撑,而ReferenceQueue本身是GC Roots之一,确保Cleaner不被提前回收,从而维系整个清理链条。
写屏障捕获Buffer相关引用的动态变更
用户线程在并发标记过程中调用buffer.put()、buffer.slice()、或把buffer赋值给新变量,可能新建引用关系。JVM通过写屏障实时响应这类操作:
- 当某线程执行obj.field = buffer时,写屏障触发,将该字段所在卡页标记为“脏”;
- G1使用记忆集(Remembered Set)记录跨区域引用,ZGC/SATB则在标记开始时拍快照,后续仅增量扫描被修改的引用位置;
- 这样即使buffer被临时放入Map、作为异步回调参数传递,也不会漏标。
穿透attachment实现间接引用追踪
很多NIO用法会把业务对象绑定到Buffer上,例如channel.write(buffer, attachment)。这个attachment字段是DirectByteBuffer的普通成员变量,会被标记算法自然读取:
- 一旦buffer被标记为灰色,扫描器会访问其attachment字段;
- 如果attachment非null且是Java对象,它立即变为灰色,进入待扫描队列;
- 后续对该attachment的字段(如其中的List、Handler等)继续递归扫描,形成完整引用链。
堆外内存不靠标记穿透,而靠对象不可达后触发清理
并发标记阶段不处理堆外内存本身,只决定DirectByteBuffer对象的命运:
- 只要buffer对象断开所有GC Roots引用,它会在本轮或下轮GC中被判定为白色;
- 随后进入F-Queue,关联的Cleaner被Reference Handler线程发现并执行Unsafe.freeMemory();
- 所以“穿透”的终点不是native memory,而是attachment所连的Java对象图——native memory释放是结果,不是标记目标。











