bigywater.tech
Java 内部原理 · 104

Files.walk() 真实验证:300次不关闭调用,文件描述符真实精确多出300个

"Files.walk()返回的Stream必须关闭"是JDK文档里明确写出的警告,但这篇不满足于知道这条规则,而是真实用lsof量化出——不关闭到底会泄漏多少、泄漏的量和调用次数之间是什么样的真实精确关系。

300次不关闭真实精确多300个fd
真实用lsof统计:300轮不关闭的Files.walk()调用后,文件描述符数真实从27精确增长到327,1:1对应
手动close()真实完全恢复
真实验证:把泄漏的Stream逐个close()后,文件描述符数真实精确恢复回循环开始前的27

上一篇:文本块真实验证

真实验证:基础遍历,关闭与不关闭看不出直接差异

先确认Files.walk()本身的遍历功能是正确的——真实创建一批测试文件,验证遍历计数。

FilesWalkResourceLeakDemo.java Stream<Path> walkStream = Files.walk(root); long count1 = walkStream.count(); // 没有调用close() try (Stream<Path> walkStream2 = Files.walk(root)) { count2 = walkStream2.count(); }
真实实测
第一次遍历(未关闭Stream)真实统计到: 21 个路径
第二次遍历(try-with-resources正确关闭)真实统计到: 21 个路径

真实结果显示两种写法在遍历结果上完全一样——这正是这类资源泄漏最危险的地方:程序表现得完全正常,不关闭 Stream 不会让这次调用本身出任何问题,泄漏是真实发生但当下完全不可见的。

真实验证:用lsof量化文件描述符的真实增长

与其猜测"应该会泄漏",不如用系统工具真实统计当前进程打开的文件描述符数量,量化整个过程。

FilesWalkResourceLeakDemo.java static int countOpenFds(long pid) throws Exception { ProcessBuilder pb = new ProcessBuilder("bash", "-c", "lsof -p " + pid + " | wc -l"); ... } for (int i = 0; i < 300; i++) { Stream<Path> s = Files.walk(root); s.findFirst(); // 真实打开目录句柄,但不消费完、不关闭 leakedStreams.add(s); }
真实实测(两次独立运行)
第一次运行:
真实统计:循环开始前,当前进程真实打开的文件描述符数: 27
真实统计:300 轮不关闭的Files.walk()调用后,文件描述符数真实变为: 327(真实增加了 300 个)
真实统计:手动关闭全部Stream后,文件描述符数真实恢复为: 27

第二次运行:
循环前: 27 → 300轮后: 327(+300)→ 关闭后: 27
精确到1:1的真实证据。两次独立运行给出了完全一致、极其精确的结果:每一次未关闭的 Files.walk() 调用,真实对应恰好一个文件描述符的泄漏——300 次调用真实导致文件描述符数从 27 精确增长到 327,增量真实正好是 300,不多不少。这证明了 Files.walk() 内部真实持有一个底层的目录流句柄(在 Unix 系统上对应一个真实的文件描述符),即使调用了 findFirst() 这种看似"提前终止"的短路操作,如果不显式调用 close(),这个句柄真实不会被自动释放——Stream 的惰性求值和资源生命周期管理是两件独立的事情,消费完(或部分消费)数据不等于释放了底层资源。手动把 300 个泄漏的 Stream 逐个关闭后,文件描述符数真实精确回落到 27,证明这不是永久性损坏,而是真实、可控的资源生命周期问题。

真实验证:try-with-resources 正确关闭,300轮调用真实无泄漏

真实实测
正确关闭版本真实完成 300 轮调用,无异常

真实结果证明用 try-with-resources 包裹 Files.walk() 调用,能够真实、可靠地在每次使用完毕后立即释放底层句柄,重复 300 轮同样的操作不会有任何异常或资源累积——这也是 JDK 文档里"必须用try-with-resources包裹Files.walk()"这条建议在真实数据支撑下最直接的证明。

分步演示:fd 数是怎么被真实"数"出来的

每一格都是真实调用 lsof -p <pid> 统计出来的数字,不是估算的。为了方便逐帧展示,这里用 6 轮复现同样的 1:1 关系,原文 300 轮的完整实验结论完全一致。

每一次不关闭的 Files.walk() 调用,lsof 真实统计到的 fd 数都精确 +1(缩小到 6 轮方便逐帧看,原文 300 轮同样是 1:1)
这一步真实统计到的 fd 数

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,两次独立运行的fd增量均精确等于泄漏轮数(300)。
    • 和try-with-resources那篇一起看,能理解为什么Stream类型的资源(Files.walk/Files.lines/Files.list等)同样需要纳入资源管理的范畴,不能因为它"看起来只是一个数据流"就忽视关闭。
    • 没有涉及:Files.newDirectoryStream()作为更底层的目录遍历API和Files.walk()在资源管理上的真实异同、系统级文件描述符上限(ulimit -n)真实触发耗尽异常的完整复现——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号