Анализируем в java загружаемые классы

October 27, 2008Comments Off on Анализируем в java загружаемые классы

Полагаю, что те, кто профессионально занимается разработкой на java, знает что такое classpath и насколько он важен для правильной работы приложения. Когда среда выполнения java выполняет ваш код, например, такой:
  1. package foo;
  2.  
  3. import java.util.List;
  4. import java.util.ArrayList;
  5. import java.util.Date;
  6. import java.math.BigDecimal;
  7.  
  8. public class Boo {
  9.  
  10.     public static void main(String[] args) {
  11.         List li =new ArrayList ();
  12.         li.add(new Date());
  13.         li.add(new BigDecimal(1000));
  14.         System.out.println("li = " + li);
  15.     }
  16.  
  17. }
То classloader загружает следующий перечень классов: java.util.List, java.util.ArrayList, java.util.Date, java.math.BigDecimal и еще один миллион классов, от которых прямо или косвенно зависят используемые мною объекты и классы: java.lang.Object, Collection, PrintStream, Number, Comparable ...

Когда поступает запрос на загрузку класса, например, Number, то этот запрос проходит через цепочку специализированных объектов classloader-ов. Есть встроенные classloader-ы и есть те, которые вы можете добавлять сами, например, для загрузки классов/ресурсов из неизвестных на стадии запуска приложения источников (например, загрузка классов плагинов из jar-файлов подкладываемых в каталог plugins приложения).

Итак, есть три встроенных classloader-а, у каждого из которых есть родительский classloader (кроме первого, изначального). Когда необходимо выполнить загрузку класса, то последовательно вызываются шаги:

3. System class loader. Прежде всего делегирует вызов на загрузку класса своему родительскому classloader-у (номер 2). И только если родитель не смог загрузить класс, то пытается выполнить загрузку сам, из classpath. Список источников классов (jar-архивов и каталогов с *.class-файлами) указывается при запуске java-машины как параметр командной строки. Помните, что первый архив обладает приоритетом перед последующими. Т.е. System class loader просматривает эти архивы по очереди, в поисках определения нужного класса. Так что засорение classpath (указание в нем тысячи и одной библиотеки) несет массу проблем. Например, загрузка не той версии класса, что нужна для работы остальной части приложения.
java -cp path-to-lib-1.jar;path-to-lib-2.jar;path-to-directory package1.package2.ClassFoo
2. Extension class loader. Снова делегирует вызов загрузки к своему родителю (номер 1) и только, если родитель не смог помочь то класс ищется в библиотеках размещенных в каталоге lib/ext вашей jre. Каталог lib/ext это специально выделенное место для размещения классов образующих java platform extension. Проще говоря, это набор классов, которые расширяют возможности java-платформы и которые не должны быть помещены в classpath. Т.е. такие jar-ки ен являясь частью jre могут быть использованы всеми приложениями для расширения их возможностей. Примерами расширений является java3d, java media framework и т.д. Главное в том, что когда вы создаете приложение нуждающееся в этих зависимостях, то указываете в файле manifest-а META-INF/manifest.mf перечисление того, какие расширения вам нужны, версии этих расширений и то откуда нужно загрузить ресурсы, в том случае, если их нет в lib/ext каталоге на вашей машине.

1. Bootstrap class loader. Классы загружаются из rt.jar. Надо отметить, что sun-овцы создали механизм позволяющий внести изменения даже в процесс загрузки java-ядра. Т.е. bootstrap вовсе не является последней инстанцией в определении того откуда будут загружаться классы, и вы можете подменить реализацию даже такой фундаментальной вещи как java.lang.Object на свою собственную (ну наверное можно, я никогда такого не пробовал, хотя и интересно). Для этого при запуске jre вы указываете следующие параметры командной строки (взято из справки):
-Xbootclasspath:bootclasspath
    Specify a semicolon-separated list of directories, JAR archives, and ZIP archives to search for boot class files. 
These are used in place of the boot class files included in the Java 2 SDK. Note: Applications that use this option for 
the purpose of overriding a class in rt.jar should not be deployed as doing so would contravene the Java 2 
Runtime Environment binary code license.
-Xbootclasspath/a:path
    Specify a semicolon-separated path of directires, JAR archives, and ZIP archives to append to the default bootstrap class path.
-Xbootclasspath/p:path
    Specify a semicolon-separated path of directires, JAR archives, and ZIP archives to prepend in front of 
