Странный код в java.util.concurrent.LinkedBlockingQueue

Все!

Я нашел странный код в LinkedBlockingQueue:

private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
}

Кто может объяснить, зачем нам нужна локальная переменная h? Как это может помочь GC?

Ответ 1

Чтобы лучше понять, что происходит, посмотрите, как выглядит список после выполнения кода. Сначала рассмотрим начальный список:

1 -> 2 -> 3

Затем h указывает на head и first на h.next:

1 -> 2 -> 3
|    |
h    first

Затем h.next указывает на h и head указывает на first:

1 -> 2 -> 3
|   / \
h head first

Теперь практически мы знаем, что есть только активная ссылка, указывающая на первый элемент, который сам по себе (h.next = h), и мы также знаем, что GC собирает объекты, которые не имеют более активных ссылок, поэтому, когда метод заканчивается, (старая) глава списка может быть безопасно собрана GC, поскольку h существует только в рамках метода.

Сказав это, было указано, и я согласен с этим, что даже с классическим методом dequeue (т.е. просто сделать first точкой head.next и head указать на first), нет больше ссылок, указывающих на старую голову. Однако в этом сценарии старая голова остается болтающейся в памяти и все еще имеет поле next, указывающее на first, тогда как в отправленном вами коде остается только один объект, указывающий на себя. Это может привести к тому, что GC будет действовать быстрее.

Ответ 2

Если вы посмотрите на jsr166 src, вы обнаружите оскорбительную фиксацию

http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/main/java/util/concurrent/LinkedBlockingQueue.java?view=log (см. v 1.51)

Это показывает, что ответ указан в этом отчете об ошибке

http://bugs.sun.com/view_bug.do?bug_id=6805775

Полное обсуждение в этой теме

http://thread.gmane.org/gmane.comp.java.jsr.166-concurrency/5758

"Помощник GC" - это то, что вы избегаете кровотечения.

Приветствия

Matt

Ответ 3

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

Прежде всего, каждый java GC делает какую-то трассировку из корневого набора так или иначе. Это означает, что если старая голова будет собрана, мы все равно не будем читать переменную next - нет причин для этого. Следовательно, IF голова собирается на следующей итерации, это не имеет значения.

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

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

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

Краткий пример:

Предположим, что 1 (old) -> 2 (young) -> 3 (xx). Если мы удалим 1 и 2 из нашего списка, мы можем ожидать, что оба элемента будут собраны следующим GC. Но если происходит только молодой GC, и мы НЕ удалили указатель next в старом, оба элемента 1 и 2 не будут собраны. В отличие от этого, если мы удалили указатель в 1, 2 будет собран молодым GC..

Ответ 4

Вот пример кода, который иллюстрирует вопрос: http://pastebin.com/zTsLpUpq. Выполнение GC после runWith() и получение дампа кучи для обеих версий говорит только один экземпляр элемента.