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

3225

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

Каким будет пример?

Mat B.
источник
275
Я бы сказал им , что Java использует сборщик мусора, и попросить их быть немного более конкретно об их определении «утечки памяти», объясняя , что - за исключением ошибок JVM - Java не может произойти утечка памяти в совершенно таким же образом C / C ++ может. Вы должны иметь ссылку на объект где-то .
Дариен
371
Я нахожу забавным, что в большинстве ответов люди ищут эти крайние случаи и уловки и, кажется, полностью упускают суть (ИМО). Они могут просто показать код, который хранит бесполезные ссылки на объекты, которые никогда не будут использоваться снова, и в то же время никогда не удалять эти ссылки; Можно сказать, что эти случаи не являются «истинными» утечками памяти, потому что все еще существуют ссылки на эти объекты, но если программа никогда не использует эти ссылки снова и никогда не удаляет их, это полностью эквивалентно (и так плохо, как) «» Истинная утечка памяти ".
ehabkost
62
Честно говоря, я не могу поверить, что аналогичный вопрос, который я задавал о "Go", был понижен до -1. Здесь: stackoverflow.com/questions/4400311/… По сути, утечки памяти, о которых я говорил, - это те, кто получил +200 голосов к OP, и все же меня атаковали и оскорбили за вопрос, не возникла ли такая же проблема у «Go». Почему-то я не уверен, что все вики работают так здорово.
СинтаксисT3rr0r
74
@ SyntaxT3rr0r - Дариен не фанатизм. он явно признал, что некоторые JVM могут иметь ошибки, которые означают утечку памяти. это отличается от самой спецификации языка, допускающей утечки памяти.
Питер Рекор
31
@ehabkost: Нет, они не эквивалентны. (1) Вы обладаете способностью восстанавливать память, в то время как при «истинной утечке» ваша программа на C / C ++ забывает выделенный диапазон, безопасного восстановления не существует. (2) Вы можете очень легко обнаружить проблему с профилированием, потому что вы можете видеть, какие объекты включает «раздувание». (3) «Истинная утечка» - это однозначная ошибка, в то время как программа, которая хранит множество объектов до тех пор, пока не будет завершена, может быть преднамеренной частью того, как она должна работать.
Дариен

Ответы:

2309

Вот хороший способ создать настоящую утечку памяти (объекты недоступны при выполнении кода, но все еще хранятся в памяти) в чистой 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, которые каким-то образом указывают на себя. Это может произойти по ряду скрытых причин, и его часто трудно отладить и / или исправить.

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

Daniel Pryden
источник
186
+1 Утечки ClassLoader являются одними из наиболее болезненных утечек памяти в мире JEE, часто вызываемых сторонними библиотеками, которые преобразуют данные (BeanUtils, кодеки XML / JSON). Это может произойти, когда библиотека загружена вне корневого загрузчика классов вашего приложения, но содержит ссылки на ваши классы (например, путем кэширования). Когда вы удаляете / повторно развертываете свое приложение, JVM не может собрать мусор как загрузчик классов приложения (и, следовательно, все классы, загруженные им), поэтому при повторном развертывании сервер приложений в конечном итоге не работает. Если вам повезет, вы получите подсказку с ClassCastException, zxyAbc не может быть приведен к zxyAbc
earcam
7
tomcat использует трюки и обнуляет ВСЕ статические переменные во ВСЕХ загруженных классах, хотя tomcat имеет много данных и плохое кодирование (нужно потратить некоторое время и отправить исправления), а также потрясающий ConcurrentLinkedQueue в качестве кеша для внутренних (маленьких) объектов, настолько маленький, что даже ConcurrentLinkedQueue.Node занимает больше памяти.
bestsss
57
+1: Утечки в Classloader - это кошмар. Я потратил недели, пытаясь понять их. Печально то, что, как сказал @earcam, они в основном вызваны сторонними библиотеками, а также большинство профилировщиков не могут обнаружить эти утечки. В этом блоге есть хорошее и четкое объяснение утечек Classloader. blogs.oracle.com/fkieviet/entry/…
Адриан М
4
@Nicolas: Ты уверен? JRockit делает объекты GC Class по умолчанию, а HotSpot - нет, но AFAIK JRockit по-прежнему не может GC Class или ClassLoader, на который ссылается ThreadLocal.
Даниэль Приден
6
Tomcat попытается обнаружить эти утечки и предупредить их: wiki.apache.org/tomcat/MemoryLeakProtection . Самая последняя версия иногда даже исправит утечку за вас.
Маттис Биерман,
1212

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

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

Призыв String.intern()на длинную строку

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

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

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 .

Prashant Bhate
источник
178
Я не согласен с тем, что контекст и атрибуты сеанса являются «утечками». Это просто долгоживущие переменные. А статическое конечное поле более или менее просто константа. Возможно, следует избегать больших констант, но я не думаю, что было бы справедливо называть это утечкой памяти.
Ян Маклаирд
80
(Незакрытые) открытые потоки (файл, сеть и т. Д.) , Не просачиваются по-настоящему, во время финализации (что будет после следующего цикла GC) close () будет запланировано ( close()обычно не вызывается в финализаторе поток, так как может быть операция блокировки). Это плохая практика - не закрывать, но это не приводит к утечке. Незакрытый java.sql. Соединение такое же.
bestsss
33
В большинстве здравомыслящих JVM создается впечатление, что класс String имеет слабую ссылку на свое internхеш-содержимое. Таким образом , он будет мусор должным образом и не течь. (но IANAJP) mindprod.com/jgloss/interned.html#GC
Мэтт Б.
42
Как статическое поле, содержащее ссылку на объект [esp final field], является утечкой памяти ??
Канагавелу Сугамар
5
@cHao Правда. Опасность, с которой я столкнулся, заключается не в проблемах, связанных с утечкой памяти Streams. Проблема в том, что им не хватает утечки памяти. Вы можете вытекать много ручек, но все еще есть много памяти. Сборщик мусора мог бы тогда решить не создавать полную коллекцию, потому что у нее все еще есть много памяти. Это означает, что финализатор не вызывается, поэтому у вас закончились дескрипторы. Проблема заключается в том, что финализаторы (как правило) будут запускаться до того, как у вас закончатся потоки памяти, но он может не вызываться до того, как у вас закончится что-то кроме памяти.
Патрик М
460

