Создание утечки памяти с Java - PullRequest
2986 голосов
/ 24 июня 2011

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

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

Ответы [ 54 ]

2170 голосов
/ 24 июня 2011

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

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

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

(Это было хуже во многих реализациях JVM, особенно до Java 7, потому что Classes и ClassLoaders были размещены прямо в permgen и никогда не были GC'ами вообще. Однако, независимо от того, как JVM обрабатывает выгрузку классов, ThreadLocal будет по-прежнему препятствует восстановлению объекта Class.)

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

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

1161 голосов
/ 01 июля 2011

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

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 .

440 голосов
/ 24 июня 2011

Простая вещь, которую нужно сделать, это использовать 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.
261 голосов
/ 30 июня 2011

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

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

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

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

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

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

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

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

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

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

187 голосов
/ 01 июля 2011

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

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

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

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

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

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

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

153 голосов
/ 02 июля 2011

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

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

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

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

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

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

128 голосов
/ 24 июня 2011

Ниже приведен довольно бессмысленный пример, если вы не понимаете 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 finalize не очищает все, можно обнаружить, что физическое соединение с сервером базы данных будет длиться несколько циклов сбора мусора, пока сервер базы данных в конечном итоге не выяснит что соединение не является активным (если оно существует) и должно быть закрыто.

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

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

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

  • Управление экземпляром List, когда вы только добавляете в список, а не удаляете из него (хотя вы должны избавляться от элементов, которые вам больше не нужны), или
  • Открытие Socket с или File с, но не закрытие их, когда они больше не нужны (аналогично приведенному выше примеру с классом Connection).
  • Не выгружать Singletons при закрытии приложения Java EE. По всей видимости, загрузчик классов, который загрузил класс singleton, сохранит ссылку на класс, и, следовательно, экземпляр singleton никогда не будет собран. При развертывании нового экземпляра приложения обычно создается новый загрузчик классов, и прежний загрузчик классов продолжает существовать из-за синглтона.
115 голосов
/ 25 июня 2011

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

66 голосов
/ 24 июня 2011

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

50 голосов
/ 11 июля 2011

Вы можете сделать утечку памяти с 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();
        }
    }

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...