Before I explain what's going on, a general point to keep in mind:
This kind of hackery is unsupported. And I mean that in the literal sense: The JDK specification takes pains to make zero promises on this, and therefore, any JDK change (be it because some different vendor ships it, it's for another platform, it's a new version) may change how this works or break it entirely. Hence, any code you create that attempts to modify static finals, even if 'it works fine every time on all hardware and platforms I have access to', is still a hot potato: That doesn't mean it'll work anywhere else, and pretty much every JDK release you have to put in your agenda: Check if this stuff still works. Because it might not, and if it no longer does, you can file bug reports at openjdk.net all day long and they'll ignore them. Because that isn't a bug: The spec doesn't promise this will ever work.
In other words, my advice is: Find another way to do this stuff. The route you have chosen, even once you tackle the problems you are now having with it, is unstable, requiring constant vigilance.
Nevermind the fact that use of unSafe is, on its own, something that OpenJDK is pretty much saying every day: We're going to kill that off some day.
So why is this happening
The key lies in javap
. Javap turns class files into bytecode dumps, it lets you see exactly what is javac
did, and often this can lead to useful insights. Let's first compile this code and then javap it; we have to use -c
and -v
to respectively show all bytecode and show as much detail as it can. Sometimes, toss -p
in there too (show even private/package-private stuff).
> cat Test.java
class Test {
public static final String SF = "Original";
}
> javac Test.java; javap -c -v Test
....
public static final java.lang.String SF;
descriptor: Ljava/lang/String;
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String Original
....
(and there is no static block here at all)
This shows that certain fields can be compiled into a 'constant' - where the field directly knows that value (The ConstantValue: String Original
line).
Usage of this variable in other code:
> cat Test2.java
class Test2 {
String x = Test.SF;
}
> javac Test2.java; javap -c -v Test2
....
#9 = String #10 // Original
#10 = Utf8 Original
Test2();
descriptor: ()V
flags: (0x0000)
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #9 // String Original
7: putfield #11 // Field x:Ljava/lang/String;
....
This shows that the Test2
code loads "Original"
straight in, it doesn't actually refer to Test.SF
whatsoever (and, in fact, Test2 is 100% independent from Test, you can entirely delete Test.class now; Test2.class doesn't need it).
Obviously then, for compile time constant cases, you can change the original (Test.SF
) field all day long, no code actually uses it, instead, at compile time javac
noticed it was constant and just inlined them all.
Okay, but, I used Object
Yes, and we can now tell how that changes things:
> cat Test.java
class Test {
public static final Object SF = "Original";
}
> javac Test.java; javap -c -v Test
....
public static final java.lang.Object SF;
descriptor: Ljava/lang/Object;
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL static {};
.....
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #7 // String Original
2: putstatic #9 // Field SF:Ljava/lang/Object;
5: return
....
Note how now all of a sudden a static block appeared, and this block loads in the string constant and assigns it to a static field (that's what the putstatic
bytecode does).
The static field no longer has a ConstantValue:
associated with it at all.
static {}
is code, it must be executed. When a JVM starts up, it does not initialize each and every class on the entire classpath. If it did, JVM bootup would take a ridiculously long time. Instead, the JVM initializes classes on demand: Anytime any class is needed for the first time, the JVM just grabs that from its classloader cache, but if there is no such class in the cache, it is loaded: Disk access (or whatever is needed to get the data in the class file) happens to load it. Class loading has a 2-step model:
- First, load it in, as in, parse through the contents of the
.class
definition.
- Second, 'initialize' it which requires, amongst a few other things, running any and all
static{}
initializers. Note that e.g. static final long FOO = System.currentTimeMillis();
in java code is just a 'weird' way to have a static initializer - anytime you assign an expression to a static field right there as you declare it, unless that is a compile time constant, it results in a static initializer block. static { ... }
is merely a way to make it explicit.
What's happening in your example is that you're using theUnsafe
to modify the static field in between those 2 steps. So, you modify it first, and then you cause the init step to happen which runs the static initializer which overwrites what you just hacked it into. Once it is initialized, the JVM doesn't initialize it again, so once you cause initialization to happen (and new Other()
will do just that), then you no longer run into this issue.
A trivial solution to this problem is to force initialization to happen on your own (Class.forName("name.of.TheClass")
is one way to do that), and only then futz about with it via theUnsafe
.