Java dynamic class loading (java.lang.ClassLoader)

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Это глава из будущей книги «Readings in Java Type System».
---
Вопросы, на которые дам ответы:
1) можно ли загрузить «системный класс» (String, Thread, Class, Object, ClassLoader, ...) by user defined class loader?
2) можно ли организовать повторную загрузку класса?
3) есть ли в Java выгрузка классов?
4) как происходят утечки памяти через загрузку классов у популярных библиотек?
5) анонимные загрузчики классов в Java 7
6) параллельная загрузка классов
7) инструментирование классов при загрузке
.
Пример: исключение при попытке «привести класс к самому себе»:

public class NameSpaceTest {
    public static void main(String[] args) throws Exception {
        final String className = NameSpaceTest.class.getName();
        final byte[] byteCodes = ClassLoaderUtil.loadByteCode(className);
        NameSpaceTest ref = (NameSpaceTest) new MyClassLoader()
                .defineClass(className, byteCodes)
                .newInstance();
    }
}
>> Exception in thread «main» java.lang.ClassCastException: NameSpaceTest cannot be cast to NameSpaceTest
.
Пример: переполнение PermGen
import static java.lang.String.format;
public class OutOfMemoryError_PermGen {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = OutOfMemoryError_PermGen.class;
        byte[] buffer = ClassLoaderUtil.loadByteCode(clazz.getName());
        MyClassLoader loader = new MyClassLoader();
        for (long index = 0; index < Long.MAX_VALUE; index++) {
            String newClassName = "_" + format("%0" + (clazz.getSimpleName().length() - 1) + "d", index);
            byte[] newClassData = new String(buffer, "latin1")
                    .replaceAll(clazz.getSimpleName(), newClassName)
                    .getBytes("latin1");
            System.out.println("Count of loaded classes: " + index);
            loader.defineClass(
                    clazz.getName().replace(clazz.getSimpleName(), newClassName),
                    newClassData);
        }
    }
}
Count of loaded classes: 0
...
Count of loaded classes: 31075
Exception in thread «main» java.lang.OutOfMemoryError: PermGen space
// todo: пересмотреть в виду PermGen->Metaspace в JRE 8 от Oracle.
.
Пример: демонстрация выгрузки классов
import static java.lang.String.format;
public class ClassUnloading {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = ClassUnloading.class;
        byte[] buffer = ClassLoaderUtil.loadByteCode(clazz.getName());
        MyClassLoader loader;
        for (long index = 1; index < Long.MAX_VALUE; index++) {
            String newClassName = "_" + format("%0" + (clazz.getSimpleName().length() - 1) + "d", index);
            byte[] newClassData = new String(buffer, "latin1")
                    .replaceAll(clazz.getSimpleName(), newClassName)
                    .getBytes("latin1");
            loader = new MyClassLoader();
            System.out.println("Count of loaded classes: " + index);
            loader.defineClass(
                    clazz.getName().replace(clazz.getSimpleName(), newClassName),
                    newClassData);
        }
    }
}
Count of loaded classes: 0
...
Count of loaded classes: 100000
...
Count of loaded classes: 1000000
...
Count of loaded classes: 10000000
...
.
Пример: демонстрация наличия отношения делегирования между загрузчиками классов
public interface Root {public Leaf getLeaf();}
public interface Leaf {}
public class LeafImpl implements Leaf {}
public class RootImpl implements Root {
    private Leaf leaf = new LeafImpl();

