我开发的一个程序偶尔会因为这个错误而导致JVM崩溃:http://bugs.java.com/bugdatabase/view_bug.do?臭虫id=8029516。不幸的是,Oracle还没有解决这个错误,错误报告说没有已知的解决方法。
我试图通过调用KeyWatcher线程中的. Register(sWatchService,eventKinds)来修改 bug 报告中的示例代码,而是将所有待处理的寄存器请求添加到我在KeyWatcher线程中循环的列表中,但它仍然崩溃。我猜这与在sWatchService上同步的效果相同(就像 bug 报告的提交者尝试的那样)。
你能想到任何解决这个问题的方法吗?
来自评论:
当有待处理的ReadDirectoryChangesW未完成时,我们似乎遇到了I/O取消问题。
语句和示例代码表明 bug 在以下情况下触发:
WatchService.poll()
或WatchService.take()
)可见也可能不可见WatchKey.cancel()
在键上调用这是一个令人讨厌的bug,没有通用的解决方法。方法取决于应用程序的具体情况。考虑将手表集中到一个地方,这样您就不需要调用WatchKey.cancel()
。如果在某一点上池太大,请关闭整个Watch Service
并重新开始。类似的东西。
public class FileWatcerService {
static Kind<?>[] allEvents = new Kind<?>[] {
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY
};
WatchService ws;
// Keep track of paths and registered listeners
Map<String, List<FileChangeListener>> listeners = new ConcurrentHashMap<String, List<FileChangeListener>>();
Map<WatchKey, String> keys = new ConcurrentHashMap<WatchKey, String>();
boolean toStop = false;
public interface FileChangeListener {
void onChange();
}
public void addFileChangeListener(String path, FileChangeListener l) {
if(!listeners.containsKey(path)) {
listeners.put(path, new ArrayList<FileChangeListener>());
keys.put(Paths.get(path).register(ws, allEvents), path);
}
listeners.get(path).add(l);
}
public void removeFileChangeListener(String path, FileChangeListener l) {
if(listeners.containsKey(path))
listeners.get(path).remove(l);
}
public void start() {
ws = FileSystems.getDefault().newWatchService();
new Thread(new Runnable() {
public void run() {
while(!toStop) {
WatchKey key = ws.take();
for(FileChangeListener l: listeners.get(keys.get(key)))
l.onChange();
}
}
}).start();
}
public void stop() {
toStop = true;
ws.close();
}
}
我已经设法创建了一个变通方法,虽然它有点难看。
该错误位于JDK方法WindowsWatchKey.invalidate()
中,该方法释放本机缓冲区,而后续调用仍可能访问它。此单行代码通过将缓冲区清理延迟到 GC 来解决此问题。
这是JDK的编译补丁。为了应用它,添加以下Java命令行标志:-Xbootclasspath/p:jdk-8029516-patch.jar
如果在您的情况下无法选择修补 JDK,则在应用程序级别仍有解决方法。它依赖于Windows WatchService内部实现的知识。
public class JDK_8029516 {
private static final Field bufferField = getField("sun.nio.fs.WindowsWatchService$WindowsWatchKey", "buffer");
private static final Field cleanerField = getField("sun.nio.fs.NativeBuffer", "cleaner");
private static final Cleaner dummyCleaner = Cleaner.create(Thread.class, new Thread());
private static Field getField(String className, String fieldName) {
try {
Field f = Class.forName(className).getDeclaredField(fieldName);
f.setAccessible(true);
return f;
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
public static void patch(WatchKey key) {
try {
cleanerField.set(bufferField.get(key), dummyCleaner);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
}
在注册密钥后立即调用JDK_8029516.patch(watchKey)
,这将防止watchKey.cancel()
过早释放本机缓冲区。
您可能无法解决问题本身,但您可以处理错误并处理它。我不知道你的具体情况,但我可以想象最大的问题是整个JVM的崩溃。将所有内容放在 try
块中不起作用,因为您无法捕获 JVM 崩溃。
不了解更多关于你的项目会让你很难建议一个好的/可接受的解决方案,但这可能是一个选择:在一个单独的JVM进程中完成所有文件监视工作。从你的主进程开始一个新的JVM(例如使用ProcessBuilder.start()
)。当进程终止时(即新启动的JVM崩溃),重新启动它。显然你需要能够恢复,即你需要跟踪要观看的文件,你也需要将这些数据保存在你的主进程中。
现在剩下的最大部分是实现主进程和文件监视进程之间的一些通信。这可以使用文件监视过程的标准输入/输出或使用Socket
/ServerSocket
或其他一些机制来完成。