Простая вещь, которую нужно сделать, это использовать HashSet с неправильным (или не существующим) hashCode()или 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.
Peter Lawrey
источник
68
На самом деле, вы можете удалить элементы из HashSet, даже если класс элемента получает hashCode и равен неправильному; просто получите итератор для набора и используйте его метод remove, поскольку итератор фактически работает с самими базовыми записями, а не с элементами. (Обратите внимание , что невыполненный хэш - код / равно это не достаточно , чтобы вызвать утечку, по умолчанию реализовать простой идентификатор объекта , и поэтому вы можете получить элементы и удалить их , как правило.)
Донал Fellows
12
@ То, что я пытаюсь сказать, думаю, я не согласен с вашим определением утечки памяти. Я бы посчитал (для продолжения аналогии), что ваша техника удаления итераторов будет капельницей при утечке; утечка все еще существует независимо от поддона.
CorsiKa
94
Я согласен, это не «утечка» памяти, потому что вы можете просто удалить ссылки на хэш-набор и подождать, пока GC включится, и сделать это! память возвращается.
user541686
12
@ SyntaxT3rr0r, я истолковал твой вопрос как вопрос, есть ли в языке что-нибудь, что естественно приводит к утечкам памяти. Ответ - нет. Этот вопрос спрашивает, возможно ли придумать ситуацию, чтобы создать что-то вроде утечки памяти. Ни один из этих примеров не является утечкой памяти так, как это понимает программист C / C ++.
Питер Лоури
11
@Peter Lawrey: также, что вы думаете по этому поводу: «В языке C нет ничего, что естественным образом просачивается в утечку памяти, если вы не забудете вручную освободить выделенную память» . Как это было бы для интеллектуальной нечестности? Во всяком случае, я устал: вы можете сказать последнее слово.
СинтаксисT3rr0r
271

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

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

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

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

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

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

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

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

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

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

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

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

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

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

bestsss
источник
23
Creating but not starting a Thread...Да, я был сильно укушен этим несколько веков назад! (Java 1.3)
leonbloy
@leonbloy, до того как стало еще хуже, поскольку поток был добавлен прямо в группу потоков, не запуск означал очень серьезную утечку. Не только увеличивает unstartedсчет, но и предотвращает уничтожение группы потоков (меньшее зло, но все же утечка)
bestsss
Спасибо! «Вызов, ThreadGroup.destroy()когда у ThreadGroup нет потоков сам по себе ...» - невероятно тонкая ошибка; Я преследовал это в течение нескольких часов, сбиваясь с пути, потому что перечисление потока в моем управляющем GUI ничего не показывало, но группа потоков и, по-видимому, по крайней мере одна дочерняя группа не исчезли бы.
Лоуренс Дол
1
@bestsss: Мне любопытно, почему вы хотите удалить хук выключения, учитывая, что он работает при завершении работы JVM?
Лоуренс Дол
203

Большинство примеров здесь "слишком сложны". Это крайние случаи. В этих примерах программист допустил ошибку (например, не переопределяет equals / hashcode) или был укушен угловым случаем JVM / JAVA (загрузка класса со статическим ...). Я думаю, что это не тот пример, который хочет интервьюер, или даже самый распространенный случай.

Но есть действительно более простые случаи утечки памяти. Сборщик мусора освобождает только то, на что больше нет ссылок. Мы, как разработчики Java, не заботимся о памяти. Мы распределяем его по мере необходимости и позволяем автоматически его освобождать. Хорошо.

Но любое долгоживущее приложение, как правило, имеет общее состояние. Это может быть что угодно, статика, синглтоны ... Часто нетривиальные приложения имеют тенденцию составлять графы сложных объектов. Достаточно просто забыть установить ссылку на ноль или чаще забыть удалить один объект из коллекции, чтобы вызвать утечку памяти.

Конечно, всевозможные слушатели (например, слушатели пользовательского интерфейса), кеши или любое долгоживущее общее состояние имеют тенденцию вызывать утечку памяти, если не обрабатываются должным образом. Следует понимать, что это не случай Java или проблема с сборщиком мусора. Это проблема дизайна. Мы разрабатываем, что мы добавляем слушателя к долгоживущему объекту, но мы не удаляем слушателя, когда он больше не нужен. Мы кешируем объекты, но у нас нет стратегии их удаления из кеша.

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

Как будто мы должны закрыть соединения или файлы SQL. Нам нужно установить правильные ссылки на null и удалить элементы из коллекции. У нас должны быть правильные стратегии кэширования (максимальный объем памяти, количество элементов или таймеры). Все объекты, позволяющие уведомлять слушателя, должны предоставлять метод addListener и removeListener. И когда эти уведомители больше не используются, они должны очистить свой список слушателей.

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