    @Override
    public Leaf getLeaf() {
        return leaf;
    }
}
public class ChainTest {
    public static void main(String[] args) throws Exception {
        final String className = RootImpl.class.getName();
        final byte[] byteCodes = ClassLoaderUtil.loadByteCode(className);
        // comparing RootImpl classes
        Root rootA = (Root) loadClass(className, byteCodes);
        Root rootB = (Root) loadClass(className, byteCodes);
        System.out.println(rootA.getClass() == rootB.getClass());
        // comparing LeafImpl classes
        Leaf leafA = rootA.getLeaf();
        Leaf leafB = rootB.getLeaf();
        System.out.println(leafA.getClass() == leafB.getClass());
    }
    private static Object loadClass(String className, byte[] byteCodes) throws Exception {
        return new MyClassLoader().defineClass(className, byteCodes).newInstance();
    }
}
false
true
.
Пример: демонстрация наличия кэша загруженных классов
public class ClassCache {
    public static void main(String[] args) throws Exception {
        String name = ClassCache.class.getName();
        byte[] byteCodes = ClassLoaderUtil.loadByteCode(name);
        // load - load
        MyClassLoader loaderLL = new MyClassLoader();
        Class<?> clazzLLA = loaderLL.loadClass(name);
        Class<?> clazzLLB = loaderLL.loadClass(name);
        System.err.println("load " + (clazzLLA == clazzLLB ? "==" : "!=") + " load");
        // load - define
        MyClassLoader loaderLD = new MyClassLoader();
        Class<?> clazzLDA = loaderLD.loadClass(name);
        Class<?> clazzLDB = loaderLD.defineClass(name, byteCodes);
        System.err.println("load " + (clazzLDA == clazzLDB ? "==" : "!=") + " define");
        // define - load
        MyClassLoader loaderDL = new MyClassLoader();
        Class<?> clazzDLA = loaderDL.defineClass(name, byteCodes);
        Class<?> clazzDLB = loaderDL.loadClass(name);
        System.err.println("define " + (clazzDLA == clazzDLB ? "==" : "!=") + " load");
        // define - define
        MyClassLoader loaderDD = new MyClassLoader();
        Class<?> clazzDDA = loaderDD.defineClass(name, byteCodes);
        Class<?> clazzDDB = loaderDD.defineClass(name, byteCodes);
        System.err.println("define " + (clazzDDA == clazzDDB ? "==" : "!=") + " define");
    }
}
load == load
load != define
define == load
... LinkageError: ... attempted duplicate class definition ...
.
Пример:
.
Утилитарные классы:
public class MyClassLoader extends ClassLoader {
    public Class<?> defineClass(String name, byte[] byteCodes) {
        return super.defineClass(name, byteCodes, 0, byteCodes.length);
    }
}
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
public class ClassLoaderUtil {
    public static byte[] loadByteCode(String className) throws IOException {
        String fileName = "/" + className.replaceAll("\\.", "/") + ".class";
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        URL url = ClassLoaderUtil.class.getResource(fileName);
        Files.copy(Paths.get(url.getPath().substring(1)), buffer);
        return buffer.toByteArray();
    }
}
.
Литература
[DCLinJVM] S. Liang, G. Bracha, «Dynamic Class Loading in the JavaTM Virtual Machine», 1998
👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Коментар порушує правила спільноти і видалений модераторами.

А ставить символ переноса строки, отделяя логические блоки кода — это уже не модно?

Старался минимизировать объем примера.
Правильнее, конечно, разбить на блоки и покрыть камментами.

А что происходит в «демонстрация наличия отношения делегирования между загрузчиками классов»?

У каждого экземпляра ClassLoader есть поле parent типа ClassLoader. Его значение либо передаешь в конструкторе, либо автоматически выбирается system class loader.
Скорее всего, это — загрузчик приложения (но там все кастомизируемо).
Так вот, «правильный class loader» (с неперегруженной версией loadClass()) в начале делегирует загрузку класса к parent, и только если тот не справился, то ищет сам.
Для класса RootImpl я явно указал загрузить класс своими созданными загрузчиками — и это разные классы, но неявная загрузка для LeafImpl делегировалась к parent, а parent для обоих загрузчиков — это application class loader, и, поэтому, два разных загрузчика таки определили один класс LeafImpl.
---
В статье четко различают initiating class loader (у которого вызвали loadClass) и defining class loader (который после цепочки делегирования таки выполнил defineClasss).

Таким образом у нас автоматически строится «дерево пространств имен».
Скажем, если я в томкате загрузил три варника, то java.lang.String загружен один раз загрузчиком томката и разделяется всеми варниками, а вот каждый варник со своими либками грузится отдельным загрузчиком.
Это позволяет каждому варнику иметь свой вариант классов, скажем, Log4j, не пересекаясь c другими варниками.

Если добавить в конец примера строки

System.out.println(rootA.getClass().getClassLoader());
System.out.println(rootB.getClass().getClassLoader());
System.out.println(leafA.getClass().getClassLoader());
System.out.println(leafB.getClass().getClassLoader());
то в консоли будет
net.golovach.jts.classloader.MyClassLoader@3b3219ed
net.golovach.jts.classloader.MyClassLoader@69e0312b
sun.misc.Launcher$AppClassLoader@89ffb18
sun.misc.Launcher$AppClassLoader@89ffb18

Ок, просто из кода совсем непонятно откуда у root берется leaf.

Залил, спасибо за замечание.

Это глава из будущей книги «Readings in Java Type System».
---
Не густо :)
Серьезно, заведите себе твиттер, выложите исходники книги на гитхаб, но смыла плодить темы на ДОУ — 0, развешо дешевый самопиар (и не факт что положительный)

Підписатись на коментарі