24просмотров
29 декабря 2025 г.
Score: 26
👻 Memory Leak в PermGen/Metaspace. Когда классы не хотят умирать. Часть 1. Казалось бы, сборщик мусора удаляет неиспользуемые объекты. Но что, если сам класс нельзя выгрузить?
Сценарий: Динамическая загрузка классов (плагины, hot deploy, Spring DevTools) + использование не того ClassLoader'а. 📣 Суть проблемы:
ClassLoader — это объект, который загружает классы. У каждого класса в метаданных JVM есть скрытая ссылка classLoader на его загрузчик.
Ключевое: Чтобы выгрузить класс (и его ClassLoader), нужно, чтобы не было живых ссылок ни на класс, ни на загрузчик. Но эта служебная ссылка classLoader в метаданных класса — это и есть ссылка на загрузчик! ⚠️ Как возникает утечка:
1. Кастомный ClassLoader загружает класс MyPlugin
2. Создаётся объект этого класса
3. Класс содержит static-поле с ссылкой на… сам ClassLoader!
4. Объекты удаляются, но ClassLoader не может быть выгружен, т.к. класс ссылается на него
public class LeakyPlugin { // ФАТАЛЬНАЯ ОШИБКА: статическое поле держит ClassLoader private static final Object[] CACHE = new Object[1000]; // Косвенная ссылка через контекст private static final ThreadLocal<MyContext> CONTEXT = new ThreadLocal<>();
}
// В метаданных класса хранится ссылка на его ClassLoader → ClassLoader живой → все его классы живые → Metaspace растёт 📈 Диагностика:
-XX:NativeMemoryTracking=summary -XX:+PrintClassHistogramBeforeFullGC ✔️Лечение:
1. Избегайте статических ссылок на объекты из загружаемых классов
2. Используйте слабые ссылки (WeakReference)
3. Для плагинов — отдельные ClassLoader'ы и правильная иерархия Пошаговый разбор сценария утечки:
🔤Шаг 1: Кастомный ClassLoader загружает класс MyPlugin
// 1. Создаём кастомный загрузчик
class PluginClassLoader extends URLClassLoader { public PluginClassLoader(URL[] urls) { super(urls, null); // Родительский загрузчик = null (изоляция) }
}
// 2. Загружаем плагин
PluginClassLoader pluginLoader = new PluginClassLoader(pluginJarUrl);
Class<?> pluginClass = pluginLoader.loadClass("com.example.MyPlugin");
Теперь:
1. pluginClass знает своего загрузчика: pluginClass.getClassLoader() == pluginLoader
2. В метаданных класса в PermGen/Metaspace лежит скрытая ссылка → pluginLoader 🔤Шаг 2: Создаём объект класса
Object pluginInstance = pluginClass.newInstance(); 🔤Шаг 3: Класс содержит static-поле с ссылкой на... сам ClassLoader!
Вот здесь самый важный момент! Посмотрите на этот класс плагина:
public class MyPlugin { // ФАТАЛЬНАЯ ОШИБКА: статическое поле (классовый уровень!) // Хранит ссылку на объект, который содержит ссылку на ClassLoader private static final byte[] CACHE = new byte[10 1024 1024]; // 10 MB // ИЛИ другой вариант: private static final ThreadLocal<Context> CONTEXT = new ThreadLocal<>(); // ИЛИ даже так: private static final MyPlugin INSTANCE = new MyPlugin(); // self-reference
} Что происходит:
1. Статическое поле CACHE принадлежит классу MyPlugin
2. Класс MyPlugin загружен через pluginLoader
3. В метаданных класса MyPlugin есть скрытая ссылка → pluginLoader
4. CACHE живёт, пока живёт класс MyPlugin
5. Класс MyPlugin живёт, пока живёт ссылка на его ClassLoader Петля замкнулась! ClassLoader не может быть выгружен, потому что на него ссылается класс, который не может быть выгружен, потому что на него ссылается статическое поле... и т.д. 🔤Шаг 4: Объекты удаляются, но ClassLoader не может быть выгружен!
// Удаляем все явные ссылки
pluginInstance = null;
pluginClass = null; // Но в Metaspace остаётся:
// 1. Класс MyPlugin (со статическим полем CACHE)
// 2. Скрытая ссылка из MyPlugin → pluginLoader
// 3. НЕТ ссылок из приложения на pluginLoader, но JVM видит ссылку из метаданных класса! // Сборщик мусора:
// - Видит, что нет GC Roots → pluginLoader
// - Проверяет, можно ли удалить pluginLoader?
// - Обнаруживает, что класс MyPlugin (который он загрузил) всё ещё "живой"
// - MyPlugin "живой", потому что его статическое поле CACHE аллоцировано
// - Замкнутый круг → УТЕЧКА ✈