Переменные Scoped и WeakReferences взаимодействуют странным образом - некоторые объекты не собирают мусор - PullRequest
3 голосов
/ 26 февраля 2020

Я вижу странное поведение в программе Java, и мне интересно, ожидается ли такое поведение и документируется ли оно где-либо.

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

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

Ниже приведен набор модульных тестов, которые показывают поведение, которое я вижу. Все эти тесты проходят как написано, и есть комментарии, где видно странное поведение. (Протестировано с использованием Oracle JDK 1.8 и OpenJDK 11.)

В первом тесте я вставляю в коллекцию WeakReference объекту, который возвращается из вызова функции:

List<WeakReference<Person>> refs = Lists.newArrayList();
refs.add(new WeakReference(getPerson("abc")));

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

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

List<WeakReference<Person>> refs = Lists.newArrayList();
{
    Person person = getPerson("abc");
    refs.add(new WeakReference(person));
}

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

List<WeakReference<Person>> refs = Lists.newArrayList();
{
    Person person = getPerson("abc");
    refs.add(new WeakReference(person));
}
...
{
    Person person = null;
}

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

List<WeakReference<Person>> refs = Lists.newArrayList();
{
    Person person1 = getPerson("abc");
    refs.add(new WeakReference(person1));
}
...
{
    Person person4 = null;
}

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


Обновлен 1: новый тест / обходной путь:

Если я явно установил переменную scoped в null, прежде чем она выйдет из В области видимости объекты собирают мусор, как я и ожидал.

List<WeakReference<Person>> refs = Lists.newArrayList();
{
    Person person = getPerson("abc");
    refs.add(new WeakReference(person));
    person = null;
}

Обновлено 2: еще один новый тест:

Новый посторонний объект не обязательно должен быть того же типа. Это отлично работает.

List<WeakReference<Person>> refs = Lists.newArrayList();
{
    Person person = getPerson("abc");
    refs.add(new WeakReference(person));
}
...
{
    String unused = "unused string";
}

import com.google.common.base.MoreObjects;
import com.google.common.collect.Lists;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

import static org.testng.Assert.assertEquals;

public class WeakReferenceCollectionTest {
    private static final Logger logger = LoggerFactory.getLogger(WeakReferenceCollectionTest.class);

    static class Person {
        private String name;

        public Person() {

        }

        public String getName() {
            return name != null ? name : "<null>";
        }

