Обходной путь для ошибки Java, которая вызывает аварийный дамп


Программа, которую я разработал, иногда сбивает JVM из-за этой ошибки: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8029516 . К сожалению, ошибка не была решена Oracle, и в отчете об ошибке говорится, что нет известных обходных путей.

Я попытался изменить пример кода из отчета об ошибке, позвонив .вместо этого зарегистрируйте (sWatchService, eventKinds) в потоке KeyWatcher, добавив все отложенные запросы на регистрацию в список, который я просматриваю в цикле нить KeyWatcher, но она все еще разбивается. Я предполагаю, что это просто имело тот же эффект, что и синхронизация на sWatchService (как и отправитель сообщения об ошибке пытался).

Можете ли вы придумать какой-нибудь способ обойти это?

3 8

3 ответа:

Мне удалось создать обходной путь, хотя он несколько уродлив.

Ошибка находится в методе 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().

Из комментариев:

Похоже, что у нас есть проблема с отменой ввода-вывода, когда есть ожидающий ReadDirectoryChangesW непогашенный.

Оператор и пример кода указывают, что ошибка срабатывает, когда:

  1. существует ожидающее событие, которое не было использовано (оно может быть или не может быть видимым для WatchService.poll() или WatchService.take())
  2. WatchKey.cancel() вызывается по ключу

Это неприятная ошибка без универсального решения. Подход зависит от особенности вашего приложения. Рассмотрите возможность объединения часов в одно место, чтобы вам не нужно было звонить WatchKey.cancel(). Если в какой-то момент пул становится слишком большим, закройте весь WatchService и начните все сначала. Что-то похожее.

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();
    }
}

Возможно, вы не сможете обойти саму проблему, но вы можете справиться с ошибкой и справиться с ней. Я не знаю вашей конкретной ситуации, но могу предположить, что самая большая проблема-это крах всего СП. Помещать все в блок try не работает, потому что вы не можете поймать сбой JVM.

Не зная больше о вашем проекте, трудно предложить хорошее / приемлемое решение, но, возможно, это может быть вариант: сделать все файлы, просматривающие материал в отдельном процессе JVM. Из вашего основного процесса запустите новый JVM (например, используя ProcessBuilder.start()). Когда процесс завершится (то есть вновь запущенный JVM выйдет из строя), перезагрузите его. Очевидно, что вы должны быть в состоянии восстановить, то есть вы должны отслеживать, какие файлы смотреть, и вы должны сохранить эти данные в вашем основном процессе тоже.

Теперь самая большая оставшаяся часть-это реализовать некоторую связь между основным процессом и процессом просмотра файлов. Это можно сделать с помощью стандартного ввода /выход процесса наблюдения за файлами или использования Socket/ServerSocket или какой-то другой механизм.