Peter Rader
Peter Rader

Reputation: 245

WeakReference not collected in curly brackets?

This fails

public void testWeak() throws Exception {
    waitGC();
    {
        Sequence a = Sequence.valueOf("123456789");
        assert Sequence.used() == 1;
        a.toString();
    }
    waitGC();
}

private void waitGC() throws InterruptedException {
    Runtime.getRuntime().gc();
    short count = 0;
    while (count < 100 && Sequence.used() > 0) {
        Thread.sleep(10);
        count++;
    }
    assert Sequence.used() == 0: "Not removed!";
}

The test fails. Telling Not removed!.

This works:

public void testAWeak() throws Exception {
    waitGC();
    extracted();
    waitGC();
}
private void extracted() throws ChecksumException {
    Sequence a = Sequence.valueOf("123456789");
    assert Sequence.used() == 1;
    a.toString();
}
private void waitGC() throws InterruptedException {
    Runtime.getRuntime().gc();
    short count = 0;
    while (count < 100 && Sequence.used() > 0) {
        Thread.sleep(10);
        count++;
    }
    assert Sequence.used() == 0: "Not removed!";
}

It seems like the curly brackets does not affect the weakness.

Some official resources?

Upvotes: 2

Views: 89

Answers (1)

Holger
Holger

Reputation: 298153

Scope is a compile-time thing. It is not determining the reachability of objects at runtime, only has an indirect influence due to implementation details.

Consider the following variation of your test:

static boolean WARMUP;
public void testWeak1() throws Exception {
    variant1();
    WARMUP = true;
    for(int i=0; i<10000; i++) variant1();
    WARMUP = false;
    variant1();
}
private void variant1() throws Exception {
    AtomicBoolean track = new AtomicBoolean();
    {
        Trackable a = new Trackable(track);
        a.toString();
    }
    if(!WARMUP) System.out.println("variant1: "
                      +(waitGC(track)? "collected": "not collected"));
}
public void testWeak2() throws Exception {
    variant2();
    WARMUP = true;
    for(int i=0; i<10000; i++) variant2();
    WARMUP = false;
    variant2();
}
private void variant2() throws Exception {
    AtomicBoolean track = new AtomicBoolean();
    {
        Trackable a = new Trackable(track);
        a.toString();
        if(!WARMUP) System.out.println("variant2: "
                      +(waitGC(track)? "collected": "not collected"));
    }
}
static class Trackable {
    final AtomicBoolean backRef;
    public Trackable(AtomicBoolean backRef) {
        this.backRef = backRef;
    }
    @Override
    protected void finalize() throws Throwable {
        backRef.set(true);
    }
}

private boolean waitGC(AtomicBoolean b) throws InterruptedException {
    for(int count = 0; count < 10 && !b.get(); count++) {
        Runtime.getRuntime().gc();
        Thread.sleep(1);
    }
    return b.get();
}

on my machine, it prints:

variant1: not collected
variant1: collected
variant2: not collected
variant2: collected

If you can’t reproduce it, you may have to raise the number of warmup iterations.

What it demonstrates: whether a is in scope (variant 2) or not (variant 1) doesn’t matter, in either case, the object has not been collected in cold execution, but got collected after a number of warmup iterations, in other words, after the optimizer kicked in.


Formally, a is always eligible for garbage collection at the point we’re invoking waitGC(), as it is unused from this point. This is how reachability is defined:

A reachable object is any object that can be accessed in any potential continuing computation from any live thread.

In this example, the object can not be accessed by potential continuing computation, as no such subsequent computation that would access the object exists. However, there is no guaranty that a particular JVM’s garbage collector is always capable of identifying all of those objects at each time. In fact, even a JVM not having a garbage collector at all would still comply to the specification, though perhaps not the intent.

The possibility of code optimizations having an effect on the reachability analysis has also explicitly mentioned in the specification:

Optimizing transformations of a program can be designed that reduce the number of objects that are reachable to be less than those which would naively be considered reachable. For example, a Java compiler or code generator may choose to set a variable or parameter that will no longer be used to null to cause the storage for such an object to be potentially reclaimable sooner.


So what happens technically?

As said, scope is a compile-time thing. At the bytecode level, leaving the scope defined by the curly braces has no effect. The variable a is out of scope, but its storage within the stack frame still exists holding the reference until overwritten by another variable or until the method completes. The compiler is free to reuse the storage for another variable, but in this example, no such variable exists. So the two variants of the example above actually generate identical bytecode.

In an unoptimized execution, the still existing reference within the stack frame is treated like a reference preventing the object’s collection. In an optimized execution, the reference is only held until its last actual use. Inlining of its fields can allow its collection even earlier, up to the point that it is collected right after construction (or not getting constructed at all, if it hadn’t a finalize() method). The extreme end is finalize() called on strongly reachable object in Java 8

Things change, when you insert another variable, e.g.

private void variant1() throws Exception {
    AtomicBoolean track = new AtomicBoolean();
    {
        Trackable a = new Trackable(track);
        a.toString();
    }
    String message = "variant1: ";
    if(!WARMUP) System.out.println(message
                      +(waitGC(track)? "collected": "not collected"));
}

Then, the storage of a is reused by message after a’s scope ended (that’s of course, compiler specific) and the object gets collected, even in the unoptimized execution.

Note that the crucial aspect is the actual overwriting of the storage. If you use

private void variant1() throws Exception {
    AtomicBoolean track = new AtomicBoolean();
    {
        Trackable a = new Trackable(track);
        a.toString();
    }
    if(!WARMUP)
    {
        String message = "variant1: "
                       +(waitGC(track)? "collected": "not collected");
        System.out.println(message);
    }
}

The message variable uses the same storage as a, but its assignment only happens after the invocation of waitGC(track), so you get the same unoptimized execution behavior as in the original variant.


By the way, don’t use short for local loop variables. Java always uses int for byte, short, char, and int calculations (as you know, e.g. when trying to write shortVariable = shortVariable + 1;) and requiring it to cut the result value to short (which still happens implicitly when you use shortVariable++), adds an additional operation, so if you thought, using short improved the efficiency, notice that it actually is the opposite.

Upvotes: 1

Related Questions