        public Person setName(String name) {
            this.name = name;
            return this;
        }

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this)
                              .add("name", name)
                              .toString();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            final Person person = (Person) o;
            return Objects.equals(name, person.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name);
        }
    }

    @Test
    public void collectionWorksAsExpected() throws InterruptedException {
        List<WeakReference<Person>> refs = Lists.newArrayList();
        refs.add(new WeakReference(getPerson("abc")));
        refs.add(new WeakReference(getPerson("bcd")));
        refs.add(new WeakReference(getPerson("cde")));

        assertEquals(refs.size(), 3);

        System.gc();
        Thread.sleep(1000);

        evictDeadRefs(refs);

        assertEquals(refs.size(), 0);

        refs.add(new WeakReference(getPerson("def")));
        refs.add(new WeakReference(getPerson("efg")));
        refs.add(new WeakReference(getPerson("fgh")));

        assertEquals(refs.size(), 3);

        System.gc();
        Thread.sleep(1000);

        evictDeadRefs(refs);

        assertEquals(refs.size(), 0);
    }

    @Test
    public void collectionWithScopesWorksDifferently() throws InterruptedException {
        List<WeakReference<Person>> refs = Lists.newArrayList();
        {
            Person person = getPerson("abc");
            refs.add(new WeakReference(person));
        }
        {
            Person person = getPerson("bcd");
            refs.add(new WeakReference(person));
        }
        {
            Person person = getPerson("cde");
            refs.add(new WeakReference(person));
        }

        assertEquals(refs.size(), 3);

        System.gc();
        Thread.sleep(1000);

        evictDeadRefs(refs);

        assertEquals(refs.size(), 1); // last one never goes away
        assertEquals(refs.get(0).get().getName(), "cde");

        {
            Person person = getPerson("def");
            refs.add(new WeakReference(person));
        }
        {
            Person person = getPerson("efg");
            refs.add(new WeakReference(person));
        }
        {
            Person person = getPerson("fgh");
            refs.add(new WeakReference(person));
        }

        assertEquals(refs.size(), 4); // previous last one is still in there

        System.gc();
        Thread.sleep(1000);

        evictDeadRefs(refs);

        assertEquals(refs.size(), 1); // last one never goes away
        assertEquals(refs.get(0).get().getName(), "fgh");
    }

    @Test
    public void collectionWithScopesAndNewVariableSetToNull() throws InterruptedException {
        List<WeakReference<Person>> refs = Lists.newArrayList();
        {
            Person person = getPerson("abc");
            refs.add(new WeakReference(person));
        }
        {
            Person person = getPerson("bcd");
            refs.add(new WeakReference(person));
        }
        {
            Person person = getPerson("cde");
            refs.add(new WeakReference(person));
        }
        {
            Person person = null;
        }

        assertEquals(refs.size(), 3);

        System.gc();
        Thread.sleep(1000);

        evictDeadRefs(refs);

        assertEquals(refs.size(), 0);
    }

    @Test
    public void collectionWithScopesAndDifferentVariableNames() throws InterruptedException {
        List<WeakReference<Person>> refs = Lists.newArrayList();
        {
            Person person1 = getPerson("abc");
            refs.add(new WeakReference(person1));
        }
        {
            Person person2 = getPerson("bcd");
            refs.add(new WeakReference(person2));
        }
        {
            Person person3 = getPerson("cde");
            refs.add(new WeakReference(person3));
        }
        {
            Person person4 = null;
        }

        assertEquals(refs.size(), 3);

        System.gc();
        Thread.sleep(1000);

        evictDeadRefs(refs);

        assertEquals(refs.size(), 0);
    }

    @Test
    public void collectionWithScopesAndExplicitlySetToNull() throws InterruptedException {
        List<WeakReference<Person>> refs = Lists.newArrayList();
        {
            Person person = getPerson("abc");
            refs.add(new WeakReference(person));
            person = null;
        }
        {
            Person person = getPerson("bcd");
            refs.add(new WeakReference(person));
            person = null;
        }
        {
            Person person = getPerson("cde");
            refs.add(new WeakReference(person));
            person = null;
        }

        assertEquals(refs.size(), 3);

        System.gc();
        Thread.sleep(1000);

        evictDeadRefs(refs);

        assertEquals(refs.size(), 0);
    }

    @Test
    public void createUnrelatedVariable() throws InterruptedException {
        List<WeakReference<Person>> refs = Lists.newArrayList();
        {
            Person person = getPerson("abc");
            refs.add(new WeakReference(person));
        }
        {
            Person person = getPerson("bcd");
            refs.add(new WeakReference(person));
        }
        {
            Person person = getPerson("cde");
            refs.add(new WeakReference(person));
        }
        {
            String unused = "unused string";
        }

        assertEquals(refs.size(), 3);

        System.gc();
        Thread.sleep(1000);

        evictDeadRefs(refs);

        assertEquals(refs.size(), 0);
    }

    private void evictDeadRefs(List<WeakReference<Person>> refs) {
        final Iterator<WeakReference<Person>> it = refs.iterator();
        while (it.hasNext()) {
            final WeakReference<Person> ref = it.next();
            if (ref.get() == null) {
                logger.debug("evictDeadRefs(): removing ref");
                it.remove();
            } else {
                logger.debug("evictDeadRefs(): ref is not null: " + ref.get());
            }
        }
    }

    private Person getPerson(String s) {
        return new Person().setName(s);
    }
}

Ответы [ 2 ]

4 голосов
/ 27 февраля 2020

Я думаю, вы видите некоторые взаимодействия с тем, как код Java компилируется в байт-код. Обратите внимание на две важные вещи:

  1. Сборщик мусора не гарантирует, когда или даже если объект будет собран. Гарантия на то, какие объекты не будут.
  2. Байт-код не имеет "локальных переменных". Вместо этого он имеет локальный стек со многими кадрами стека. Локальная переменная преобразуется в указанное c местоположение в кадре стека.

Из-за # 1 ограничивающие фигурные скобки Java не должны быть реализованы как новый кадр стека , Вместо этого компилятор java может создать один кадр стека для всего метода и использовать его таким образом, чтобы это соответствовало правилам области видимости. Это означает, что во втором тесте локальная переменная person представлена ​​индексом фрейма стека, который длится до конца метода, предотвращая сборку мусора.

Из-за # 2 и потому, что локальные переменные должны инициализируясь перед использованием, компилятор java может повторно использовать один индекс фрейма стека для представления нескольких локальных переменных, если только два из них не находятся в области действия одновременно. Таким образом, все ваши "разные" person локальные переменные в тестах 3 и 4 в конечном итоге окажутся в одном и том же месте в стеке.

TL; DR: не ожидайте, что сборка мусора будет последовательной. Когда объект собирается, на него может повлиять как используемая JVM G C, так и спецификация реализации c деталей вашего Java компилятора.

0 голосов
/ 26 февраля 2020

с Foo bar = new Foo ();

  • во-первых, вы создаете ссылку на объект
  • , во-вторых, вы создаете сам объект Foo.

Пока существует эта или другая ссылка, указанный c объект не может быть g c 'd. однако, когда вы присваиваете null этой ссылке ...

bar = null; и предполагая, что ничто иное не имеет ссылки на объект, оно освобождается и становится доступным для g c при следующем прохождении сборщика мусора.

...