Статьи

ClassLoaderLocal: как избежать утечек ClassLoader при повторном развертывании приложения

«OutOfMemoryError: PermGen» — очень распространенное сообщение, которое можно увидеть после нескольких повторных развертываний. Причина, по которой это так распространено, заключается в том, что утечка загрузчика классов поразительно проста. Достаточно хранить одну внешнюю ссылку на объект, созданный из класса, загруженного упомянутым загрузчиком классов, чтобы этот загрузчик классов не был GC-d.

В этом посте я расскажу, как мы решили эту проблему в JavaRebel , и поделюсь этим решением с вами. Это не волшебное решение, но оно поможет облегчить некоторые проблемы, возникающие как в библиотеках, так и в приложениях в Java EE.

Самый распространенный способ утечки — зарегистрировать некоторый объект обратного вызова и никогда не отменять его регистрацию. Например, посмотрите на следующий код:

Core.addListener(new MyListener());

Если Core является частью фреймворка / платформы / контейнера, то он будет храниться в MyListener еще долго после того, как приложение будет повторно развернуто, а загрузчик классов останется зависшим.

 

Посмотрим, сможем ли мы что-нибудь сделать, чтобы решить эту проблему. Реализация Core выглядит примерно так:

public class Core {
List listeners = new ArrayList();

void addListener(Listener l) {
listeners.add(l);
}

void fireListeners() {
// Exercise for the reader!
}
}

Проблема заключается в том, что слушатели предоставляют сильную ссылку на объект Listener. Что если мы заменим его слабым ?

public class Core {
List listeners = new ArrayList();

void addListener(Listener l) {
listeners.add(new WeakReference(l));
}

void fireListeners() {
// Exercise for the reader!
}
}

К сожалению, хотя это решает проблему GC-загрузчика классов, на самом деле это не работает. Слушатель, стоящий за слабой ссылкой, будет GC-d при первой возможности, и после этого он больше не будет получать никаких обратных вызовов. Чтобы проиллюстрировать, почему это проблема, приведенный выше код в основном эквивалентен удалению ссылки в целом:

public class Core {
List listeners = new ArrayList();

void addListener(Listener l) {
// Listener is ignored and GC-d
}
}

Замена слабой ссылки на мягкую не улучшает ситуацию, а лишь задерживает неизбежное немного дальше. Оба полезны для кешей, где объекты могут быть воссозданы по желанию, но не в этом случае, когда у нас есть внешне созданный объект.

Так что же нам делать? Нам бы хотелось, чтобы ссылка Listener как-то зависела от загрузчика классов. К сожалению, насколько мне известно, не существует готового решения для этого, и нет способа достичь его с помощью любых комбинаций слабых ссылок, не вызывая проблем.

Мы хотели бы иметь возможность добавить сильную ссылку на загрузчик классов: другими словами, он имеет собственное свойство:

void addListener(Listener l) {
ClassLoader cl = l.getClass().getClassLoader();
List lls = (List) cl.getProperty("CoreListeners");
if (lls == null) {
lls = new ArrayList();
cl.putProperty("CoreListeners", lls);
}
lls.add(l);
}

Это сработало бы, не так ли? Ну, не совсем. Нам также нужно сохранить ссылку на загрузчики классов, чтобы позже мы могли просмотреть их все. Здесь WeakHashMap полезен:

Map classLoaders = new WeakHashMap();

void addListener(Listener l) {
//...
classLoaders.put(cl, Boolean.TRUE);
}

В Java нет WeakHashSet, поэтому мы просто используем логический флаг в качестве значения.

Так что это, вероятно, сработает, но, к сожалению, загрузчики классов не имеют API getProperty () / putProperty (). Однако оказывается, что с помощью небольшого взлома мы можем смоделировать его, генерируя уникальный класс для каждого загрузчика классов, который будет содержать свойства для нас. Посмотрим как это делается!

Начнем с небольшого шаблона:

class ClassLoaderLocalMap {
private static Method defineMethod;
private static Method findLoadedClass;

static {
try {
defineMethod = ClassLoader.class.getDeclaredMethod(
"defineClass",
new Class[] {
String.class,
byte[].class,
int.class,
int.class });
defineMethod.setAccessible(true);

findLoadedClass =
ClassLoader.class.getDeclaredMethod(
"findLoadedClass",
new Class[] { String.class});
findLoadedClass.setAccessible(true);
}
catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}

Это даст нам доступ к защищенным методам ClassLoader defineClass () и findLoadedClass () позже. Теперь давайте настроим базовый API:

public static void put(
ClassLoader cl,
Object key,
Object value) {
// Synchronizing over ClassLoader is safest
synchronized (cl) {
getLocalMap(cl).put(key, value);
}
}

public static Object get(
ClassLoader cl,
Object key) {
// Synchronizing over ClassLoader is safest
synchronized (cl) {
return getLocalMap(cl).get(key);
}
}

Метод getLocalMap () должен возвращать карту записей, связанных с загрузчиком классов. Как это должно работать?

Далее мы представляем карту от загрузчиков классов до уникальных имен классов держателей. Мы также представляем метод nextHolderName (), который генерирует уникальные имена:

private static final Map classLoaderToHolderClassName = 
Collections.synchronizedMap(new WeakHashMap())
private static int counter = 1;

private static synchronized String nextHolderName() {
return "ClassLoaderLocalMapHolder$$GEN$$" + counter++;
}

Наконец, мы можем реализовать метод getLocalMap () (для экономии места я удалил всю обработку исключений):

private static Map getLocalMap(ClassLoader cl) {
String holderClassName =
(String) classLoaderToHolderClassName.get(cl);
if (holderClassName == null) {
holderClassName= nextHolderName();
classLoaderToHolderClassName.put(
cl, holderClassName);
}

Class holderClass =
(Class) findLoadedClass.invoke(
cl,
new Object[] {propertiesClassName});

if (holderClass == null) {
byte[] classBytes =
buildHolderByteCode(holderClassName);

holderClass = (Class) defineMethod.invoke(cl,
new Object[] {
holderClassName,
classBytes,
new Integer(0),
new Integer(classBytes.length)});
}

return (Map) holderClass
.getDeclaredField("localMap").get(null);
}

Последний метод для реализации — buildHolderByteCode. Это довольно тривиально и создает следующий класс, переименованный в уникальное имя:

public class ClassLoaderLocalMapHolder$$GEN$$X {
public static final Map localMap = new HashMap();
}

Код может быть получен с использованием ASMifier с небольшой настройкой, вы можете посмотреть его в полном исходном коде.

Хотя теперь мы могли бы легко реализовать исходный пример, имеет смысл сделать немного больше усилий и ввести ClassLoaderLocal с поведением, аналогичным ThreadLocal:

public class ClassLoaderLocal {
private Object key = new Object();

public Object get(ClassLoader cl) {
if (!ClassLoaderProperties.containsKey(cl, key))
return null;
return ClassLoaderProperties.get(cl, key);
}

public void set(ClassLoader cl, Object value) {
ClassLoaderProperties.put(cl, key, value);
}
}

Итак, оригинальный пример теперь становится:

Map classLoaders = new WeakHashMap();
ClassLoaderLocal cll = new ClassLoaderLocal();

void addListener(Listener l) {
ClassLoader cl = l.getClass().getClassLoader();
List lls = (List) cll.get(cl);
if (lls == null) {
lls = new ArrayList();
cll.set(cl, lls);
}
lls.add(l);

classLoaders.put(cl, Boolean.TRUE);
}

В этом коде, если какой-либо слушатель поступает из освобожденного загрузчика классов, то он будет GC-d как из Core.classLoaders, так и ClassLoaderProperties.classLoaderToHolderClassName, так как оба являются WeakHashMaps, и нет никаких сильных ссылок на загрузчики классов. Сгенерированный класс ClassLoaderLocalMapHolder $$ GEN $$ X также будет GC-d вместе с загрузчиком классов, поэтому мы эффективно устранили утечку загрузчика классов без явных вызовов очистки от пользователя.

Я надеюсь, что этот код будет полезен для кого-то. Я не могу дать никаких гарантий, будет ли это работать или нет, и это явно взлом (хотя сплошной взлом). Пожалуйста, используйте его, если вы действительно понимаете, что происходит. Если вы видите ошибку в коде или у вас есть хорошее предложение, обязательно прокомментируйте. Там может быть бесплатная лицензия JavaRebel для вас 🙂 Полный исходный код:
ClassLoaderLocalMap.java ,
ClassLoaderLocal.java .

Посмотреть оригинальный пост и обсуждение на dow.ngra.de .