Статьи

Как (не) создать утечку пермгена

На днях я тестировал часть нашего приложения, касающуюся наших алгоритмов обнаружения утечек permgen . Особая особенность, с которой я боролся, требовала нового теста.

Заметив, что все наши тесты в полевых условиях были слишком сложными — включая тяжелую работу по развертыванию приложений и инициализации загрузчика классов, я бросил новый тест, похожий на следующий:

import javassist.ClassPool;

public class MicroGenerator {

	public static void main(String[] args) throws Exception {
		try {
			for (int i = 0; i < 20_000; i++) {
				generate("eu.plumbr.demo.Generated" + i);
			}
		} catch (Error e) {
			e.printStackTrace();
		}
	}

	public static Class generate(String name) throws Exception {
		ClassPool pool = ClassPool.getDefault();
		return pool.makeClass(name).toClass();
	}
}

Фрагмент кода довольно прост, он повторяет цикл и генерирует классы во время выполнения. При обнаружении ошибки приведенный выше код хорошо подготовлен, чтобы поймать его и распечатать трассировку стека.

неожиданный сюрприз

Вся сложная магия выполняется с использованием библиотеки javassist, что делает пример коротким и лаконичным.

Глядя на тест, можно ожидать, что код выполнит свой долг и умрет со знакомым сообщением « java.lang.OutOfMemoryError: Permgen space », перехваченным в блоке метода catch () метода main () .

Очевидно, ситуация немного сложнее, чем эта — через полчаса я обнаружил, что все еще смотрю на несколько иное сообщение об ошибке. Я должен признать, что получил свою справедливую долю OutOfMemoryErrors в последние годы. Так что я думал, что видел все это. Неправильно, этот все еще застал меня врасплох:

Exception in thread "main" 
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

Если он выглядит так же, как любой другой OutOfMemoryError , обратите внимание на детали — эта ошибка была обработана UncaughtExceptionHandler, а не обычными потоками управления. Этот обработчик, как следует из названия, вступит в игру, когда поток собирается завершиться из-за необработанного исключения. В этом случае виртуальная машина Java запросит у потока свой UncaughtExceptionHandler и вызовет метод обработчика uncaughtException .

Несколько сессий отладки позже я нашел причину, которая подобна тихому отказу памяти, произошедшему и описанному несколько лет назад. А именно — память используется до такой степени, что JVM не может создать новый экземпляр OutOfMemoryError () , заполнить его трассировку стека и отправить выходные данные в поток печати.

Поэтому, когда вы возьмете приведенный выше код и запустите его с одним небольшим дополнением, инициализирующим объект Error вместе с его стеком и потоком вывода, вы получите другой результат:

	static {
		new OutOfMemoryError().printStackTrace();
	}

Выполнение того же теста теперь приводит к гораздо более знакомым выводам в ваши файлы журнала:

java.lang.OutOfMemoryError: PermGen space
	at javassist.ClassPool.toClass(ClassPool.java:1099)
	at javassist.ClassPool.toClass(ClassPool.java:1042)
	at javassist.ClassPool.toClass(ClassPool.java:1000)
	at javassist.CtClass.toClass(CtClass.java:1224)
	at eu.plumbr.demo.MicroGenerator.generate(MicroGenerator.java:24)
	at eu.plumbr.demo.MicroGenerator.main(MicroGenerator.java:15)

Мораль истории? Защитные сети в JVM углубляются. Сегодня чертовски сложно перехитрить современные JVM. Но, как видно из вышесказанного, все еще есть некоторые крайние случаи, когда меры безопасности применяются таким образом, что затрудняет понимание того, что на самом деле происходит. Так что, надеюсь, этот пост спасет кого-то от несчастного сеанса отладки. Если это действительно так, рассмотрите возможность следования нашим советам по настройке производительности в Twitter .

Обратите внимание, что этот эксперимент проводился на MB Pro в середине 2013 года с Oracle Hotspot 1.7.0_51 и 1.7.0_45 на 64-битной Mac OS X 10.9.2 со стандартными размерами permgen и heap.