the default bootstrap class path. Note: Applications that use this option for the purpose of overriding 
a class in rt.jar should not be deployed as doing so would contravene the Java 2 Runtime Environment 
binary code license.
Итак вернемся назад к причинам побудившим меня написать данную заметку. Был у меня проект, работал он себе, работал и никого не трогал. А потом я решил его немного оптимизировать, а он взял взял и "поломался": начало вылетать исключение ClassCastException. Ошибка в том, что класс A вдруг оказался не совместимым с классом B (причем гарантированно известно, что A является наследником от B). Подобная ситуация может происходить только в том случае, если классы были загружены разными classloader-ами: т.е. есть класс A загруженный класслоадером C1 из архива A.jar и тот же самый класс из того же самого архива A1 был загружен класслоадером C2. По-правилам требуется, чтобы любой classloader, перед тем как начнет загружать класс, обратился к своему родительскому classloader-у и попросил его загрузить ресурс. И только в том случае, если родитель не смог этого сделать, то класс должен загружаться самим classloader-ом. Фактически, если эти два класслоадера C1 и C2 имеют один общий родительский класслоадер, который в состоянии загрузить определение класса, то такая ошибка никогда не возникнет. На этом приеме основано написание плагинов, когда описание класса загружается с помощью своего класслоадера, однако этот класс обязан реализовывать специальный интерфейс, загружаемый родительским класслоадером для класслоадера плагина и для класслоадера самого приложения. Или класслоадер приложения являются родительским для класслоадера плагина. Вся беда в том, что генерируемое исключение происходило настолько глубоко внутри используемой инфраструктуры сервера приложений (естественно, продукта с закрытым исходным кодом), что понять почему и как невозможно. Анализ внесенных мною изменений также не дал никаких подсказок. Анализ classpath-а сервера также ничего не подсказал. Как шаг решения проблемы мне нужно было узнать какие классы и из каких ресурсов загружаются. Т.е. если какой-то класс (тот самый или связанный с ним по линии наследования был загружен дважды), то это подтверждает мою догадку об конфликте класслоадеров. Ну а дальше дело практики: подменить класс, удаленный debug, trace стека и google, google, google.

При запуске виртуальной машины можно указать специальный параметр, включающий вывод на экран сведений об том какие классы и из каких источников загружаются.
java -verbose:class -classpath foo.jar;bar.jar my.Mega > log.txt (перенаправляю вывод сообщений с экрана в текстовый файл)
Выглядит такой лог примерно так:
[Opened E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.lang.Object from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.io.Serializable from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.lang.Comparable from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.lang.CharSequence from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.lang.String from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.lang.reflect.Type from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.lang.Class from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.lang.Cloneable from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.lang.ClassLoader from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
[Loaded java.lang.System from E:\Program_Files_2\j2dk1.6.0\jre\lib\rt.jar]
Собственно, на этом можно было остановиться и "поиском" проверить мою гипотезу. Но у меня было в запасе достаточное количество времени и я решил написать небольшую утилитку, которая бы читала файл лога, анализировала и представляла его в понятной и красивой форме.

В верхней части окна размещены текстовые поля и кнопки выбора файлов журнала.



Указав имена файлов, вы жмете на кнопку "Compare" и после секундного ожидания видите что все закладки были заполнены информацией об загружаемых классах. На первой и второй закладках перечисляется список тех пар (класс и файл из которого он был загружен) которые соответствуют первому и второму файлу.



Информация представляется в двух формах: таблице и дереве. Как видите дерево устроено очень просто: список файлов образуют набор узлов первого уровня, в которые вложены узлы соответствующие загруженным классам. Для каждого файла-ресурса указывается статистика, сколько классов из файла было загружено.



Т.к. количество классов может быть (вернее всего будет) очень большим, то я добавил функцию фильтрации результатов по имени класса. Просто введите в текстовое поле выражение, содержащее часть имени класса (можно использовать символ * для задания шаблона). Нажав ввод вы примените фильтр (если поле пустое, то фильтр будет отменен).



Третья закладка "Common classes" похожа на первые две по функционалу, но представляет сведения об тех классах, которые были загружены из одних и тех же ресурсов в первом и втором случае.

Закладки "In 1 not 2" и "In 2 not 1", очевидно, хранят сведения об том какие классы были загружены в первом случае, но не загружены во втором и наоборот (совпадение проверяется по паре класс-файл).

Последняя закладка "Differect class storages" устроена отлично от ранее описанных: здесь только одна таблица с тремя колонками: "file 1", "clazz", "file 2". Т.е. так можно отследить ситуацию, когда один и тот же класс был загружен из двух различных ресурсов.



Исходники проекта размещены здесь: /java/compareloading

Categories: Java