Николас Буске
источник
24
Я нахожу забавным, что в других ответах люди ищут эти крайние случаи и уловки и, кажется, полностью упускают суть. Они могут просто показать код, который хранит бесполезные ссылки на объекты, которые никогда не будут использоваться снова, и никогда не удалять эти ссылки; Можно сказать, что эти случаи не являются «истинными» утечками памяти, потому что все еще существуют ссылки на эти объекты, но если программа никогда не использует эти ссылки снова и никогда не удаляет их, это полностью эквивалентно (и так плохо, как) «» Истинная утечка памяти ".
ehabkost
@ Николас Буске: «Утечка памяти действительно возможна» Большое спасибо. +15 голосов. Ницца. Я кричал здесь за то, что констатировал этот факт в качестве вопроса о языке Go: stackoverflow.com/questions/4400311 У этого вопроса все еще есть отрицательные отрицательные голоса :(
SyntaxT3rr0r
GC в Java и .NET в некотором смысле основан на предположении, что граф объектов, содержащих ссылки на другие объекты, такой же, как граф объектов, «заботящихся» о других объектах. В действительности, возможно, что в ссылочном графе могут существовать ребра, которые не представляют «заботливых» отношений, и для объекта возможно заботиться о существовании другого объекта, даже если прямой или косвенной ссылочной траектории (даже использующей WeakReference) не существует от одного к другому. Если бы у ссылки на объект был запасной бит, было бы полезно иметь индикатор «заботится о цели» ...
суперкат
... и система должна отправлять уведомления (с помощью средств, аналогичных PhantomReference), если был найден объект, о котором никто не заботился. WeakReferenceподходит немного ближе, но должен быть преобразован в сильную ссылку, прежде чем его можно будет использовать; если цикл GC происходит, пока существует сильная ссылка, цель будет считаться полезной.
суперкат
Это на мой взгляд правильный ответ. Мы написали симуляцию много лет назад. Каким-то образом мы случайно связали предыдущее состояние с текущим состоянием, что привело к утечке памяти. Из-за крайнего срока мы никогда не устраняли утечку памяти, а делали ее «функцией», документируя ее.
17
163

Ответ полностью зависит от того, что, по мнению интервьюера, они спрашивают.

Возможно ли на практике сделать утечку Java? Конечно, и в других ответах есть множество примеров.

Но есть несколько мета-вопросов, которые, возможно, задавались?

  • Является ли теоретически «совершенная» реализация Java уязвимой для утечек?
  • Понимает ли кандидат разницу между теорией и реальностью?
  • Понимает ли кандидат, как работает сборка мусора?
  • Или как сборка мусора должна работать в идеальном случае?
  • Они знают, что могут вызывать другие языки через нативный интерфейс?
  • Они знают об утечке памяти на этих других языках?
  • Знает ли кандидат даже, что такое управление памятью и что происходит за кулисами в Java?

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

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

PlayTank
источник
22
Всякий раз, когда я задавал этот вопрос, я искал довольно простой ответ - продолжайте наращивать очередь, не закрывайте, наконец, дб и т. Д., А не нечетные подробности загрузчика классов / потоков, подразумевает, что они понимают, что gc может и не может сделать для вас. Зависит от работы, с которой вы берете интервью.
DaveC
Пожалуйста, взгляните на мой вопрос, спасибо stackoverflow.com/questions/31108772/…
Даниэль Ньютаун,
130

Ниже приведен довольно бессмысленный пример, если вы не понимаете JDBC . Или, по крайней мере, то, как JDBC ожидает, что разработчик закроет Connection, Statementи ResultSetэкземпляры перед тем, как отбросить их или потерять ссылки на них, вместо того чтобы полагаться на реализацию finalize.

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

Проблема с вышеупомянутым состоит в том, что Connectionобъект не закрыт, и, следовательно, физическое соединение будет оставаться открытым, пока сборщик мусора не придет и не обнаружит, что он недоступен. GC вызовет finalizeметод, но есть драйверы JDBC, которые не реализуют finalize, по крайней мере, не так, как Connection.closeэто реализовано. В результате получается, что, хотя память будет возвращена из-за того, что объекты недоступны, ресурсы (включая память), связанные с Connectionобъектом, могут быть просто не восстановлены.

В таком случае , где Connection«S finalizeметод не очищает все, можно было бы на самом деле считаем , что физическое соединение с сервером базы данных будет продолжаться несколько циклов сборки мусора, пока сервер базы данных в конце концов выясняет , что соединение не является живым (если он делает), и должен быть закрыт.

Даже если драйвер JDBC должен быть реализован finalize, исключения могут быть выброшены во время финализации. В результате получается, что любая память, связанная с теперь «неактивным» объектом, не будет возвращена, поскольку finalizeгарантированно будет вызван только один раз.

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

Есть еще много примеров, которые вы можете придумать - например,

  • Управление Listэкземпляром, в котором вы только добавляете в список, а не удаляете из него (хотя вы должны избавляться от элементов, которые вам больше не нужны), или
  • Открытие Sockets или Files, но не закрытие их, когда они больше не нужны (аналогично приведенному выше примеру с Connectionклассом).
  • Не выгружать Singletons при закрытии приложения Java EE. По-видимому, загрузчик классов, который загрузил класс singleton, сохранит ссылку на класс, и, следовательно, экземпляр singleton никогда не будет собран. При развертывании нового экземпляра приложения обычно создается новый загрузчик классов, и прежний загрузчик классов будет продолжать существовать из-за синглтона.
Vineet Reynolds
источник
98
Вы достигнете максимального предела открытого соединения прежде, чем достигнете пределов памяти обычно. Не спрашивайте меня, почему я знаю ...
Hardwareguy
Драйвер JDBC Oracle известен тем, что делает это.
чотки
@ Hardwareguy Я много раз сталкивался с ограничениями соединений баз данных SQL, пока не включил Connection.closeв блок finally все мои вызовы SQL. Для дополнительного удовольствия я вызвал некоторые долго выполняемые хранимые процедуры Oracle, которые требовали блокировок на стороне Java для предотвращения слишком большого количества обращений к базе данных.
Михаил Шопсин
@Hardwareguy Это интересно, но на самом деле не обязательно, чтобы действительные пределы подключения были нарушены для всех сред. Например, для приложения, развернутого на сервере приложений WebLogic 11g, я видел утечки соединения в большом масштабе. Но из-за возможности сбора утечек соединений соединения с базой данных оставались доступными, пока возникали утечки памяти. Я не уверен во всех средах.
Aseem Bansal
По моему опыту, вы получаете утечку памяти, даже если вы закрываете соединение. Сначала необходимо закрыть ResultSet и PreparedStatement. Был сервер, который несколько раз падал после нескольких часов или даже дней нормальной работы из-за OutOfMemoryErrors, пока я не начал это делать.
Бьорн Стенфельдт
119

Вероятно, одним из простейших примеров потенциальной утечки памяти и того, как ее избежать, является реализация ArrayList.remove (int):

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

Если бы вы реализовывали это самостоятельно, вы бы подумали очистить элемент массива, который больше не используется ( elementData[--size] = null)? Эта ссылка может поддержать огромный объект ...

меритон
источник
5
И где здесь утечка памяти?
Rds
28
@maniek: Я не имел в виду, что этот код имеет утечку памяти. Я процитировал это, чтобы показать, что иногда неочевидный код необходим, чтобы избежать случайного удержания объекта.
меритон
Что такое RangeCheck (индекс); ?
Корай Тугай
6
Джошуа Блох привел этот пример в Effective Java, демонстрируя простую реализацию стеков. Очень хороший ответ.
арендует
Но это не было бы НАСТОЯЩЕЙ утечкой памяти, даже если забыли. Элемент по-прежнему БЕЗОПАСНО доступен с помощью Reflection, он просто не был бы очевиден и напрямую доступен через интерфейс List, но объект и ссылка все еще там, и к ним можно безопасно обращаться.
Д.Гойко
68

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

Билл Ящерица
источник
14
Я не верю, что это «утечка». Это ошибка, и это по дизайну программы и языка. Утечка - это объект, который висит без каких-либо ссылок на него.
user541686
29
@ Mehrdad: Это только одно узкое определение, которое не в полной мере относится ко всем языкам. Я бы сказал, что любая утечка памяти - это ошибка, вызванная плохим дизайном программы.
Билл Ящерица
9
@ Mehrdad: ...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. я не понимаю, как вы делаете такой вывод. Существует меньше способов создать утечку памяти в Java по любому определению. Это определенно все еще актуальный вопрос.
Билл Ящерица
7
@ 31eee384: Если ваша программа хранит объекты в памяти, которые она никогда не сможет использовать, то технически это утечка памяти. Тот факт, что у вас есть большие проблемы, на самом деле не меняет этого.
Билл Ящерица
8
@ 31eee384: Если вы точно знаете, что этого не будет, то этого не может быть. Программа, как написано, никогда не получит доступ к данным.
Билл Ящерица
51

Вы можете сделать утечку памяти с классом sun.misc.Unsafe . Фактически этот класс обслуживания используется в различных стандартных классах (например, в классах java.nio ). Вы не можете создать экземпляр этого класса напрямую , но вы можете использовать отражение для этого .

Код не компилируется в Eclipse IDE - скомпилируйте его с помощью команды javac(во время компиляции вы получите предупреждения)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}
stemm
источник
1
Выделенная память невидима для сборщика мусора
stemm
4
Выделенная память также не принадлежит Java.
bestsss
Это солнце / оракул JVM является конкретным? Например, это будет работать на IBM?
Берлин Браун
2
Память, безусловно, «принадлежит Java», по крайней мере в том смысле, что i) она не доступна никому другому, ii) при выходе из приложения Java она будет возвращена в систему. Это только за пределами JVM.
Майкл Андерсон
3
Это создаст затмение (по крайней мере, в последних версиях), но вам нужно будет изменить настройки компилятора: в «Окне»> «Установки»> «Java»> «Компилятор»> «Ошибки / предупреждение»> «Устаревший и ограниченный API» установите «Запрещенную ссылку (правила доступа)» на «Предупреждение». ».
Майкл Андерсон
43

