Создание утечки памяти с Java

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

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

2820 голосов | спросил 13 revs, 11 users 48%
Mat Banik
1 Jam1000000amThu, 01 Jan 1970 03:00:00 +030070 1970, 03:00:00

30 ответов


0

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

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

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

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

Статическое поле, содержащее ссылку на объект [окончательное поле 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 .

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

Простая вещь - это использовать 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.
ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

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

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

Ниже приведен довольно бессмысленный пример, если вы не понимаете 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 s, но не закрывая их, когда они больше не нужны (аналогично приведенному выше примеру с классом Connection).
  • Не выгружать Singletons при закрытии приложения Java EE. По-видимому, загрузчик классов, который загрузил класс singleton, сохранит ссылку на класс, и, следовательно, экземпляр singleton никогда не будет собран. При развертывании нового экземпляра приложения обычно создается новый загрузчик классов, а прежний загрузчик классов продолжает существовать из-за синглтона.
ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

}
ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

Вот простой /зловещий пример с помощью 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 будет удален.

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

В Log4j этот механизм называется вложенный диагностический контекст (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, что приводило к утечке памяти.

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

Я подумал, что было интересно, что никто не использовал примеры внутренних классов. Если у вас есть внутренний класс; он по своей сути поддерживает ссылку на содержащий класс. Конечно, технически это не утечка памяти, потому что 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 предполагает, что он всегда будет существовать, и на самом деле никогда не будет пытаться очистить его, если его больше нельзя будет найти в коде. Но это совершенно не проверено.

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

Создайте статическую карту и продолжайте добавлять к ней жесткие ссылки. Это никогда не будет 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); }
}
ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

Вы можете создать движущуюся утечку памяти, создав новый экземпляр класса в методе 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());
        }
    }
}
ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

Интервьюер, вероятно, искал циклическую ссылку, подобную приведенному ниже коду (что, кстати, приводит к утечке памяти только в очень старых 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.

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

Недавно я столкнулся с более тонкой утечкой ресурсов. Мы открываем ресурсы через 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) или проводником процессов) возрастает, если приложение не получает команду ожидания. В приведенной выше настройке он выделит около 150 МБ памяти.

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

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

public class ServiceFactory {

private Map<String, Service> services;

private static ServiceFactory singleton;

private ServiceFactory() {
    services = new HashMap<String, Service>();
}

public static synchronized ServiceFactory getDefault() {

    if (singleton == null) {
        singleton = new ServiceFactory();
    }
    return singleton;
}

public void addService(String name, Service serv) {
    services.put(name, serv);
}

public void removeService(String name) {
    services.remove(name);
}

public Service getService(String name, Service serv) {
    return services.get(name);
}

// the problematic api, which expose the map.
//and user can do quite a lot of thing from this api.
//for example, create service reference and forget to dispose or set it null
//in all this is a dangerous api, and should not expose 
public Map<String, Service> getAllServices() {
    return services;
}

}

// resource class is a heavy class
class Service {

}
ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

Обратите внимание: основной шаблон для завершения рабочего потока - это установка некоторой условной переменной, видимой потоком. Поток может периодически проверять переменную и использовать ее как сигнал для завершения. Если переменная не объявлена ​​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 никогда не умрет.

(* изм *)

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37
0

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

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

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

-Wes Tarle

ответил Mauro 19 +03002017-10-19T05:33:37+03:00312017bEurope/MoscowThu, 19 Oct 2017 05:33:37 +0300 2017, 05:33:37

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132