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 代码编译为字节码的交互。需要注意两件重要的事情:
- 垃圾收集器不保证何时或什至会收集对象。保证哪些对象不会。
- 字节码没有"local variables"。相反,它有一个本地堆栈,其中包含许多堆栈帧。局部变量被翻译成堆栈帧中的特定位置。
由于#1,Java 的作用域花括号不需要作为新的堆栈框架实现。相反,java 编译器可以为整个方法创建一个堆栈帧,并以与范围规则一致的方式使用它。这意味着,在第二个测试中,局部变量 person
由一个堆栈帧索引表示,该索引一直存在到方法结束,从而防止垃圾收集。
因为#2,并且因为局部变量在使用之前必须被初始化,所以java编译器可以重用堆栈帧的一个索引来表示多个局部变量,只要其中没有两个同时在范围内。因此,测试 3 和 4 中的所有 "different" person
局部变量最终都位于堆栈中的相同位置。
TL;DR:不要指望垃圾回收是一致的。收集对象的时间会受到您使用的 JVM GC 以及 Java 编译器的具体实现细节的影响。
我在 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 代码编译为字节码的交互。需要注意两件重要的事情:
- 垃圾收集器不保证何时或什至会收集对象。保证哪些对象不会。
- 字节码没有"local variables"。相反,它有一个本地堆栈,其中包含许多堆栈帧。局部变量被翻译成堆栈帧中的特定位置。
由于#1,Java 的作用域花括号不需要作为新的堆栈框架实现。相反,java 编译器可以为整个方法创建一个堆栈帧,并以与范围规则一致的方式使用它。这意味着,在第二个测试中,局部变量 person
由一个堆栈帧索引表示,该索引一直存在到方法结束,从而防止垃圾收集。
因为#2,并且因为局部变量在使用之前必须被初始化,所以java编译器可以重用堆栈帧的一个索引来表示多个局部变量,只要其中没有两个同时在范围内。因此,测试 3 和 4 中的所有 "different" person
局部变量最终都位于堆栈中的相同位置。
TL;DR:不要指望垃圾回收是一致的。收集对象的时间会受到您使用的 JVM GC 以及 Java 编译器的具体实现细节的影响。