Я могу скопировать свой ответ отсюда: Самый простой способ вызвать утечку памяти в Java?

«Утечка памяти в информатике (или утечка в этом контексте) происходит, когда компьютерная программа использует память, но не может вернуть ее обратно в операционную систему». (Википедия)

Простой ответ: вы не можете. Java выполняет автоматическое управление памятью и освобождает ресурсы, которые вам не нужны. Вы не можете остановить это. Он ВСЕГДА сможет освободить ресурсы. В программах с ручным управлением памятью это отличается. Вы не можете получить немного памяти в C, используя malloc (). Чтобы освободить память, вам нужен указатель, который возвратил malloc, и вызовите free () для него. Но если у вас больше нет указателя (перезаписано или превышено время жизни), то, к сожалению, вы не сможете освободить эту память и, следовательно, у вас будет утечка памяти.

Все остальные ответы, по моему мнению, на самом деле не утечки памяти. Все они стремятся заполнить память бессмысленными вещами очень быстро. Но в любой момент вы все равно можете разыменовать созданные вами объекты и, таким образом, освободить память -> НЕТ УТЕЧКИ. Ответ acconrad довольно близок, хотя, как я должен признать, его решение состоит в том, чтобы просто «сбить» сборщик мусора, запустив его в бесконечный цикл).

