About, Disclaimers, Contacts

"JVM Anatomy Quarks" is the on-going mini-post series, where every post is describing some elementary piece of knowledge about JVM. The name underlines the fact that the single post cannot be taken in isolation, and most pieces described here are going to readily interact with each other.

The post should take about 5-10 minutes to read. As such, it goes deep for only a single topic, a single test, a single benchmark, a single observation. The evidence and discussion here might be anecdotal, not actually reviewed for errors, consistency, writing 'tyle, syntaxtic and semantically errors, duplicates, or also consistency. Use and/or trust this at your own risk.

350

Aleksey Shipilëv, JVM/Performance Geek
Shout out at Twitter: @shipilev; Questions, comments, suggestions: aleksey@shipilev.net

Question

The references stored in local variables are collected when they go out of scope. Right?

Theory

This has deep roots in C/C++ experience of many programmers, because it is said in the scripture:

  1. Local objects explicitly declared auto or register or not explicitly declared static or extern have automatic storage duration. The storage for these objects lasts until the block in which they are created exits.

  2. [Note: these objects are initialized and destroyed as described in 6.7. ]

  3. If a named automatic object has initialization or a destructor with side effects, it shall not be destroyed before the end of its block, nor shall it be eliminated as an optimization even if it appears to be unused, except that a class object or its copy may be eliminated as specified in 12.8.

— C++98 Standard
3.7.2 "Automatic storage duration"

This is a very useful language property, because it allows to bind the object lifetime to the syntactic code block. Which allows doing, for example, this:

void method() {
  ...something...

  {
     MutexLocker ml(mutex);
     ...something under the lock...
  } // ~MutexLocker unlocks

  ...something else...
}

Coming from a C++ land, you would naturally expect the same property to hold in Java. There are no destructors, but there are ways to detect if object is deemed unreachable, and act accordingly, e.g. via soft/weak/phantom references or finalizers. However, the syntactic code blocks in Java do not act that way. See for example:

"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."

— Java Language Specification 8
12.6.1 "Implementing Finalization"

Does this really matter?

Experiment

This difference is fairly easy to demonstrate with the experiment. Take this class as example:

public class LocalFinalize {
    ...
    private static volatile boolean flag;

    public static void pass() {
        MyHook h1 = new MyHook();
        MyHook h2 = new MyHook();

        while (flag) {
            // spin
        }

        h1.log();
    }

    public static class MyHook {
       public MyHook() {
           System.out.println("Created " + this);
       }

       public void log() {
           System.out.println("Alive " + this);
       }

       @Override
       protected void finalize() throws Throwable {
           System.out.println("Finalized " + this);
       }
    }
}

Naively, one could presume that the lifetime of h2 extends to the end of the pass method. And since there is a waiting loop in the middle that might not terminate with flag set to true, the object would never be considered for finalization.

The caveat is that we want the method to be compiled to see the interesting behavior. To force this, we can do two passes: first pass will enter the method, spin for a while, and then exit. This will compile the method fine, because the loop body would be executed many times, and that will trigger compilation. Then we can enter the second time, but never leave the loop again.

Something like this will do:

public static void arm() {
    new Thread(() -> {
        try {
             Thread.sleep(5000);
             flag = false;
        } catch (Throwable t) {}
    }).start();
}

public static void main(String... args) throws InterruptedException {
    System.out.println("Pass 1");
    arm();
    flag = true;
    pass();

    System.out.println("Wait for pass 1 finalization");
    Thread.sleep(10000);

    System.out.println("Pass 2");
    flag = true;
    pass();
}

We would also like a background thread forcing GC repeatedly, to cause finalization. Okay, the setup is done (full source here), let’s run:

$ java -version
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)

$ java LocalFinalize
Pass 1
Created LocalFinalize$MyHook@816f27d     # h1 created
Created LocalFinalize$MyHook@87aac27     # h2 created
Alive LocalFinalize$MyHook@816f27d       # h1.log called

Wait for pass 1 finalization
Finalized LocalFinalize$MyHook@87aac27   # h1 finalized
Finalized LocalFinalize$MyHook@816f27d   # h2 finalized

Pass 2
Created LocalFinalize$MyHook@3e3abc88    # h1 created
Created LocalFinalize$MyHook@6ce253f1    # h2 created
Finalized LocalFinalize$MyHook@6ce253f1  # h2 finalized (!)

