Что может вызвать внезапное исключение ClassNotFoundException в длительном процессе?
У нас очень маленький веб-сервис (менее 1 тыс. строк кода), который запускается Jetty. Служба всегда работала хорошо даже во время фазы стресс-тестирования. Однако после 13 дней безотказной работы в тот же день мы испытали исключение ClassNotFoundException в двух узлах.
Странная вещь заключается в том, что класс, который не был найден, уже был там (он является частью процедуры запуска и постоянно использовался для обслуживания предыдущих запросов). Фактически, просто перезапуск процесса решил проблему. Оба узла находятся в отдельных машинах и не зависят друг от друга. Они не зависят от внешних ресурсов, кроме одного JMS-соединения.
Я не смог найти нужную информацию, в то время как Googling это, так как большинство сообщаемых проблем связаны с отсутствующими классами в пути класса при запуске Java-процесса, что не является нашим делом. Мы подозреваем, что может произойти утечка памяти, которая каким-то образом повредила JVM-память, однако это не может объяснить, почему одна и та же проблема произошла в двух узлах в одно и то же время. В течение последних пяти дней мы проводили интенсивное стресс-тестирование, добавляя монитор JVM и анализатор утечки памяти, и все кажется прекрасным. Для этих тестов мы уменьшили память процесса с 2 до 512 МБ.
Детали:
- Использование 64-разрядной серверной виртуальной машины Java HotSpot TM (сборка 16.3-b01, смешанный режим)
- Использование jetty-runner-8.1.0.RC5.jar
- Оригинальная строка cmd: java -Xmx2048M -jar jetty-runner-8.1.0.RC5.jar --port 5000 webapp.war
- Intel Xeon E5-2680 8 ядер (x2) + 16 ГБ оперативной памяти
- Red Hat Enterprise Linux 6
- Некоторые используемые рамки: JBoss Resteasy, Spring IoC, Guava.
Можете ли вы поделиться идеями относительно того, что может заставить JVM внезапно "забыть" о существовании ранее загруженного класса, не имея возможности загрузить его снова?
Caused by: java.lang.ClassNotFoundException: com.a.b.c.SomeClass
at java.net.URLClassLoader$1.run(URLClassLoader.java:202) ~[na:1.6.0_37]
at java.security.AccessController.doPrivileged(Native Method) ~[na:1.6.0_37]
at java.net.URLClassLoader.findClass(URLClassLoader.java:190) ~[na:1.6.0_37]
at java.lang.ClassLoader.loadClass(ClassLoader.java:306) ~[na:1.6.0_37]
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301) ~[na:1.6.0_37]
at java.lang.ClassLoader.loadClass(ClassLoader.java:247) ~[na:1.6.0_37]
at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:424) ~[na:na]
at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:377) ~[na:na]
at java.lang.Class.forName0(Native Method) ~[na:1.6.0_37]
at java.lang.Class.forName(Class.java:247) ~[na:1.6.0_37]
at sun.reflect.generics.factory.CoreReflectionFactory.makeNamedType(CoreReflectionFactory.java:95) ~[na:1.6.0_37]
at sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:107) ~[na:1.6.0_37]
at sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:31) ~[na:1.6.0_37]
at sun.reflect.annotation.AnnotationParser.parseSig(AnnotationParser.java:370) ~[na:1.6.0_37]
at sun.reflect.annotation.AnnotationParser.parseClassValue(AnnotationParser.java:351) ~[na:1.6.0_37]
at sun.reflect.annotation.AnnotationParser.parseMemberValue(AnnotationParser.java:280) ~[na:1.6.0_37]
at sun.reflect.annotation.AnnotationParser.parseAnnotation(AnnotationParser.java:222) ~[na:1.6.0_37]
at sun.reflect.annotation.AnnotationParser.parseAnnotations2(AnnotationParser.java:69) ~[na:1.6.0_37]
at sun.reflect.annotation.AnnotationParser.parseAnnotations(AnnotationParser.java:52) ~[na:1.6.0_37]
at java.lang.reflect.Field.declaredAnnotations(Field.java:1014) ~[na:1.6.0_37]
at java.lang.reflect.Field.getDeclaredAnnotations(Field.java:1007) ~[na:1.6.0_37]
Edit:
Кто-то сказал мне, что при использовании монтирования NFS под Win может произойти, что JVM решает выгрузить класс, а затем повторно загружает его, когда это необходимо. Если в середине этого процесса соединение NFS было нарушено, дескриптор файла будет недействительным, и повторная загрузка завершится неудачно с аналогичной stacktrace. В нашем случае мы используем Linux, и все задействованные файлы находятся на одном и том же монтировании, который является локальным жестким диском. Просто для большего тестирования, я записал CD'd в временный каталог Jetty и вручную удалил один из известных для одного определенного класса сервиса. Если JVM выгружает его, а затем пытается повторно загрузить его из каталога классов, он завершит сбой. Хотя это не объясняет исходную проблему, она может добавить дополнительную информацию в таблицу...
Ответы
Ответ 1
Это то, что происходит:
- Когда служба запускается с использованием вышеописанного cmd, Jetty создает поддиректор под "/tmp", который содержит классы и ресурсы приложений, загружаемые JVM.
- После периода бездействия (в нашем конкретном сценарии, между 13 и 20 днями) этот каталог исчезает. В результате JVM не может загрузить файл. Мы все еще не знаем точно, если JVM выгрузил класс перед этой ошибкой или почему он попытался перечитать файл *.class. Было бы интересно посмотреть в исходный код и узнать об этом, но это не в нашем кратковременном списке ToDo.
- Просто перезагрузка Jetty вызвала воссоздание отсутствующих каталогов, и служба снова запущена.
Хороший намек на то, что некоторые люди сообщали о подобной проблеме при загрузке ресурсов в JAR по NFS в Windows (если сетевое подключение теряется на короткое время, рутки NFS становятся недействительными, а JVM не работает с аналогичной ошибкой). Это не наш случай (/tmp - локальное хранилище), но очень похоже.
Спасибо всем за помощь.
Ответ 2
Трассировка стека говорит нам, что речь идет о обработке аннотаций и не связана с загрузкой класса для выполнения кода. Похоже, что обработчик аннотации пытается разрешить значение элемента аннотации через ClassLoader
аннотированного элемента.
Другими словами, у вас есть аннотация со значением типа класса типа @Foo(xyz=ABC.class)
и класс или член, аннотированный этой конструкцией, но класс ABC
недоступен через ClassLoader
аннотированного элемента во время выполнения.
Это не противоречит тому, что этот класс уже загружен через другой ClassLoader
.