0

I tried applying sun.misc.Unsafe to modify public static final field of some class for unit testing and jmh benchmarks. Testing with, at least, JDK11 to JDK17, it seemed to work well only after the class object is created or some static function of that class is called as some trigger. My full code is in the bottom.

Questions

  • Why is it required before modification?
  • Is there any other way to do modification with less executions and memory consumption?

More Details

Some methods worked only in SOME platform or JDK version. For example:

Object o = Another.class;

// ...

or

{
    Object o = Another.class;
} // save stack

// ...

worked with version "16-ea" in sololearn playground, but not work on my OpenJDK 15.0.2+7 (Zulu15.29.15 on macOS 13.1).

And another method that worked in both platforms:

{
    Object a = new Another();
}

// ...

or

Another.hi();

// ...

class Another () {
    public static void hi () {}
}

Reference

https://stackoverflow.com/a/61150853/8244977

Full Example

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class Program
{
    public static final Object SF = "Original";

    public static void main(String[] args) throws Exception {
        ////////////////////////////////////////////////////////////////////////////////
        // Test with this class

        modify(Program.class.getDeclaredField("SF"), "New-1");
        System.out.println("Program.SF:\t" + Program.SF);  // Worked: Program.SF:   New-1

        modify(Program.class.getDeclaredField("SF"), "New-2");
        System.out.println("Program.SF:\t" + Program.SF);  // Worked: Program.SF:   New-2

        ////////////////////////////////////////////////////////////////////////////////
        // Test with another class

        modify(Another.class.getDeclaredField("SF"), "New-1");
        System.out.println("Another.SF:\t" + Another.SF);  // Failed: Another.SF:   Original

        // like a trigger
        {
            Object c = new Another();
        }

        modify(Another.class.getDeclaredField("SF"), "New-2");
        System.out.println("Another.SF:\t" + Another.SF);  // Worked: Another.SF:   New-2
    }

    public static void modify(Field f, Object v) throws Exception {
        Field fu = Unsafe.class.getDeclaredField("theUnsafe");
        fu.setAccessible(true);
        Unsafe u = (Unsafe) fu.get(null);

        u.putObject(
            u.staticFieldBase(f),
            u.staticFieldOffset(f),
            v
        );        
    }
}

class Another {
    public static final Object SF = "Original";
}
BingLi224
  • 452
  • 2
  • 6
  • 12
  • 4
    [You can't change `static final` fields](https://gist.github.com/DasBrain/ca16053f8e073c8a883bee66302c2fee). It was never supported, and while it may appear to work, it may not work at all in the future. – Johannes Kuhn Mar 12 '23 at 13:18

3 Answers3

3

You shouldn't write code (not even test code) that relies on being able to change static final fields. It has never been supported, and the various schemes for doing this are not guaranteed to work. Indeed some schemes that have been used in the past no longer work with recent Java versions.

As to your observation, the likely explanation is that if you use Unsafe to modify a static final before class initialization has been triggered, the actual class initialization (e.g. the code in the <cinit> pseudo-method) or the lazy initialization of compile-time constants1 will clobber the value you planted with your Unsafe call.

And ... yes ... this JVM behavior is fine as far as the JLS and the JVMS are concerned. The real problem is that what you are doing is unspecified, and the nasal demons rule applies to it.

Better approaches would be:

  • Redesign your code so that it doesn't depend on a static final field that takes different values in different circumstances.
  • Or declare the static final as private and always access it via a static method ... that you can mock using Mockito or PowerMock or similar in your unit tests.
  • Or make your Unsafe injection code trigger the class initialization before messing with the field; e.g. call Class.forName("className"); first.

Is there any other way to do modification with less executions and memory consumption?

In my opinion, that is not the right question to ask:

  • If you are doing this in unit (etc) tests, performance and memory usage are irrelevant.
  • If you are doing this in production code, you are playing with fire. Find a better way to do ... whatever it is you are trying to do ... that doesn't entail modifying static final fields. There will be ways.

1 - This is implementation dependent. The runtime handling of compile-time constants / constant expressions differs with different Java versions.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
2

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.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
-3

It's not recommended to change 'static final' variables. If you have a need of changing variables - generally people need them to keep track of some parameters in JMH benchmark in multi-threaded executions, I suggest you take a look at atomic variables. links: https://www.geeksforgeeks.org/atomic-variables-in-java-with-examples/ https://www.baeldung.com/java-atomic-variables

Sougata Ghosh
  • 30
  • 1
  • 5