Статьи

ClassNotFoundException: это замедляет вашу JVM?


Большинство разработчиков Java знакомы с печально известным и очень распространенным
исключением java.lang.ClassNotFoundException . Хотя источники этой проблемы, как правило, хорошо понятны (отсутствие классов / библиотек в пути к классам, проблемы с делегированием загрузчиков классов и т. Д.), Влияние на общую JVM и производительность часто неизвестно. Такая ситуация может привести к значительным последствиям для времени отклика вашего приложения и масштабируемости.


Крупные корпоративные системы Java EE с развернутыми несколькими приложениями наиболее подвержены этому типу проблем из-за большого количества различных загрузчиков классов приложений, активных во время выполнения.
Это увеличивает риск возникновения «необнаруженного» исключения ClassNotFoundException, если только не будет выявлено явное влияние на бизнес и не будет реализован тщательный мониторинг журналов, что приводит к постоянному влиянию на производительность наряду с возможным вводом-выводом классов JVM и конкуренцией за блокировку потоков.

Следующая статья и примеры программ продемонстрируют, что любое возникновение ClassNotFoundException, обнаруженное в ваших производственных системах, следует воспринимать очень серьезно и своевременно устранять.

Загрузка классов Java: недостающая ссылка для оптимальной производительности

Правильное понимание этой проблемы производительности начинается с правильного знания модели загрузки классов Java.
ClassNotFoundException по существу означает сбой JVM, чтобы найти и / или загрузить определенный класс Java, такой как:

  • Метод Class.forName ()
  • Метод ClassLoader.findSystemClass ()
  • Метод ClassLoader.loadClass ()

Хотя загрузка классов Java-приложений вашего приложения должна происходить только один раз в жизненном цикле JVM (или через динамические функции повторного развертывания), некоторые приложения также полагаются на операции загрузки динамических классов.

Несмотря на это, повторяющиеся допустимые и «сбойные» операции загрузки классов могут быть довольно навязчивыми, особенно когда процесс загрузки предпринимается самим JDK java.lang.ClassLoader по умолчанию. Действительно, поведение JDK 1.7+ по умолчанию из-за обратной совместимости позволит загружать только один класс за раз, если загрузчик классов не помечен как «параллельный». Имейте в виду, что даже если синхронизация выполняется только на уровне класса, повторяющаяся ошибка загрузки класса с тем же именем класса по-прежнему будет вызывать конфликт блокировки потока в зависимости от уровня параллелизма потока Java, с которым вы имеете дело. Ситуация была намного хуже назад с JDK 1.6 с синхронизацией, выполняемой систематически на уровне экземпляра загрузчика классов.

По этой причине контейнеры Java EE, такие как 
JBoss WildFly 8  , используют свои собственные внутренние параллельные загрузчики классов для загрузки классов вашего приложения. Эти загрузчики классов реализуют блокировку с более высокой степенью детализации, что позволяет одновременно загружать разные классы из одного и того же экземпляра загрузчика классов. Это также согласуется с последним улучшением JDK 1.7+, включающим поддержку 
многопоточных загрузчиков пользовательских классов,  которые также помогают предотвратить сценарии взаимоблокировки определенных загрузчиков классов.

При этом загрузка классов классов системного уровня, таких как контейнеры java. * И контейнеры Java EE, все еще выполняется по умолчанию в JDK ClassLoader.
Это означает, что повторяющаяся ошибка загрузки класса для
одного и того 
же  имени класса, например ClassNotFoundException, может по-прежнему вызывать серьезный конфликт блокировки потока. Это именно то, что мы будем тиражировать и демонстрировать для остальной части статьи.

Конфликт блокировки потока — проблема репликации

Чтобы воссоздать и смоделировать эту проблему, мы создали простое приложение согласно приведенным ниже спецификациям:
  • Веб-служба JAX-RS (REST), выполняющая Class.forName () для фиктивного имени класса, «расположенного» на уровне пакета системы:

  String className = «java.lang.WrongClassName»;

    Учебный класс.
forName (className);
           

