Этот пост вдохновлен статьей Пьера-Хьюга Шарбонно о том, как загрузка несуществующих классов может серьезно повредить вашей производительности . Это вернуло воспоминания о сеансе отладки, где та же проблема возникла немного по-другому.
Приложение под рукой было «Ещё одно веб-приложение», выполнявшее любые мирские задачи. Пока этого больше нет. Симптомы включали в себя конечных пользователей, жалующихся на медленное применение и прямые тайм-ауты. Файлы журналов, к которым я получил доступ, ничего особенного не выявили, кроме подтверждения того, что пользовательский интерфейс был сильно ухудшен.
Ошибка попала в особенно неприятную категорию — быть среди ошибок, которые вы уверены, что можете воспроизвести, но не по желанию. Ситуация, казалось, возникала несколько случайным образом один или два раза в неделю. Поэтому я попросил ребят из отдела операций, ответственных за это приложение, запускать диагностический сценарий всякий раз, когда возникает такая ситуация. В этом сценарии не было ничего особенного — в основном он собирал несколько дампов потоков вместе с некоторыми дополнительными показателями уровня ОС. И помолился могущественному Богу-отладчику о помощи.
На этот раз, похоже, молитва сработала. Через пару дней я смотрел на дамп потока, заполненный следами, похожими на следующие:
"Thread-96" #106 prio=5 os_prio=31 tid=0x00007fc9b283b800 nid=0x11b03 waiting for monitor entry [0x0000000130133000] java.lang.Thread.State: BLOCKED (on object monitor) at java.lang.ClassLoader.loadClass(ClassLoader.java:404) - waiting to lock <0x00000006c022e608> (a java.lang.Object) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at com.sun.org.apache.xerces.internal.utils.ObjectFactory.findProviderClass(ObjectFactory.java:209) at com.sun.org.apache.xerces.internal.utils.ObjectFactory.newInstance(ObjectFactory.java:157) at com.sun.org.apache.xerces.internal.utils.ObjectFactory.newInstance(ObjectFactory.java:143) at com.sun.org.apache.xerces.internal.impl.dv.DTDDVFactory.getInstance(DTDDVFactory.java:64) at com.sun.org.apache.xerces.internal.impl.dv.DTDDVFactory.getInstance(DTDDVFactory.java:49) at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.(XML11Configuration.java:578) at com.sun.org.apache.xerces.internal.parsers.XIncludeAwareParserConfiguration.(XIncludeAwareParserConfiguration.java:130) at com.sun.org.apache.xerces.internal.parsers.XIncludeAwareParserConfiguration.(XIncludeAwareParserConfiguration.java:91) at com.sun.org.apache.xerces.internal.parsers.DOMParser.(DOMParser.java:144) at com.sun.org.apache.xerces.internal.parsers.DOMParser.(DOMParser.java:128) at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.(DocumentBuilderImpl.java:138) at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl.newDocumentBuilder(DocumentBuilderFactoryImpl.java:76) at ClassloadingLock$Worker.run(ClassloadingLock.java:10)
Подавляющее большинство потоков приложений были заблокированы и ожидали выпуска одного монитора. Интересным было то, что монитор синхронизировал доступ к загрузке классов. Это было то, чего не часто можно увидеть, особенно в приложении, которое было инициализировано и разогрето уже за несколько дней до этого события эскалации блокировки.
Копание в коде выявило проблему. Примерно один раз в неделю работа cron запускает множество служебных потоков. Эти потоки должны были извлекать и преобразовывать данные для загрузки в хранилище данных. Правила преобразования были описаны в XML, загруженном этими потоками с использованием javax.xml.parsers.DocumentBuilder . По какой-то причине каждый поток загружал сам XML-документ каждый раз, когда происходило преобразование.
Как видно из дампа потока, это приводит к загрузке класса провайдера XML каждый раз, когда должно было быть выполнено преобразование:
at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at com.sun.org.apache.xerces.internal.utils.ObjectFactory.findProviderClass(ObjectFactory.java:209)
И теперь, если мы проверяем исходный код самого java.lang.ClassLoader.loadClass () , мы действительно можем увидеть синхронизацию перед проверкой, загружен ли уже класс или мы должны обратиться к файловой системе, чтобы загрузить его из файла класса. Теперь, когда несколько потоков обращаются к этому синхронизированному блоку одновременно, нам удалось построить базовую линию для повышения блокировки:
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class c = findLoadedClass(name); // ... cut for brevity ... }
Пытаясь воссоздать проблему, я создал небольшой тестовый пример, чтобы вы лучше поняли проблему блокировки. Пример запускает 100 потоков, которые ничего не делают, но загружают класс javax.xml.parsers.DocumentBuilder :
package eu.plumbr.demo; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; public class ClassloadingLock { static class Worker extends Thread { @Override public void run() { while (true) { try { DocumentBuilder b = DocumentBuilderFactory.newInstance() .newDocumentBuilder(); } catch (Exception e) {// Do not do this at home, log all // exceptions } } } } public static void main(String[] args) throws Exception { for (int i = 0; i < 100; i++) { new Worker().start(); } } }
Когда вы выполняете дамп потока во время выполнения приведенного выше кода или визуализируете ситуацию с помощью jvisualvm , вы действительно увидите, что потоки постоянно ожидают за монитором.
Мораль истории? Мы не думаем, что найти причины проблем с производительностью должно быть так сложно. Мы надеемся, что инструменты, которые мы создадим, сделают навыки, необходимые для устранения неполадок в этом случае, неактуальными, поскольку у вас будет гораздо более удобный способ мониторинга и устранения неполадок в ваших JVM.