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

Материал из DOM

Перейти к: навигация, поиск

Полагаю, что те, кто профессионально занимается разработкой на java, знает что такое classpath и насколько он важен для правильной работы приложения. Когда среда выполнения java выполняет ваш код, например, такой:

package foo;
 
import java.util.List;
import java.util.ArrayList;
import java.util.Date;
import java.math.BigDecimal;
 
public class Boo {
    public static void main(String[] args) {
        List li =new ArrayList ();
        li.add(new Date());
        li.add(new BigDecimal(1000));
        System.out.println("li = " + li);
    }
}

То 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]

Собственно, на этом можно было остановиться и "поиском" проверить мою гипотезу. Но у меня было в запасе достаточное количество времени и я решил написать небольшую утилитку, которая бы читала файл лога, анализировала и представляла его в понятной и красивой форме.

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

Изображение:uncover_class_loading_1.png

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

Изображение:uncover_class_loading_2.png

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

Изображение:uncover_class_loading_3.png

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

Изображение:uncover_class_loading_4.png

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

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

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

Изображение:uncover_class_loading_5.png

Исходники проекта размещены здесь: исходники проекта анализируем в java загружаемые классы

А тут можно скачать jar с скомпилированным кодом (никаких зависимостей от посторонних библиотек у проекта нет: только java core). http://black-zorro.com/sources/java/compareloading.jar

Для запуска либо наберите в командной строке java -jar compareloading.jar, либо (при нормально установленной jre) нужно выполнить двойной клик по файлу compareloading.jar.


Subscribe Now!

 

ObMachine projects & articles (java, flash, flex, php, ...)  -- black-zorro.com