Большинство разработчиков 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);
- JRE: HotSpot JDK 1.7 @ 64-bit
- Контейнер Java EE: JBoss WildFly 8
- Инструмент нагрузочного тестирования: Apache JMeter
- Мониторинг Java: JVisualVM
- Устранение неполадок параллелизма Java: анализ дампа потока JVM
Симуляция по существу выполняется одновременно с 20 потоками веб-службы JAX-RS. Каждый вызов генерирует исключение ClassNotFoundException. Ведение журнала было полностью отключено, чтобы уменьшить влияние на ввод-вывод и сосредоточиться только на конкуренции за загрузку класса.
Теперь давайте посмотрим на результаты JVisualVM из нашего 30-60 секундного прогона. Мы можем ясно видеть множество BLOCKED-потоков, ожидающих блокировки на мониторе объектов.
Анализ дампа потока JVM ясно выявляет проблему: Конфликт блокировки потока. Мы можем видеть из трассировки стека выполнения, что JBoss делегирует загрузку класса JLK ClassLoader… почему? Это потому, что наше неправильное имя класса Java обнаружено как часть пути к системному классу, например, java. *. В этой ситуации JBoss делегирует загрузку системному загрузчику классов, инициируя систематическую синхронизацию для этого конкретного имени класса и официантов из других потоков, ожидающих получения блокировки для загрузки того же имени класса.
Многие потоки ожидают получения LOCK 0x00000000ab84c0c8…
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
"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
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
"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)………………………………… |
Теперь давайте заменим наше имя класса классом Java, помеченным как часть пакета «application», и повторно запустим тест при тех же условиях загрузки.
|
1
2
|
String className = "org.ph.WrongClassName";Class.forName(className); |
Как мы видим, мы больше не имеем дело с ЗАБЛОКИРОВАННЫМИ потоками … почему это так? Давайте посмотрим на дамп потока JVM, чтобы лучше понять это изменение поведения.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
"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) ………………. |
Приведенная выше трассировка стека выполнения довольно показательна:
- Поскольку не было обнаружено, что имя класса Java является частью системного пакета Java, делегирование ClassLoader не было выполнено, поэтому синхронизация не выполнялась.
- Поскольку JBoss рассматривает JDK 1.7+ как «безопасный» JDK, был использован метод ConcurrentClassLoader .performLoadClassUnchecked (), не вызывающий блокировку монитора объекта.
- Отсутствие синхронизации означает отсутствие конфликта блокировки потока, вызванного непрерывной ошибкой ClassNotFoundException.
По-прежнему важно отметить, что, хотя JBoss делает большую работу по предотвращению конфликта блокировок потоков в этом сценарии, попытка повторяющейся загрузки классов все равно приведет к некоторому снижению производительности из-за накладных расходов ввода-вывода, связанных с чрезмерными операциями поиска файлов JAR, опять же усиливая необходимость принятия немедленных корректирующих действий.
Заключительные слова
Я надеюсь, что вам понравилась эта статья, и теперь вы лучше понимаете потенциальное влияние на производительность из-за чрезмерной загрузки классов. Хотя JDK 1.7 и современные контейнеры Java EE принесли значительные улучшения в таких проблемах, связанных с загрузчиком классов, как взаимоблокировки и конфликты блокировок потоков, потенциальные проблемные сценарии все еще остаются. По этой причине я настоятельно рекомендую вам внимательно следить за поведением своего приложения, журналами и гарантировать, что ошибки, связанные с загрузчиком классов, такие как java.lang.ClassNotFoundException и java.lang.NoClassDefFoundError , агрессивно исправляются.
Я с нетерпением жду ваших комментариев и, пожалуйста, поделитесь опытом устранения неполадок с загрузчиками классов Java



