Java 17中Files.exists()检测存在文件时耗时异常的问题
Windows 10下Java 17遍历目录判断文件存在时的性能异常问题
问题背景
在Windows 10系统上使用Java 17,存在一组格式为data/dir-0至data/dir-999的目录。通过循环遍历0到999的整数i,检查每个子目录中是否存在foo.bar文件,代码如下:
Path foobarFile = dataDir.resolve("dir-" + i); if(Files.exists(fooBarFile)) { // do something; currently does nothing }
异常现象
- 当所有
data/dir-*目录为空时,整个循环瞬间完成; - 当部分目录包含
foo.bar文件时,循环在找到第一个存在的文件后,每次Files.exists()调用耗时近半秒; - 重启程序后,系统会“记住”之前检查的位置:已检查过的目录调用
exists()速度正常,未检查过的目录调用耗时近一秒; - 该问题在Eclipse和命令行运行环境中均出现。
排查更新
发现Java 17在Windows系统中判断路径是否存在时,会通过sun.nio.fs.WindowsFileSystemProvider.checkReadAccess(WindowsPath file)方法打开文件通道实现:
try { Set<OpenOption> opts = Collections.emptySet(); FileChannel fc = WindowsChannelFactory .newFileChannel(file.getPathForWin32Calls(), file.getPathForPermissionCheck(), opts, 0L); fc.close(); } catch (WindowsException exc) { … }
需求仅为判断文件是否存在以确定是否需要生成,无需检查读取权限。
问题原因
- Java 17的Windows文件系统提供者中,
Files.exists()默认会执行读权限检查:通过打开文件通道的方式验证当前用户是否有权限读取该文件,这个操作在Windows系统上的开销远大于单纯判断文件是否存在的操作。当目标文件存在时,必须完成完整的权限校验流程,导致耗时飙升; - 当目录为空时,
Files.exists()会直接返回不存在,无需走权限检查流程,因此速度极快。
“记忆”现象的原理
这并非Java程序本身的“记忆”,而是Windows系统的文件系统缓存机制在起作用:
- 第一次访问某个目录时,系统需要从磁盘读取该目录的元数据(比如文件列表、属性等),这个过程耗时较长;
- 访问过的目录元数据会被Windows缓存到内存中,后续再次访问时直接从缓存读取数据,无需再访问磁盘,因此速度更快;
- 程序重启后,只要系统未重启,这份缓存依然存在,所以之前检查过的目录能快速响应,未检查过的则需要重新从磁盘加载元数据,导致耗时增加。
更高效的检测方法
1. 使用Files.exists()重载方法跳过权限检查
Files.exists()提供了支持LinkOption的重载版本,传入LinkOption.NOFOLLOW_LINKS可以让文件系统提供者跳过读权限检查,仅验证文件是否存在:
Path foobarFile = dataDir.resolve("dir-" + i).resolve("foo.bar"); if (Files.exists(foobarFile, LinkOption.NOFOLLOW_LINKS)) { // do something }
这个改动能直接减少权限检查带来的系统调用开销,大幅提升判断速度。
2. 预扫描批量缓存结果
一次性扫描data目录下所有子目录中的foo.bar文件,将存在该文件的目录名缓存到集合中,后续遍历直接查询集合即可:
import java.util.Set; import java.util.stream.Collectors; import java.nio.file.Files; import java.nio.file.Path; // 预扫描所有包含foo.bar的dir-*目录 Set<String> dirsWithFooBar = Files.list(dataDir) .filter(dir -> dir.getFileName().toString().matches("dir-\\d+")) .map(dir -> dir.resolve("foo.bar")) .filter(path -> Files.exists(path, LinkOption.NOFOLLOW_LINKS)) .map(path -> path.getParent().getFileName().toString()) .collect(Collectors.toSet()); // 遍历判断 for (int i = 0; i < 1000; i++) { String dirName = "dir-" + i; if (dirsWithFooBar.contains(dirName)) { // do something } }
这种方式将多次零散的IO操作合并为一次批量扫描,减少了循环中的IO交互次数,适合批量检查的场景。
内容的提问来源于stack exchange,提问作者Garret Wilson
相关产品推荐
相关产品推荐

