Статьи

Под капотом JVM — Загрузчики классов

Загрузчики классов являются низким уровнем и часто игнорируются аспектом языка Java среди многих разработчиков. В ZeroTurnaround нашим разработчикам приходилось жить, дышать, есть, пить и почти общаться с загрузчиками классов, чтобы создать технологию JRebel, которая взаимодействует на уровне загрузчиков классов для обеспечения перезагрузки классов в режиме реального времени, избегая длительных циклов перестроения / переупаковки / повторного развертывания .
Вот некоторые вещи, которые мы узнали о загрузчиках классов, в том числе некоторые советы по отладке, которые, мы надеемся, сэкономят ваше время и потенциальную работу в будущем.

Загрузчик классов — это простой Java-объект

Да, в этом нет ничего умного, ну кроме системного загрузчика классов в JVM, загрузчик классов — это просто объект Java! Это абстрактный класс ClassLoader, который может быть реализован созданным вами классом. Вот API:

01
02
03
04
05
06
07
08
09
10
11
12
13
public abstract class ClassLoader {
 
 public Class loadClass(String name);
 
 protected Class defineClass(byte[] b);
 
 public URL getResource(String name);
 
 public Enumeration getResources(String name);
 
 public ClassLoader getParent()
 
}

Выглядит довольно просто, верно? Давайте рассмотрим метод по методу. Центральный метод — это loadClass, который просто берет имя класса String и возвращает вам фактический объект Class. Это метод, который, если вы ранее использовали загрузчики классов, вероятно, наиболее знаком, поскольку он наиболее часто используется в повседневном кодировании. defineClass — это последний метод в JVM, который берет байтовый массив из файла или местоположения в сети и выдает тот же результат, объект Class.

Загрузчик классов также может найти ресурсы из пути к классам. Он работает аналогично методу loadClass. Есть несколько методов, getResource и getResources, которые возвращают URL или Перечисление URL, которые указывают на ресурс, который представляет имя, переданное в качестве ввода в метод.

У каждого загрузчика классов есть родитель; getParent возвращает родителя загрузчиков классов, который не связан с наследованием Java, а является соединением в стиле связанного списка. Мы рассмотрим это чуть позже.

Загрузчики классов ленивы, поэтому классы загружаются только тогда, когда они запрашиваются во время выполнения. Классы загружаются ресурсом, который вызывает класс, поэтому класс во время выполнения может быть загружен несколькими загрузчиками классов в зависимости от того, откуда на них ссылаются и какой загрузчик классов загрузил классы, на которые ссылаются … ой, я пошел косоглазие! Давайте посмотрим на некоторый код.

01
02
03
04
05
06
07
08
09
10
11
public class A {
 
 public void doSmth() {
 
    B b = new B();
 
    b.doSmthElse();
 
 }
 
}

Здесь у нас есть класс A, вызывающий конструктор класса B в пределах его методов. Под прикрытием это то, что происходит

1
A.class.getClassLoader().loadClass(“B”);

Загрузчик классов, который изначально загружал класс A, вызывается для загрузки класса B.

Загрузчики классов иерархичны, но, как и дети, они не всегда спрашивают своих родителей

Каждый загрузчик классов имеет родительский загрузчик классов. Когда у загрузчика классов запрашивается класс, он обычно идет прямо к родительскому загрузчику классов, сначала вызывая loadClass, который, в свою очередь, может запросить своего родителя и так далее. Если два загрузчика классов с одним и тем же родителем попросят загрузить один и тот же класс, родительский процесс сделает это только один раз. Это очень хлопотно, когда два загрузчика классов загружают один и тот же класс по отдельности, так как это может вызвать проблемы, которые мы рассмотрим позже.

Когда была разработана спецификация JEE, веб-загрузчик классов был разработан для работы в обратном направлении — отлично. Давайте посмотрим на рисунок ниже в качестве нашего примера.


Модуль WAR1 имеет собственный загрузчик классов и предпочитает загружать классы сам, а не делегировать его родителю, загрузчик классов ограничен App1.ear. Это означает, что разные модули WAR, такие как WAR1 и WAR2, не могут видеть классы друг друга. Модуль App1.ear имеет собственный загрузчик классов и является родительским для загрузчиков классов WAR1 и WAR2. Загрузчик классов App1.ear используется загрузчиками классов WAR1 и WAR2, когда им необходимо делегировать запрос вверх по иерархии, то есть класс требуется вне области действия загрузчика классов WAR. Фактически классы WAR переопределяют классы EAR там, где они существуют. Наконец родитель родительского загрузчика EAR — контейнерный загрузчик классов. Загрузчик классов EAR делегирует запросы загрузчику классов контейнера, но он не делает это так же, как загрузчик классов WAR, так как загрузчик классов EAR фактически предпочитает делегировать, а не локальные классы. Как вы можете видеть, это становится довольно проблематичным и отличается от обычного поведения загрузки классов JSE.

Плоский класс

Мы говорили о том, как системный загрузчик классов смотрит на путь к классам, чтобы найти классы, которые были запрошены. Этот classpath может включать каталоги или файлы JAR, и порядок их просмотра на самом деле зависит от используемой вами JVM. Для пути к классам может потребоваться несколько копий или версий класса, но вы всегда получите первый экземпляр класса, найденный в пути к классам. По сути, это просто список ресурсов, поэтому он называется плоским. В результате список путей к классам часто может быть относительно медленным, чтобы перебирать при поиске ресурса.