Oops. That happened because the optimizing compiler knew the last use of h2 was right after the allocation. Therefore, when communicating what live variables are present — later during the loop execution — to the garbage collector, it does not consider h2 live anymore. Therefore, garbage collector treats that MyHook instance as dead and runs its finalization. Since the h1 use is later after the loop, it is considered reachable, and finalization is silent.

This is actually a great feature, because it lets GC can reclaim huge buffers allocated locally without requiring to exit the method, e.g.:

void processAndWait() {
  byte[] buf = new byte[1024 * 1024];
  writeToBuf(buf);
  processBuf(buf); // last use!
  waitForTheDeathOfUniverse(); // oops
}

Going Deeper

In fact, you can see the technicalities of this in various disassemblies. First, the bytecode disassembly does not even mention the local variable data at all, and the slot 1 where the h2 instance is stored is left alone until the end of the method:

$ javap -c -v -p LocalFinalize.class

  public static void pass();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: new           #17                 // class LocalFinalize$MyHook
         3: dup
         4: invokespecial #18                 // Method LocalFinalize$MyHook."<init>":()V
         7: astore_0
         8: new           #17                 // class LocalFinalize$MyHook
        11: dup
        12: invokespecial #18                 // Method LocalFinalize$MyHook."<init>":()V
        15: astore_1
        16: getstatic     #10                 // Field flag:Z
        19: ifeq          25
        22: goto          16
        25: aload_0
        26: invokevirtual #19                 // Method LocalFinalize$MyHook.log:()V
        29: return

Compiling with debug data (javac -g) yields Local Variable Table (LVT), where the lifetime of local variable "seems" to extend by the end of the method:

  public static void pass();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: new           #17                 // class LocalFinalize$MyHook
         3: dup
         4: invokespecial #18                 // Method LocalFinalize$MyHook."<init>":()V
         7: astore_0
         8: new           #17                 // class LocalFinalize$MyHook
        11: dup
        12: invokespecial #18                 // Method LocalFinalize$MyHook."<init>":()V
        15: astore_1
        16: getstatic     #10                 // Field flag:Z
        19: ifeq          25
        22: goto          16
        25: aload_0
        26: invokevirtual #19                 // Method LocalFinalize$MyHook.log:()V
        29: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            8      22     0    h1   LLocalFinalize$MyHook; //  8 + 22 = 30
           16      14     1    h2   LLocalFinalize$MyHook; // 16 + 14 = 30

This might confuse people into believing the reachability is actually extended to the end of the method, because "scope" is thought to be defined by LVT. But it is not, because optimizers could actually figure out that local is not used further, and optimize accordingly. In our current test, this happens (in pseudocode):

public static void pass() {
    MyHook h1 = new MyHook();
    MyHook h2 = new MyHook();

    while (flag) {
        // spin
        // <gc safe point here>
        // Here, compiled code knows what references are present in machine
        // registers and on stack. By then, the "h2" is already past its last use,
        // and this map has no evidence of "h2". Therefore, GC treats it as dead.
    }

    h1.log();
}

This is somewhat visible in -XX:+PrintAssembly output:

data16 data16 xchg %ax,%ax      # loop alignment
                                # output would also say:
                                # ImmutableOopMap{r10=Oop rbp=Oop}
LOOP:
  test   %eax,0x15ae2bca(%rip)  # safepoint poll, switch to GC can happen here
  movzbl 0x70(%r10),%r8d        # get this.flag
  test   %r8d,%r8d              # check flag and loop back
  jne    LOOP

...

ImmutableOopMap{r10=Oop rbp=Oop} basically says that %r10 and %rbp hold the "ordinary object pointers". %r10 holds this — see how we read flag off it, and %rbp holds the reference to h1 that would be used later. Reference to h2 is missing here. If GC happens during the loop, the thread would block when doing the safepoint poll, and at that time the runtime would know exactly what registers to care for, with the help of this map.

Alternatives

Extending the reachability of the object stored in local variable to the given program point can be done by using that local variable later. However, that is seldom easy to do without observable side effects. For example, "just" calling the method and passing that local variable is not enough, because the method might get inlined, and the same optimization kicks in. Since Java 9, there is java.lang.ref.Reference::reachabilityFence method that provides required semantics.

If you "just" want to have C++ like "release on block exit" construct — to do something when leaving the block — then try-finally is your friend in Java.

Observations

Reachability for Java local variables is not defined by syntactic blocks, it is at least to the last use, and may be exactly to the last use. Using the mechanisms that notify when some object becomes unreachable (finalizers, weak/soft/phantom references) may fall victim of "early" detection while execution had not yet left the method/block it was reachable from first.