OutOfMemoryError - почему ожидающий поток не может быть сборщиком мусора? - PullRequest
5 голосов
/ 24 апреля 2009

Этот простой пример кода демонстрирует проблему. Я создаю ArrayBlockingQueue и поток, который ожидает данные в этой очереди, используя take(). После завершения цикла теоретически и очередь, и поток могут быть удалены сборщиком мусора, но на практике я вскоре получаю OutOfMemoryError. Что мешает этому быть GC'd и как это можно исправить?

/**
 * Produces out of memory exception because the thread cannot be garbage
 * collected.
 */
@Test
public void checkLeak() {
    int count = 0;
    while (true) {

        // just a simple demo, not useful code.
        final ArrayBlockingQueue<Integer> abq = new ArrayBlockingQueue<Integer>(2);
        final Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    abq.take();
                } catch (final InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();

        // perform a GC once in a while
        if (++count % 1000 == 0) {
            System.out.println("gc");
            // this should remove all the previously created queues and threads
            // but it does not
            System.gc();
        }
    }
}

Я использую Java 1.6.0.

ОБНОВЛЕНИЕ: выполнить GC после нескольких итераций, но это не помогает.

Ответы [ 7 ]

8 голосов
/ 24 апреля 2009

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

5 голосов
/ 24 апреля 2009

Вы создаете потоки на неопределенный срок, потому что все они блокируются, пока ArrayBlockingQueue<Integer> abq не будет иметь какую-либо запись. Так что в итоге вы получите OutOfMemoryError.

(редактирование)

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

2 голосов
/ 24 апреля 2009
abq.put(0);

должен спасти ваш день.

Все ваши потоки ждут своей очереди take(), но вы никогда ничего не помещаете в эти очереди.

0 голосов
/ 24 апреля 2009

Вызов System.gc ничего не делает, потому что нечего собирать. Когда поток запускается, он увеличивает счетчик ссылок на потоки, если это не будет сделано, поток будет прерываться неопределенно. Когда метод запуска потока завершается, счетчик ссылок потока уменьшается.

while (true) {
    // just a simple demo, not useful code.
    // 0 0 - the first number is thread reference count, the second is abq ref count
    final ArrayBlockingQueue<Integer> abq = new ArrayBlockingQueue<Integer>(2);
    // 0 1
    final Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                abq.take();
                // 2 2
            } catch (final InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    // 1 1
    t.start();
    // 2 2 (because the run calls abq.take)
    // after end of loop
    // 1 1 - each created object's reference count is decreased
}

Теперь существует потенциальное состояние гонки - что, если основной цикл завершается и выполняет сборку мусора до того, как поток t сможет выполнить какую-либо обработку, то есть он будет приостановлен ОС до выполнения оператора abq.take? Метод run попытается получить доступ к объекту abq после того, как GC выпустил его, что было бы плохо.

Чтобы избежать условия гонки, вы должны передать объект в качестве параметра методу выполнения. Я не уверен насчет Java в эти дни, это было давно, поэтому я бы предложил передать объект в качестве параметра конструктора классу, производному от Runnable. Таким образом, перед вызовом метода run есть дополнительная ссылка на abq, что гарантирует, что объект всегда будет действительным.

0 голосов
/ 24 апреля 2009

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

Я не могу сказать, как лучше это исправить, так как мне нужно знать, что вы пытаетесь сделать, но вы можете посмотреть на java.util.concurrent, чтобы увидеть, есть ли в нем классы для того, что вы делаете необходимо.

0 голосов
/ 24 апреля 2009

Ваш цикл while представляет собой бесконечный цикл, и он непрерывно создает новые потоки. Хотя вы запускаете выполнение потока, как только он создан, но время, необходимое ему для выполнения задачи потоком, больше, чем время, необходимое для создания потока.

Также, что вы делаете с параметром abq, объявляя его внутри цикла while?

На основании ваших правок и других комментариев. System.gc () не гарантирует цикл GC. Прочитайте мое утверждение выше, скорость исполнения вашего потока ниже, чем скорость создания.

Я проверил комментарий для метода take () "Извлекает и удаляет заголовок этой очереди, ожидая, если в этой очереди нет элементов". Я вижу, что вы определяете ArrayBlockingQueue, но вы не добавляете к нему никаких элементов, поэтому весь ваш поток просто ожидает этого метода, поэтому вы получаете OOM.

0 голосов
/ 24 апреля 2009

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

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

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

...