Вопрос-ответ

How can I create a memory leak in Java?

Как я могу создать утечку памяти в Java?

У меня только что было собеседование, где меня попросили создать утечку памяти с помощью Java.

Излишне говорить, что я чувствовал себя довольно глупо, понятия не имея, как начать ее создавать.

Каким может быть пример?

Переведено автоматически
Ответ 1

Вот хороший способ создать настоящую утечку памяти (объекты, недоступные при выполнении кода, но все еще хранящиеся в памяти) в чистой Java:


  1. Приложение создает долго работающий поток (или использует пул потоков для еще более быстрой утечки).

  2. Поток загружает класс через (необязательно пользовательский) ClassLoader.

  3. Класс выделяет большой фрагмент памяти (например, new byte[1000000]), сохраняет строгую ссылку на него в статическом поле, а затем сохраняет ссылку на себя в ThreadLocal. Выделение дополнительной памяти необязательно (достаточно утечки экземпляра класса), но это значительно ускорит устранение утечки.

  4. Приложение очищает все ссылки на пользовательский класс или на ClassLoader, из которого он был загружен.

  5. Повторяю.

Из-за способа, которым ThreadLocal реализован в Oracle JDK, это приводит к утечке памяти:


  • У каждого Thread есть личное поле threadLocals, в котором фактически хранятся локальные значения потока.

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

  • Но каждое значение является надежной ссылкой, поэтому, когда значение (прямо или косвенно) указывает на ThreadLocal объект, который является его ключом, этот объект не будет ни собран мусором, ни удален с карты, пока существует поток.

В этом примере цепочка надежных ссылок выглядит следующим образом:

Thread объект → threadLocals карта → экземпляр примера класса → пример класса → статическое ThreadLocal поле → ThreadLocal объект.

(ClassLoader На самом деле не играет роли в создании утечки, это просто усугубляет утечку из-за этой дополнительной цепочки ссылок: пример класса → ClassLoader → все классы, которые он загрузил. Это было еще хуже во многих реализациях JVM, особенно до Java 7, потому что классы и ClassLoaders выделялись прямо в permgen и вообще никогда не собирали мусор.)

Вариация этого шаблона заключается в том, почему контейнеры приложений (такие как Tomcat) могут пропускать память, как решето, если вы часто повторно развертываете приложения, которые случайно используют ThreadLocals, которые каким-то образом указывают на самих себя. Это может произойти по ряду неочевидных причин, и часто это трудно отладить и / или исправить.

Обновление: Поскольку многие люди продолжают просить об этом, вот несколько примеров кода, которые показывают это поведение в действии.

Ответ 2

Статическое поле, содержащее ссылку на объект [особенно последнее поле]

class MemorableClass {
static final ArrayList list = new ArrayList(100);
}

(Незакрытые) открытые потоки (файловые, сетевые и т.д.)

try {
BufferedReader br = new BufferedReader(new FileReader(inputFile));
...
...
} catch (Exception e) {
e.printStackTrace();
}

Незакрытые соединения

try {
Connection conn = ConnectionFactory.getConnection();
...
...
} catch (Exception e) {
e.printStackTrace();
}

Области, недоступные сборщику мусора JVM, такие как память, выделенная с помощью собственных методов.

В веб-приложениях некоторые объекты хранятся в области приложения до тех пор, пока приложение не будет явно остановлено или удалено.

getServletContext().setAttribute("SOME_MAP", map);

Неправильные или неподходящие параметры JVM, такие как noclassgc опция в IBM JDK, которая предотвращает сборку мусора неиспользуемого класса

Смотрите Настройки IBM JDK.

Ответ 3

Простая вещь, которую нужно сделать, это использовать HashSet с неправильным (или несуществующим) hashCode() or equals(), а затем продолжать добавлять "дубликаты". Вместо того, чтобы игнорировать дубликаты, как это должно быть, набор будет только расти, и вы не сможете их удалить.

Если вы хотите, чтобы эти сбойные ключи / элементы зависли, вы можете использовать статическое поле, например

class BadKey {
// no hashCode or equals();
public final String key;
public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.
Ответ 4

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


