Я вижу странное поведение в программе 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);
}
}