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.
Aleksey Shipilëv, JVM/Performance Geek
Shout out at Twitter: @shipilev; Questions, comments, suggestions: aleksey@shipilev.net
Theory
This has deep roots in C/C++ experience of many programmers, because it is said in the scripture:
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.
[Note: these objects are initialized and destroyed as described in 6.7. ]
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.
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."
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.