Scoped Variables 和 WeakReferences 奇怪地交互 - 有些对象没有得到垃圾收集

Scoped Variables and WeakReferences interact strangely - some objects don't get garbage collected

我在 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:新 test/workaround:

如果我在范围变量超出范围之前将其显式设置为 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);
    }
}

with Foo bar = new Foo();

  • 首先,您创建对象的引用
  • 其次,您创建 Foo 对象本身。

只要该引用或其他引用存在,就不能对特定对象进行 gc。但是,当您将 null 分配给该引用时...

bar = null ; 并假设没有其他对象引用该对象,它会在下次垃圾收集器经过时被释放并可供 gc 使用。

我认为您看到了一些关于如何将 Java 代码编译为字节码的交互。需要注意两件重要的事情:

  1. 垃圾收集器不保证何时或什至会收集对象。保证哪些对象不会。
  2. 字节码没有"local variables"。相反,它有一个本地堆栈,其中包含许多堆栈帧。局部变量被翻译成堆栈帧中的特定位置。

由于#1,Java 的作用域花括号不需要作为新的堆栈框架实现。相反,java 编译器可以为整个方法创建一个堆栈帧,并以与范围规则一致的方式使用它。这意味着,在第二个测试中,局部变量 person 由一个堆栈帧索引表示,该索引一直存在到方法结束,从而防止垃圾收集。

因为#2,并且因为局部变量在使用之前必须被初始化,所以java编译器可以重用堆栈帧的一个索引来表示多个局部变量,只要其中没有两个同时在范围内。因此,测试 3 和 4 中的所有 "different" person 局部变量最终都位于堆栈中的相同位置。

TL;DR:不要指望垃圾回收是一致的。收集对象的时间会受到您使用的 JVM GC 以及 Java 编译器的具体实现细节的影响。