Симуляция по существу выполняется одновременно с 
20 потоками  веб-службы JAX-RS. Каждый вызов генерирует исключение ClassNotFoundException. Ведение журнала было полностью отключено, чтобы уменьшить влияние на ввод-вывод и сосредоточиться только на конкуренции за загрузку класса.

 Теперь давайте посмотрим на результаты JVisualVM из нашего 30-60 секундного прогона.
Мы можем ясно видеть множество BLOCKED-потоков, ожидающих блокировки на мониторе объектов.

Анализ дампа потока JVM ясно выявляет проблему: Конфликт блокировки потока.
Из трассировки стека выполнения видно, что JBoss делегирует загрузку класса JLK ClassLoader… почему? Это потому, что наше неправильное имя класса Java обнаружено как часть пути к системному классу, например, java. *. В этой ситуации JBoss делегирует загрузку системному загрузчику классов, вызывая систематическую синхронизацию для этого конкретного имени класса и официантов из других потоков, ожидающих получения блокировки для загрузки того же имени класса.

Многие потоки ожидают получения LOCK 0x00000000ab84c0c8…
"default task-15" prio=6 tid=0x0000000014849800 nid=0x2050 waiting for monitor entry [0x000000001009d000] 
  java.lang.Thread.State: BLOCKED (on object monitor)   
            at java.lang.ClassLoader.loadClass(ClassLoader.java:403) 

  - waiting to lock <0x00000000ab84c0c8> (a java.lang.Object)
 // Waiting to acquire a LOCK held by Thread “default task-20” 
  at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) 
  at java.lang.ClassLoader.loadClass(ClassLoader.java:356)   // JBoss now delegates to system ClassLoader..                                                          
  at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:371) 
  at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:119) 
  at java.lang.Class.forName0(Native Method) 
  at java.lang.Class.forName(Class.java:186) 
  at org.jboss.tools.examples.rest.MemberResourceRESTService.SystemCLFailure(MemberResourceRESTService.java:176) 
  at org.jboss.tools.examples.rest.MemberResourceRESTService$Proxy$_$$_WeldClientProxy.SystemCLFailure(Unknown Source) 
  at sun.reflect.GeneratedMethodAccessor15.invoke(Unknown Source)   
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 
  at java.lang.reflect.Method.invoke(Method.java:601) 

…………………… ..   
Поток Culprit — задание по умолчанию-20
"default task-20" prio=6 tid=0x000000000e3a3000 nid=0x21d8 runnable [0x0000000010e7d000] 
  java.lang.Thread.State: RUNNABLE   
  at java.lang.Throwable.fillInStackTrace(Native Method) 
  at java.lang.Throwable.fillInStackTrace(Throwable.java:782) 
  - locked <0x00000000a09585c8> (a java.lang.ClassNotFoundException) 
  at java.lang.Throwable.<init>(Throwable.java:287) 
  at java.lang.Exception.<init>(Exception.java:84) 
  at java.lang.ReflectiveOperationException.<init>(ReflectiveOperationException.java:75) 
at java.lang.ClassNotFoundException.<init>(ClassNotFoundException.java:82) // ClassNotFoundException!                                      at java.net.URLClassLoader$1.run(URLClassLoader.java:366)                                                          
  at java.net.URLClassLoader$1.run(URLClassLoader.java:355) 
  at java.security.AccessController.doPrivileged(Native Method) 
  at java.net.URLClassLoader.findClass(URLClassLoader.java:354) 
  at java.lang.ClassLoader.loadClass(ClassLoader.java:423) 
  - locked <0x00000000ab84c0e0> (a java.lang.Object)   
  at java.lang.ClassLoader.loadClass(ClassLoader.java:410) 
- locked <0x00000000ab84c0c8> (a java.lang.Object) // java.lang.ClassLoader: LOCK acquired                                                             at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)                                                  
  at java.lang.ClassLoader.loadClass(ClassLoader.java:356) 
  at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:371) 
  at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:119) 
  at java.lang.Class.forName0(Native Method) 
  at java.lang.Class.forName(Class.java:186) 
  at org.jboss.tools.examples.rest.MemberResourceRESTService.SystemCLFailure(MemberResourceRESTService.java:176)  at org.jboss.tools.examples.rest.MemberResourceRESTService$Proxy$_$$_WeldClientProxy.SystemCLFailure(Unknown Source)…………………………………