  • File.deleteOnExit() - всегда происходит утечка строки, если строка является подстрокой, утечка еще хуже (утечка базового символа [] также происходит) - в Java 7 substring также копирует char[], поэтому более поздний вариант не применяется; @Daniel, впрочем, голоса не нужны.

Я сосредоточусь на потоках, чтобы показать опасность неуправляемых потоков, в основном, не хочу даже касаться swing.


  • Runtime.addShutdownHook и не удалять ... и тогда даже с removeShutdownHook из-за ошибки в классе ThreadGroup, касающейся незадействованных потоков, он может не быть собран, что приведет к утечке ThreadGroup. У JGroup утечка в GossipRouter.


  • Создание, но не запуск a Thread относится к той же категории, что и выше.


  • При создании потока наследуются ContextClassLoader и AccessControlContext, плюс ThreadGroup и any InheritedThreadLocal, все эти ссылки являются потенциальными утечками, наряду со всеми классами, загруженными загрузчиком классов, и всеми статическими ссылками, и ja-ja. Эффект особенно заметен для всего фреймворка j.u.c.Executor, который имеет очень простой ThreadFactory интерфейс, но большинство разработчиков понятия не имеют о скрытой опасности. Также многие библиотеки запускают потоки по запросу (слишком много популярных в отрасли библиотек).


  • ThreadLocal кэши; во многих случаях это зло. Я уверен, что все видели довольно много простых кэшей на основе ThreadLocal, что ж, плохая новость: если поток продолжает работать дольше, чем ожидалось в течение срока службы context ClassLoader, это чистая небольшая утечка. Не используйте ThreadLocal кэши, если это действительно не необходимо.


  • Вызов ThreadGroup.destroy(), когда у ThreadGroup нет самих потоков, но она по-прежнему сохраняет дочерние ThreadGroups. Серьезная утечка, которая не позволит удалить ThreadGroup из родительской группы, но все дочерние элементы станут неисчисляемыми.


  • Используя WeakHashMap, значение (in) напрямую ссылается на ключ. Это сложно найти без дампа кучи. Это применимо ко всем расширенным, Weak/SoftReference которые могут сохранять жесткую ссылку на защищаемый объект.


  • Используя java.net.URL с протоколом HTTP (S) и загружая ресурс из (!). Этот особенный, KeepAliveCache создает новый поток в системной ThreadGroup, который пропускает загрузчик классов контекста текущего потока. Поток создается по первому запросу, когда активного потока не существует, так что либо вам повезет, либо просто произойдет утечка. Утечка уже исправлена в Java 7, и код, создающий поток, должным образом удаляет context classloader. Есть еще несколько случаев (как в ImageFetcher, также исправлено) создания подобных потоков.


  • Используя InflaterInputStream передачу new java.util.zip.Inflater() в конструкторе (PNGImageDecoder например), а не вызывая end() inflater. Ну, если вы передаете конструктор с помощью just new, никаких шансов... И да, вызов close() в потоке не закрывает inflater, если он передан вручную как параметр конструктора. Это не настоящая утечка, поскольку она будет выпущена завершителем... когда он сочтет это необходимым. До этого момента он настолько сильно съедал собственную память, что мог заставить Linux oom_killer безнаказанно убивать процесс. Основная проблема заключается в том, что финализация в Java очень ненадежна, и G1 усугубила ее до версии 7.0.2. Мораль истории: освободите собственные ресурсы как можно скорее; финализатор просто слишком плохой.


  • Тот же случай с java.util.zip.Deflater. Это намного хуже, поскольку Deflater в Java испытывает нехватку памяти, т. Е. Всегда использует 15 бит (максимум) и 8 уровней памяти (максимум 9), выделяя несколько сотен КБ встроенной памяти. К счастью, Deflater широко не используется, и, насколько мне известно, JDK не содержит злоупотреблений. Всегда вызывайте end(), если вы вручную создаете Deflater или Inflater. Лучшая часть последних двух: вы не можете найти их с помощью обычных доступных инструментов профилирования.


(Я могу добавить еще несколько случаев потери времени, с которыми я столкнулся, по запросу.)

Удачи и будьте в безопасности; утечки - это зло!

java