FileChannel:部分传输、映射生命周期与零拷贝边界
一次文件传输没有报错,接收端却少了一截;文件通道已经关闭,旧文件仍不能删除。这两个现象都来自把不同层次的“完成”混在一起:方法返回、字节传完、映射释放和数据持久化,各有边界。本文以 JDK 21 的传统 MappedByteBuffer API 为基线。
transferTo 返回的是进度
transferTo(position, count, target) 的 count 是本次请求的上限,返回值才是实际传输量。它可能小于请求量,也可能为零;不会用 -1 表示结束。源通道的当前位置不随调用推进,因此不能反复传入同一个 offset,指望下一次自动续传。JDK 21 FileChannel 契约是处理返回值的依据。
例如发送十字节,目标每次只接受三字节:第一次返回 3,第二次应从 3 开始,而非再次从 0 开始。维护的是“已交付范围”,不是“尝试次数”。
long offset = 0;
while (offset < length) {
long n = src.transferTo(offset, length - offset, dst);
if (n == 0) throw new IOException("no progress at " + offset);
offset += n;
}这里采用同步示例的快速失败策略,避免死循环。零返回不等于文件末尾:非阻塞 SocketChannel 暂时不可写也会没有进展。网络服务应保存 offset 和目标长度,等待可写事件后续传,并设置超时或取消条件;持续可写时也要给单次处理设置预算,避免一个大文件占满事件线程。
开始前读取 size 也没有创建快照。复制过程中源文件被截短,计划长度便可能永远无法完成;被原地修改则可能得到混合版本。工程上可采用不可变文件版本,把源文件稳定性作为调用前提。若改用 read/write 回退,仍需处理部分写入,不能把未写完的 Buffer 直接 clear。
零拷贝是一条可能的优化路径
“零”通常针对某些用户态中转复制,并不表示磁盘、页缓存、网卡间完全没有数据搬运。FileChannel API 给实现留下优化空间,却没有承诺每个目标通道都会采用同一条内核路径。自定义通道、加密或压缩处理都可能改变数据路径。
因此,选择 transferTo 的第一理由是表达文件区间传输;性能收益需要在目标 JDK、操作系统、文件系统和协议下测量。冷缓存与热缓存、CPU 使用率与吞吐量应分开观察。本文的限流通道验证返回值处理,不能作为操作系统零拷贝的性能证据。
关闭通道不等于解除映射
映射建立后拥有独立于通道的生命周期。try-with-resources 关闭 FileChannel,只能说明通道资源已关闭;仍被引用的 MappedByteBuffer 可以继续访问。传统 API 没有公开的 close/unmap,不能把 System.gc() 当作确定性的释放协议。参见 MappedByteBuffer 生命周期。
| 操作 | 实际含义 | 不能据此推断 |
|---|---|---|
| channel.close() | 关闭通道 | 映射已经释放 |
| mapped.force() | 请求映射修改写入存储 | 解除映射或完成业务事务 |
| 丢弃一个引用 | 减少可达引用 | 所有视图都已失效 |
| clear() | 重置 Buffer 游标状态 | 清零文件或释放映射 |
slice、duplicate 等视图共享内容,容易让映射比业务对象活得更久。不要在轮换文件时仅将主变量置 null,就马上假定可以回收磁盘空间。映射活跃期间尤其应避免截短文件:原本合法的索引可能落入不可访问区域,异常类型和发生时机也不能作为跨平台控制流程。
force() 与释放同样独立。它针对映射修改;对非本地设备没有同等持久化保证,PRIVATE 映射也不应拿来回写原文件。读取到新字节只证明此次可见性,不构成断电恢复验证。具体限制见 force 官方说明。
平台差异影响文件轮换
Windows 官方文档说明,文件存在内存映射等占用时,DeleteFile 可能失败;关闭文件句柄也不会替代解除映射视图。DeleteFileW 与映射对象关闭说明共同解释了为什么“通道关了仍删不掉”。权限、共享标志等也影响结果,不能把任意删除失败都归因于映射。
POSIX 的 unlink 删除目录项;尚有打开引用时,内容的回收可以延后。路径消失与已有映射失效不是同一事件,见 The Open Group unlink 规范。这类差异意味着跨平台应用不能依赖“映射后立即删文件”管理生命周期。可采用版本化文件名、停止新读者、等待旧读者退出后重试清理的策略;对严格要求确定释放时刻的场景,应重新评估传统映射是否合适。
可运行例子与阅读延伸
配套源码位于 examples/java-advanced/nio-file-channel/FileChannelDemo.java。它验证十字节经过 3、3、3、1 四次传输保持完整、源 position 不变,以及关闭通道后映射仍可读写。零进展触发 IOException、只读视图写入触发 ReadOnlyBufferException 是明确的预期失败,由程序捕获并验证。
JDK 21 的命令、原始输出和运行限制保存在 docs/upgrade-100/checks/nio-file-channel/。这里只做本地功能验证,没有跨平台删除实验、性能对比或断电测试;不涉及数据库与 Redis。继续阅读字节与字符 IO、Buffer 与 Channel、Selector 生命周期,或返回技术进阶阅读入口。