Длинный ответ: вы можете получить утечку памяти, написав библиотеку для Java с использованием JNI, которая может иметь ручное управление памятью и, таким образом, иметь утечки памяти. Если вы вызовете эту библиотеку, ваш процесс Java будет утечка памяти. Или вы можете иметь ошибки в JVM, так что JVM теряет память. Вероятно, есть ошибки в JVM, могут даже быть некоторые известные, так как сборка мусора не так уж тривиальна, но все же это ошибка. По замыслу это невозможно. Вы можете запросить некоторый код Java, который вызван такой ошибкой. Извините, я не знаю ни одного, и в любом случае это может быть ошибкой в ​​следующей версии Java.

янки
источник
12
Это чрезвычайно ограниченное (и не очень полезное) определение утечек памяти. Единственное определение, имеющее смысл для практических целей, - «утечка памяти - это любое состояние, при котором программа продолжает удерживать память, выделенную после того, как данные, в которых она хранится, больше не нужны».
Мейсон Уилер
1
Упомянутый ответ acconrad был удален?
Томаш Зато - Восстановить Монику
1
@ TomášZato: Нет, это не так. Я превратил ссылку выше, чтобы ссылку сейчас, чтобы вы могли легко найти ее.
Янки
Что такое воскресение объекта? Сколько раз вызывается деструктор? Как эти вопросы опровергают этот ответ?
аутист
1
Ну, конечно, вы можете создать утечку памяти внутри Java, несмотря на GC, и все же выполнить приведенное выше определение. Просто имейте структуру данных только для добавления, защищенную от внешнего доступа, чтобы никакой другой код не удалял ее - программа не может освободить память, потому что у нее нет кода.
toolforger
39

Вот простой / зловещий через http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 .

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

Поскольку подстрока относится к внутреннему представлению оригинала, гораздо более длинной строки, оригинал остается в памяти. Таким образом, до тех пор, пока в игре есть StringLeaker, в памяти также хранится вся исходная строка, даже если вы думаете, что просто держитесь за односимвольную строку.

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

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

Для дополнительного вреда вы можете также .intern()подстроку:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

Это сохранит в памяти как оригинальную длинную строку, так и производную подстроку даже после удаления экземпляра StringLeaker.

Джон Чемберс
источник
4
Я бы не назвал это утечкой памяти, как таковой . Когда muchSmallerStringосвобождается (потому что StringLeakerобъект уничтожен), длинная строка также будет освобождена. То, что я называю утечкой памяти, - это память, которая никогда не может быть освобождена в этом экземпляре JVM. Тем не менее, вы показали себе, как освободить память this.muchSmallerString=new String(this.muchSmallerString). С настоящей утечкой памяти ничего не поделаешь.
Rds
2
@rds, это справедливо. Этот internслучай может быть скорее «сюрпризом памяти», чем «утечкой памяти». .intern()Однако использование подстроки, безусловно, создает ситуацию, когда ссылка на более длинную строку сохраняется и не может быть освобождена.
Джон Чемберс
15
Метод substring () создает новую строку в java7 (это новое поведение)
anstarovoyt
Вам даже не нужно выполнять подстроку () самостоятельно: используйте сопоставление регулярных выражений, чтобы сопоставить крошечную часть огромного ввода, и долгое время носите с собой «извлеченную» строку. Огромный вклад остается живым до Java 6.
Bananeweizen
37

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

паули
источник
1
Платформа Android дает пример утечки памяти, созданной путем кэширования растрового изображения в статическом поле представления .
Rds
36

Возьмите любое веб-приложение, работающее в любом контейнере сервлетов (Tomcat, Jetty, Glassfish, что угодно ...). Повторно развертывайте приложение 10 или 20 раз подряд (этого может быть достаточно, чтобы просто коснуться WAR в каталоге автоматического развертывания сервера.

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

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

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

Потоки, запущенные вашим приложением, переменные ThreadLocal, добавление журналов - это некоторые из тех, которые обычно вызывают утечку из загрузчика классов.

Харальд Веллманн
источник
1
Это не из-за утечки памяти, а потому, что загрузчик классов не выгружает предыдущий набор классов. Поэтому не рекомендуется повторно развертывать сервер приложений без перезапуска сервера (не физического компьютера, а сервера приложений). Я видел ту же проблему с WebSphere.
Свен
35

Может быть, используя внешний нативный код через JNI?

С чистой Java это практически невозможно.

Но это касается «стандартного» типа утечки памяти, когда вы больше не можете получить доступ к памяти, но она все еще принадлежит приложению. Вместо этого вы можете хранить ссылки на неиспользуемые объекты или открывать потоки, не закрывая их впоследствии.

Рогач
источник
22
Это зависит от определения «утечки памяти». Если «память хранится, но больше не нужна», то это легко сделать в Java. Если это «память, которая выделена, но вообще не доступна для кода», то это становится немного сложнее.
Иоахим Зауэр
@ Йоахим Зауэр - я имел в виду второй тип. Первое довольно легко сделать :)
Рогач
6
«С чистой Java это почти невозможно». Что ж, мой опыт - другой, особенно когда дело доходит до реализации кэшей людьми, которые не знают о подводных камнях здесь.
Фабиан Барни
4
@Rogach: в основном есть +400 откликов на разные ответы людей с +10 000 повторений, показывающих, что в обоих случаях Йоахим Сауэр прокомментировал, что это очень возможно. Так что твое «почти невозможно» не имеет смысла.
СинтаксисT3rr0r
32

