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

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

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

Ответы [ 54 ]

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

Создайте статическую карту и продолжайте добавлять жесткие ссылки на нее. Они никогда не будут 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); }
}
17 голосов
/ 22 июля 2011

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

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

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

16 голосов
/ 22 июля 2011

Вы можете создать движущуюся утечку памяти, создав новый экземпляр класса в методе 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());
        }
    }
}
15 голосов
/ 13 сентября 2012

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

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

15 голосов
/ 03 июля 2011

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

14 голосов
/ 21 июля 2011

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

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

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

11 голосов
/ 31 августа 2013

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

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

(* изм *)

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

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

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 {

}
10 голосов
/ 03 июля 2011

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

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

Конечно, после идентификации проблему можно легко решить.

10 голосов
/ 26 апреля 2018

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

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


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

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

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 графика видно, как растет память.

Memory dump - TreeMap

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

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

Memory dump - HashMap

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


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

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