我在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:新的测试/解决方法:
如果我在作用域变量超出作用域之前将其显式设置为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);
}
}
我认为您看到了一些关于如何将Java代码编译为字节码的交互。需要注意的两件重要事情:
- 垃圾收集器不保证何时,甚至不保证是否会收集对象。保证的是哪些物体不会出现
- 字节码没有"局部变量"。相反,它有一个本地堆栈,有许多堆栈帧。局部变量被转换到堆栈帧中的特定位置
由于#1,Java的作用域大括号不需要作为新的堆栈框架来实现。相反,java编译器可以为整个方法创建一个堆栈帧,并以与作用域规则一致的方式使用它。这意味着,在第二个测试中,局部变量person
由一个堆栈帧索引表示,该索引一直存在到方法结束,从而防止垃圾收集。
由于#2,并且局部变量在使用之前必须初始化,因此java编译器可以重用堆栈帧的一个索引来表示多个局部变量,只要它们中没有两个同时在作用域中即可。因此,测试3和测试4中所有"不同"的person
局部变量最终都位于堆栈上的同一位置。
TL;DR:不要期望垃圾收集是一致的。收集对象的时间可能会受到正在使用的JVM GC以及Java编译器的特定于实现的细节的影响。
使用Foo bar=new Foo((;
- 首先,创建对对象的引用
- 其次,创建Foo对象本身
只要那个引用或另一个引用存在,特定对象就不能是gc’d。但是,当您为该引用指定null时。。。
bar=空;假设没有任何其他对象引用该对象,那么在垃圾收集器下次经过时,它将被释放并可用于gc