Однажды у меня произошла приятная «утечка памяти» в отношении PermGen и разбора XML. Синтаксический анализатор XML, который мы использовали (я не могу вспомнить, какой именно), сделал String.intern () для имен тегов, чтобы сделать сравнение быстрее. У одного из наших клиентов была отличная идея хранить значения данных не в XML-атрибутах или тексте, а в виде тэгов, поэтому у нас был такой документ:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

На самом деле они использовали не цифры, а более длинные текстовые идентификаторы (около 20 символов), которые были уникальными и приходили со скоростью 10–15 миллионов в день. Это составляет 200 МБ мусора в день, который больше никогда не понадобится, и никогда не будет GCed (поскольку он есть в PermGen). Для permgen было установлено значение 512 МБ, поэтому потребовалось около двух дней, чтобы возникла исключительная ситуация нехватки памяти (OOME) ...

Рон
источник
4
Просто чтобы придираться к вашему примеру кода: я думаю, что числа (или строки, начинающиеся с цифр) не допускаются в качестве имен элементов в XML.
Paŭlo Ebermann
Обратите внимание, что это больше не относится к JDK 7+, где интернирование строк происходит в куче. См. Эту статью для подробного рецензирования
jmiserez
Итак, я думаю, что использование StringBuffer вместо String решит эту проблему? не так ли?
anubhs
24

Что такое утечка памяти:

  • Это вызвано ошибкой или плохим дизайном.
  • Это пустая трата памяти.
  • Со временем становится хуже.
  • Сборщик мусора не может его почистить.

Типичный пример:

Кеш объектов - хорошая отправная точка, чтобы все испортить.

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

Ваш кеш растет и растет. И довольно скоро вся база данных засасывается в память. В лучшем дизайне используется LRUMap (только хранит недавно использованные объекты в кеше).

Конечно, вы можете сделать все намного сложнее:

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

Что часто бывает:

Если этот Info-объект имеет ссылки на другие объекты, которые снова имеют ссылки на другие объекты. В некотором смысле вы также можете считать это некоторой утечкой памяти (вызванной плохим дизайном).

оборота бвдб
источник
22

Я думал, что это было интересно, что никто не использовал примеры внутреннего класса. Если у вас есть внутренний класс; он по своей сути поддерживает ссылку на содержащий класс. Конечно, технически это не утечка памяти, потому что Java в конечном итоге ее очистит; но это может заставить классы зависать дольше, чем ожидалось.

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

Теперь, если вы вызовете Example1 и получите Example2, отбрасывающий Example1, у вас по-прежнему будет ссылка на объект Example1.

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

Я также слышал слух, что если у вас есть переменная, которая существует дольше определенного времени; Java предполагает, что он всегда будет существовать, и на самом деле никогда не будет пытаться очистить его, если он больше не будет доступен в коде. Но это совершенно не проверено.

Suroot
источник
2
внутренние классы редко являются проблемой. Это простой случай, который очень легко обнаружить. Слух тоже только слух.
bestsss
2
«Слух» звучит как кто-то наполовину прочитавший о том, как работает GC поколений. Долгоживущие, но теперь недостижимые объекты могут действительно оставаться на месте и некоторое время занимать пространство, потому что JVM продвигала их из подрастающего поколения, чтобы она могла перестать проверять их при каждом проходе. По замыслу, они уклонятся от аккуратных пропусков «очистить мои 5000 временных строк». Но они не бессмертны. Они по-прежнему имеют право на сбор, и, если виртуальная машина привязана к ОЗУ, она в конечном итоге запустит полную очистку GC и вернет эту память.
Чао
22

Недавно я столкнулся с ситуацией утечки памяти, вызванной каким-то образом log4j.

Log4j имеет этот механизм, называемый Nested Diagnostic Context (NDC), который представляет собой инструмент для различения чередующихся выходных данных журнала из разных источников. Гранулярность, на которой работает NDC - это потоки, поэтому он различает выходные данные журнала из разных потоков по отдельности.

Чтобы хранить специфичные для потока теги, класс NDC в log4j использует Hashtable, который определяется самим объектом Thread (в отличие от, скажем, идентификатора потока), и, таким образом, до тех пор, пока тег NDC не останется в памяти, все объекты, которые свисают с потока Объект также остается в памяти. В нашем веб-приложении мы используем NDC для пометки выходов из системы с помощью идентификатора запроса, чтобы отдельно отличать журналы от одного запроса. Контейнер, который связывает тег NDC с потоком, также удаляет его при возврате ответа из запроса. Проблема возникла, когда во время обработки запроса порождался дочерний поток, что-то вроде следующего кода:

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

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

оборота Пунит
источник
Это отстой. Вы должны проверить эту библиотеку журналов, которая выделяет нулевую память при регистрации в файле: mentalog.soliveirajr.com
TraderJoeChicago
+1 Вы знаете, есть ли аналогичная проблема с MDC в slf4j / logback (продукты-наследники того же автора)? Я собираюсь сделать глубокое погружение в источник, но сначала хотел проверить. В любом случае, спасибо за публикацию этого.
sparc_spread
20

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

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

Затем вы можете объяснить, что при подсчете ссылок вышеприведенный код приведет к утечке памяти. Но большинство современных JVM больше не используют подсчет ссылок, большинство используют сборщик мусора, который фактически собирает эту память.