Now let’s replace our Class name by a Java class tagged as part of the “application” package and re-run the test under the same load conditions.
String className = «org.ph.WrongClassName»;
Class.
forName(className);

As we can see, we are no longer dealing with BLOCKED threads…why is that? Let’s have a look at the JVM thread dump to better understand this change of behaviour.
"default task-51" prio=6 tid=0x000000000dd33000 nid=0x200c runnable [0x000000001d76d000] 
  java.lang.Thread.State: RUNNABLE 
  at java.io.WinNTFileSystem.getBooleanAttributes(Native Method) // IO overhead due to JAR file search operation 
  at java.io.File.exists(File.java:772) 
  at org.jboss.vfs.spi.RootFileSystem.exists(RootFileSystem.java:99) 
  at org.jboss.vfs.VirtualFile.exists(VirtualFile.java:192) 
  at org.jboss.as.server.deployment.module.VFSResourceLoader$2.run(VFSResourceLoader.java:127) 
  at org.jboss.as.server.deployment.module.VFSResourceLoader$2.run(VFSResourceLoader.java:124) 
  at java.security.AccessController.doPrivileged(Native Method) 
  at org.jboss.as.server.deployment.module.VFSResourceLoader.getClassSpec(VFSResourceLoader.java:124) 
  at org.jboss.modules.ModuleClassLoader.loadClassLocal(ModuleClassLoader.java:252) 
  at org.jboss.modules.ModuleClassLoader$1.loadClassLocal(ModuleClassLoader.java:76) 
  at org.jboss.modules.Module.loadModuleClass(Module.java:526) 
  at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:189) // JBoss now fully responsible to load the class 
  at org.jboss.modules.ConcurrentClassLoader.performLoadClassUnchecked(ConcurrentClassLoader.java:444) // Unchecked since using JDK 1.7 e.g. tagged as “safe” JDK 
  at org.jboss.modules.ConcurrentClassLoader.performLoadClassChecked(ConcurrentClassLoader.java:432) 
  at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:374) 
  at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:119) 
  at java.lang.Class.forName0(Native Method)   
  at java.lang.Class.forName(Class.java:186) 
  at org.jboss.tools.examples.rest.MemberResourceRESTService.AppCLFailure(MemberResourceRESTService.java:196) 
  at org.jboss.tools.examples.rest.MemberResourceRESTService$Proxy$_$$_WeldClientProxy.AppCLFailure(Unknown Source)  at sun.reflect.GeneratedMethodAccessor60.invoke(Unknown Source) ……………….  
The above execution stack trace is quite revealing:

  • Since the Java class name was not detected to be part of the Java system package, no ClassLoader delegation was performed, thus no synchronization.
  • Since JBoss considers JDK 1.7+ as a “safe” JDK, the ConcurrentClassLoader .performLoadClassUnchecked() method was used, not triggering any Object monitor lock.
  • No synchronization means no Thread lock contention triggered as a result of the non-stop ClassNotFoundException error.
It is still important to note that while JBoss is doing a great job at preventing Thread lock contention in this scenario, the repetitive class loading attempt will still degrade the performance to a certain degree due to IO overhead associated with excessive JAR file search operations, again re-enforcing the need to take immediate corrective actions.

Final words
I hope that you enjoyed this article and now have a better understanding of potential performance impacts due to excessive class loading operations. While JDK 1.7 and modern Java EE containers brought great improvements on class loader related issues such as deadlocks and thread lock contention, potential problematic scenarios still remain. For this reason, I highly recommend that you closely monitor your application behaviour, logs and ensure that class loader related errors such as 
java.lang.ClassNotFoundException and
java.lang.NoClassDefFoundError are aggressively corrected.
I’m looking forward for your comments and please share your troubleshooting experience with Java class loaders.