为什么必须在使用Unsafe修改另一个类的静态final字段之前创建或调用Java类?

hmmo2u0o  于 2023-03-14  发布在  Java
关注(0)|答案(3)|浏览(128)

我尝试应用sun.misc.Unsafe来修改某个类的public static final字段,以进行单元测试和jmh基准测试。至少用JDK 11到JDK 17进行测试,似乎只有在创建了类对象或调用了该类的某个静态函数作为某个触发器之后,它才能正常工作。我的完整代码在底部。

问题

*为什么在修改前需要?
*是否有其他方法可以在执行次数和内存消耗较少的情况下进行修改?

更多详细信息

有些方法只能在 SOME 平台或JDK版本中使用,例如:

Object o = Another.class;

// ...

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

// ...

我在sololearn playground中使用过版本“16-ea”,但没有在我的OpenJDK 15.0.2+7(macOS 13.1上的Zulu15.29.15)上使用过。
另一种方法在两个平台上都有效:

{
    Object a = new Another();
}

// ...

Another.hi();

// ...

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

参考

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

完整示例

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";
}
baubqpgj

baubqpgj1#

你不应该编写依赖于static final字段修改能力的代码(甚至测试代码)。它从来没有被支持过,而且用于实现这一点的各种方案也不能保证有效。事实上,过去使用的一些方案 * 不再 * 适用于最新的Java版本。
对于您的观察,* 可能 * 的解释是,如果您在类初始化被触发之前使用Unsafe修改static final,则实际的类初始化(例如<cinit>伪方法中的代码)或 * 编译时 * 常量1的惰性初始化将 * 清除 * 您在Unsafe调用中植入的值。
而且......是的......就JLS和JVMS而言,这种JVM行为是好的,但真实的的问题是 * 你 * 正在做的事情是未指定的,并且nasal demons rule适用于它。
更好的办法是:

  • 重新设计代码,使其不依赖于在不同情况下采用不同值的static final字段。
  • 或者将static final声明为private,并始终通过static方法访问它......您可以在单元测试中使用Mockito或PowerMock或类似方法模拟该方法。
  • 或者让您的Unsafe注入代码在扰乱字段之前触发类初始化;例如首先调用Class.forName("className");

有没有其他方法可以减少执行次数和内存消耗?
在我看来,这不是一个正确的问题:

  • 如果您是在单元测试(等等)中执行此操作,则性能和内存使用是无关紧要的。
  • 如果你在生产代码中这样做,你就是在玩火。找到一个更好的方法来做...无论你想做什么...都不需要修改static final字段。

1 -这取决于实现。编译时常量/常量表达式的运行时处理随Java版本的不同而不同。

qnakjoqk

qnakjoqk2#

在我解释这是怎么回事之前,有一点需要记住:
这种黑客行为是不受支持的。我的意思是从字面上看:JDK规范煞费苦心地对此不做任何承诺,因此也不做任何JDK更改(可能是因为不同的供应商提供了它,也可能是因为它是为另一个平台提供的,也可能是新版本)可能会改变它的工作方式,或者完全破坏它。因此,任何你创建的试图修改静态finals的代码,即使“它每次在我访问的所有硬件和平台上都能正常工作”,仍然是一个烫手山芋:但这并不意味着它可以在其他任何地方工作,几乎所有您必须列入日程的JDK版本都可以:检查一下这些东西是否还能用。因为它可能不起作用,如果它不再起作用,你可以整天在www.example.com上提交bug报告openjdk.net,他们会忽略它们。因为这不是bug:说明书上可没保证能成功。
换句话说,我的建议是:找到另一种方法来做这件事。你选择的路线,即使你解决了你现在用它遇到的问题,也是不稳定的,需要不断的警惕。
不要介意这样一个事实,即使用unSafe本身就是OpenJDK每天都在说的事情:总有一天我们会把它消灭的。

为什么会这样

关键在于javap。Javap把类文件转换成字节码转储,它让你确切地看到javac做了什么,这通常会带来有用的见解。让我们先编译这段代码,然后javap它;我们必须使用-c-v来分别显示所有的字节码和尽可能多的细节。有时,也可以使用-p(显示私有/包私有的内容)。

> 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)

这表明某些字段可以被编译成“常量”--其中字段 * 直接 * 知道那个值(ConstantValue: String Original行)。
此变量在其他代码中的用法:

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

这表明Test2代码直接在中加载"Original",它实际上并没有引用Test.SF *(实际上,Test 2是100%独立于Test的,现在可以完全删除Test.class;类不需要它)。
显然,对于编译时常量的情况,您可以整天更改原始的(Test.SF)字段,实际上没有代码使用它,相反,在编译时javac注意到它是常量,并将它们全部内联。

好吧,但是,我用的是Object

是的,我们现在可以知道这是如何改变事情的:

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

注意,现在突然出现了一个静态块,这个块加载字符串常量并将其赋给一个静态字段(这就是putstatic字节码的作用)。
静态字段 * 根本 * 不再有与之关联的ConstantValue:
static {}是代码,它必须被执行。当JVM启动时,它不会初始化整个类路径上的每一个类。如果它初始化了,JVM启动将花费非常长的时间。相反,JVM根据需要初始化类:每当第一次需要某个类时,JVM就从它的类加载器缓存中获取它,但是如果该高速缓存中没有这样的类,就加载它:磁盘访问(或任何需要获取类文件中数据的操作)碰巧会加载它。类加载有一个2步模型:

  • 首先,将其加载到中,如中所示,解析.class定义的内容。
  • 第二,“初始化”它,这需要运行任何和所有的static{}初始化器。注意,例如,java代码中的static final long FOO = System.currentTimeMillis();只是一个“奇怪”的方式来拥有一个静态初始化器-每当你在声明一个表达式时将它赋给一个静态字段,除非它是一个编译时常量,它导致静态初始化程序块。static { ... }只是使其显式的一种方式。

在你的例子中,你使用theUnsafe来修改这两个步骤之间的静态字段**。所以,你首先修改它,然后你导致init步骤发生,它运行静态初始化器,覆盖你刚刚侵入的内容。一旦它被初始化,JVM就不会再次初始化它。因此,一旦初始化发生(new Other()将执行此操作),就不会再遇到此问题。
这个问题的一个简单解决方案是强制您自己进行初始化(Class.forName("name.of.TheClass")是一种方法),然后才通过theUnsafe来处理它。

rkue9o1l

rkue9o1l3#

不建议修改'static final'变量,如果你有修改变量的需求--通常人们需要它们来跟踪JMH基准测试中多线程执行的一些参数,我建议你看看原子变量。链接:https://www.geeksforgeeks.org/atomic-variables-in-java-with-examples/https://www.baeldung.com/java-atomic-variables

相关问题