Далее вы можете объяснить создание объекта, который имеет базовый собственный ресурс, например:

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

Затем вы можете объяснить, что это технически утечка памяти, но на самом деле она вызвана нативным кодом в JVM, выделяющим базовые нативные ресурсы, которые не были освобождены вашим Java-кодом.

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

deltamind106
источник
19

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

  1. Объявите нативный метод.
  2. В нативном методе звоните malloc. Не звони free.
  3. Вызовите родной метод.

Помните, что выделение памяти в нативном коде происходит из кучи JVM.

Пол Мори
источник
1
Основано на реальных событиях.
Рег
18

Создайте статическую карту и продолжайте добавлять жесткие ссылки на нее. Это никогда не будет GC'd.

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}
оборота Даффимо
источник
87
Как это утечка? Он делает именно то, что вы просите. Если это утечка, создание и хранение объектов в любом месте - это утечка.
Фалмарри
3
Я согласен с @Falmarri. Я не вижу утечки, вы просто создаете объекты. Вы, безусловно, могли бы «освободить» память, которую вы только что выделили, с помощью другого метода, называемого «removeFromCache». Утечка - это когда вы не можете восстановить память.
Кайл
3
Я хочу сказать, что тот, кто продолжает создавать объекты, возможно, помещает их в кэш, может получить ошибку OOM, если он не будет осторожен.
Duffymo
8
@duffymo: Но на самом деле вопрос не в этом. Это не имеет ничего общего с простым использованием всей вашей памяти.
Фалмарри
3
Абсолютно недействительный. Вы просто собираете кучу объектов в коллекцию карт. Их ссылки будут сохранены, потому что карта содержит их.
gyorgyabraham
16

Вы можете создать движущуюся утечку памяти, создав новый экземпляр класса в методе finalize этого класса. Бонусные баллы, если финализатор создает несколько экземпляров. Вот простая программа, которая пропускает всю кучу за несколько секунд или нескольких минут в зависимости от размера вашей кучи:

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}
sethobrien
источник
15

Я не думаю, что кто-то сказал это еще: вы можете воскресить объект, переопределив метод finalize (), так что finalize () хранит ссылку на это где-то. Сборщик мусора будет вызываться только один раз для объекта, поэтому после этого объект никогда не будет уничтожен.

Бен
источник
10
Это неправда. finalize()не будет вызван, но объект будет собран, когда не будет больше ссылок. Сборщик мусора тоже не называется.
bestsss
1
Этот ответ вводит в заблуждение, finalize()метод может быть вызван JVM только один раз, но это не означает, что его нельзя собрать повторно, если объект воскресен, а затем снова разыменован. Если в finalize()методе есть код закрытия ресурса, этот код больше не будет запускаться, это может привести к утечке памяти.
Том Камманн
15

Недавно я столкнулся с более тонкой утечкой ресурсов. Мы открываем ресурсы через getResourceAsStream загрузчика классов, и получилось так, что дескрипторы входного потока не были закрыты.

Хм, вы могли бы сказать, что за идиот.

Что делает это интересным, так это то, что вы можете вытекать кучу памяти из основного процесса, а не из кучи JVM.

Все, что вам нужно, это файл jar с файлом внутри, на который будет ссылаться код Java. Чем больше файл jar, тем быстрее выделяется память.

Вы можете легко создать такую ​​банку с помощью следующего класса:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

Просто вставьте в файл с именем BigJarCreator.java, скомпилируйте и запустите его из командной строки:

javac BigJarCreator.java
java -cp . BigJarCreator

Et voilà: в вашем текущем рабочем каталоге вы найдете архив jar с двумя файлами внутри.

Давайте создадим второй класс:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

Этот класс в основном ничего не делает, но создает объекты InputStream без ссылок. Эти объекты будут немедленно собраны мусором и, следовательно, не влияют на размер кучи. Для нашего примера важно загрузить существующий ресурс из файла JAR, и размер имеет значение здесь!

Если вы сомневаетесь, попробуйте скомпилировать и запустить класс выше, но убедитесь, что выбрали подходящий размер кучи (2 МБ):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

Здесь вы не столкнетесь с ошибкой OOM, так как ссылки не сохраняются, приложение будет работать независимо от того, какой размер вы выбрали ITERATIONS в приведенном выше примере. Потребление памяти вашим процессом (видимым сверху (RES / RSS) или проводником процессов) возрастает, если приложение не получает команду wait. В приведенной выше настройке он выделит около 150 МБ памяти.

Если вы хотите, чтобы приложение было безопасным, закройте поток ввода прямо там, где оно создано:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

и ваш процесс не будет превышать 35 МБ, независимо от количества итераций.

Довольно просто и удивительно.

сойка
источник
14

Как полагают многие, утечки ресурсов довольно легко вызвать - как примеры JDBC. Фактические утечки памяти немного сложнее - особенно если вы не полагаетесь на битые части JVM, чтобы сделать это за вас ...

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

Хотя один из способов, который раньше работал - и я не знаю, работает ли он по-прежнему, - это иметь трехглубинную круговую цепь. Поскольку в объекте A есть ссылка на объект B, объект B имеет ссылку на объект C, а объект C имеет ссылку на объект A. GC был достаточно умен, чтобы знать, что две глубокие цепочки - как в A <-> B - может быть безопасно собрана, если A и B недоступны для чего-либо еще, но не справились с трехсторонней цепью ...

Грэхем
источник
7
Некоторое время не было. Современные GC знают, как обрабатывать циклические ссылки.
assylias
13