Проблемы могут возникать, когда приложения, использующие один и тот же путь к классам, хотят использовать разные версии класса. Давайте в качестве примера рассмотрим Hibernate. Если на пути к классам существуют две версии JAR Hibernate, одна версия не может быть выше пути к классу для одного приложения, чем для другого, что означает, что обеим сторонам придется использовать одну и ту же версию. Одним из способов решения этой проблемы является разгрузка приложения (WAR) всеми необходимыми библиотеками, чтобы они использовали свои локальные ресурсы, но это приводит к большим приложениям, которые трудно поддерживать. Добро пожаловать в банку ада! Здесь OSGi предлагает решение, так как оно позволяет создавать версии файлов JAR или пакетов, что приводит к механизму, позволяющему подключаться к определенным версиям файлов JAR, избегая проблем плоского пути к классам.

Как отладить ошибки загрузки моего класса?

NoClassDefFoundError / ClassNotFoundException / ClassNoDefFoundException?

Итак, у вас есть ошибка / исключение, как показано выше. Ну, класс действительно существует? Не беспокойтесь о поиске в вашей IDE, поскольку именно там вы скомпилировали свой класс, он должен быть там, иначе вы получите исключение времени компиляции. Это исключение во время выполнения, поэтому во время выполнения мы хотим найти класс, в котором говорится, что мы пропустили … но с чего начать? Рассмотрим следующий фрагмент кода …

1
2
Arrays.toString((((URLClassLoader) Test.class.getClassLoader())
                                                   .getURLs()));

Этот код возвращает список массивов всех jar-файлов и каталогов на пути к классам загрузчика классов, который использует класс Test. Итак, теперь мы можем видеть, находится ли JAR или место, в котором должен существовать наш таинственный класс, на пути к классам. Если его не существует, добавьте его! Если он существует, проверьте каталог JAR /, чтобы убедиться, что ваш класс действительно существует в этом месте, и добавьте его, если он отсутствует. Это две типичные проблемы, которые приводят к этому случаю ошибки.

NoSuchMethodError / NoSuchFieldError / AbstractMethodError / IllegalAccessError?

Теперь становится интересно! Все это подклассы IncompatibleClassChangeError. Мы знаем, что загрузчик классов нашел класс, который нам нужен (по имени), но он явно не нашел нужную версию.
Здесь у нас есть класс с именем Test, который вызывает другой класс, Util, но BANG — мы получаем исключение! Давайте посмотрим на следующий фрагмент кода для отладки:

1
2
Test.class.getClassLoader().getResource(Util.class.getName()
                                .replace('.', '/') + ".class");

Мы вызываем getResource для загрузчика классов класса Test. Это возвращает нам URL ресурса Util. Обратите внимание, что мы заменили «.» с ‘/’ и добавил ‘.class’ в конце строки. Это изменяет пакет и имя класса искомого класса (с точки зрения загрузчика классов) в структуру каталогов и имя файла в файловой системе — аккуратно. Это покажет нам точный класс, который мы загрузили, и мы сможем убедиться, что это правильная версия. Мы можем использовать javap -private для класса в командной строке, чтобы увидеть байт-код и проверить, какие методы и поля действительно существуют. Вы можете легко увидеть структуру класса и проверить, безумно ли это вы или среда исполнения Java! Поверьте мне, на том или ином этапе вы будете задавать вопросы обоим, и почти каждый раз это будете вы!

LinkageError / ClassCastException / IllegalAccessError

Это может произойти, если два разных загрузчика классов загружают один и тот же класс, и они пытаются взаимодействовать … ой! Да, сейчас становится немного волосатым. Это может вызвать проблемы, так как мы не знаем, будут ли они загружать классы из одного места. Как это может случиться? Давайте посмотрим на следующий код, все еще в классе Test:

1
Factory.instance().sayHello();

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


Здесь мы видим, что веб-загрузчик классов (который загрузил класс Test) предпочтет локальные классы, поэтому, когда он ссылается на класс, он будет загружен веб-загрузчиком классов, если это возможно. Пока довольно просто. Класс Test использует класс Factory для получения экземпляра класса Util, что является довольно типичной практикой в ​​Java, но класс Factory не существует в WAR, поскольку он является внешней библиотекой. Это не проблема, поскольку веб-загрузчик классов может делегировать общий загрузчик классов, который может видеть класс Factory. Обратите внимание, что совместно используемый загрузчик классов теперь загружает свою собственную версию класса Util, так как при создании экземпляра класса Factory он использует совместно используемый загрузчик классов (как показано в первом примере ранее). Класс Factory возвращает объект Util (созданный совместно используемым загрузчиком классов) обратно в WAR, который затем пытается использовать класс и эффективно приводит класс к потенциально другой версии того же класса (класс Util, видимый для веб-загрузчика классов ). БУМ!

Мы можем запустить тот же код, что и раньше, в обоих местах (метод Factory.instance () и класс Test), чтобы увидеть, откуда загружается каждый из наших классов Util.

1
2
Test.class.getClassLoader().getResource(Util.class.getName()
                                .replace('.', '/') + ".class"));

Надеюсь, это дало вам представление о мире загрузки классов, и вместо того, чтобы не понимать загрузчик классов, теперь вы можете оценить это с намеком на страх и неуверенность! Спасибо за чтение и доведение до конца. Мы все хотели бы пожелать вам счастливого Рождества и счастливого нового года от ZeroTurnaround! Удачного кодирования!

Ссылка: Под капотом JVM — загрузчики классов от нашего партнера JCG Саймона Мэйпла в блоге Java Advent Calendar .