Другой способ создать потенциально большие утечки памяти - хранить ссылки на Map.Entry<K,V>a TreeMap.

Трудно понять, почему это применимо только к TreeMaps, но, глядя на реализацию, причина может быть в следующем: a TreeMap.Entryхранит ссылки на своих братьев и сестер, поэтому, если a TreeMapготов для сбора, но какой-то другой класс содержит ссылку на любой из его Map.Entry, тогда вся карта будет сохранена в памяти.


Реальный сценарий:

Представьте, что у вас есть запрос БД, который возвращает большую TreeMapструктуру данных. Люди обычно используют TreeMaps, поскольку порядок вставки элемента сохраняется.

public static Map<String, Integer> pseudoQueryDatabase();

Если запрос вызывался много раз, и для каждого запроса (то есть для каждого Mapвозвращаемого) вы сохраняете Entryгде-то, память постоянно будет расти.

Рассмотрим следующий класс-оболочку:

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}

Заявка:

public class LeakTest {

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() {
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> {
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue().equals(index)) {
                    holdersCache.add(new EntryHolder(entry));
                    break;
                }
            }
            // to observe behavior in visualvm
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public static Map<String, Integer> pseudoQueryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    }

    public static void main(String[] args) throws Exception {
        new LeakTest().run();
    }
}

После каждого pseudoQueryDatabase()вызова mapэкземпляры должны быть готовы к сбору, но этого не произойдет, так как по крайней мере один Entryхранится где-то еще.

В зависимости от ваших jvmнастроек приложение может вылетать на ранней стадии из-за OutOfMemoryError.

Из этого visualvmграфика видно, как растет память.

Дамп памяти - TreeMap

То же самое не происходит с хешированной структурой данных ( HashMap).

Это график при использовании HashMap.

Дамп памяти - HashMap

Решение? Просто сохраните ключ / значение (как вы, вероятно, уже сделали) вместо сохранения Map.Entry.


Я написал более обширный тест здесь .

Марко Пачак
источник
11

Потоки не собираются до тех пор, пока они не прекратятся. Они служат корнями для сбора мусора. Они являются одним из немногих объектов, которые не будут возвращены, просто забыв о них или очистив ссылки на них.

Обратите внимание: основной шаблон для завершения рабочего потока состоит в том, чтобы установить некоторую переменную условия, видимую потоком. Поток может периодически проверять переменную и использовать ее в качестве сигнала для завершения. Если переменная не объявленаvolatile , то изменение в переменной может не быть замечено потоком, поэтому он не будет знать, что завершить. Или представьте, что некоторые потоки хотят обновить общий объект, но зашли в тупик, пытаясь заблокировать его.

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

В качестве примера игрушки:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

Называй System.gc()все, что хочешь, но переданный объект leakMeникогда не умрет

(* Изм *)

оборота Boann
источник
1
@ Spidey Ничто не "застряло". Вызывающий метод возвращается быстро, и переданный объект никогда не будет возвращен. Это именно утечка.
Boann
1
У вас будет поток, «работающий» (или спящий, что угодно) на протяжении всей жизни вашей программы. Это не считается утечкой для меня. Как и пул не считается утечкой, даже если вы не используете его полностью.
Spidey
1
@Spidey "У тебя будет [вещь] на всю жизнь твоей программы. Это для меня не утечка". Вы слышите себя?
Boann
3
@Spidey Если вы посчитаете, что память, о которой знает процесс, не просочилась, то все ответы здесь неверны, поскольку процесс всегда отслеживает, какие страницы в его виртуальном адресном пространстве отображаются. Когда процесс завершается, ОС устраняет все утечки, помещая страницы обратно в свободный стек страниц. Чтобы довести это до следующего крайнего уровня, можно довести до смерти любую аргументированную утечку, указав, что ни один из физических битов в микросхемах ОЗУ или в области подкачки на диске не был физически неуместен или уничтожен, поэтому вы можете выключить компьютер и снова, чтобы устранить любую утечку.
Boann
1
Практическое определение утечки состоит в том, что это память, которая была потеряна из-за того, что мы не знаем и, следовательно, не можем выполнить процедуру, необходимую для ее восстановления; мы должны были бы снести и восстановить все пространство памяти. Подобный мошеннический поток может возникать естественным образом из-за тупиковой или хитрой реализации пула потоков. Объекты, на которые ссылаются такие потоки, даже косвенно, теперь не могут быть собраны, поэтому у нас есть память, которая не будет естественным образом возвращаться или использоваться повторно в течение всей жизни программы. Я бы назвал это проблемой; в частности, это утечка памяти.
Boann
10

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

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

Конечно, однажды выявленная проблема может быть легко решена.

mschonaker
источник
10

Интервьюер, возможно, искал круговое справочное решение:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

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

-Вес Тарле

Уэсли Тарле
источник
12
Это классическая проблема с подсчетом ссылок сборщиками мусора. Даже 15 лет назад Java не использовала подсчет ссылок. Ссылка Подсчет также медленнее, чем GC.
bestsss
4
Не утечка памяти. Просто бесконечный цикл.
Эсбен Сков Педерсен
2
@Esben На каждой итерации предыдущая firstбесполезна и должна собираться мусором. При подсчете ссылок сборщиком мусора объект не будет освобожден, потому что на нем есть активная ссылка (сама по себе). Бесконечный цикл призван продемонстрировать утечку: при запуске программы объем памяти будет увеличиваться бесконечно.
Rds
@rds @ Уэсли Тарле, предположим, цикл не был бесконечным. Будет ли утечка памяти?
nz_21