静态数据如何初始化?

3
有很多关于“何时”进行静态类初始化的好答案,比如在这个帖子中 - 何时进行静态类初始化? 现在我的问题是“如何”。以下是Stephen C的回答摘录:

类的静态初始化通常发生在以下事件之一发生之前:

  • 创建类的实例,
  • 调用类的静态方法,
  • 分配类的静态字段,
  • 使用非常量静态字段或
  • 对于顶层类,在类中词法嵌套的assert语句被执行。

那么它是如何在内部完成的呢? 可以将可能触发初始化的每条指令都包装在if中吗? 对于任何可行的实现的详细信息都可以。

我正在使用“Java”标记该问题,但如果我没有弄错,C#和Swift也会按需初始化静态数据-总体上。


2
这取决于具体的实现。我知道Hotspot使用一些技巧,例如引发段错误来懒惰地检测某些条件。 - chrylis -cautiouslyoptimistic-
知道“如何”这一点的问题在于它随时可能会改变,因此您实际上不能依赖它。最重要的细节是初始化是线程安全的,除此之外,您冒着对实现进行假设而不总是正确的风险。 - Peter Lawrey
1
段错误(Segfaults)或总线错误(bus errors)已经使用了很长时间。它们基本上是为了支持虚拟内存而开发的。每当操作系统加载一个虚拟内存页面时,它首先会生成一个段错误。如果虚拟内存被设置为将未使用的类指向未使用的内存,那么就会生成一个段错误。 - markspace
1
如果你真的感兴趣,OpenJDK是开源的。去看一下吧。 - markspace
@chrylis 不是很准确。我已经使用segfaults实现了页面错误。据我所知,这是标准的实现方式。在我使用过的任何硬件上,页面错误和其他错误之间没有区别,只有操作系统处理错误的方式不同。 - markspace
显示剩余4条评论
2个回答

2
如评论中所提到的,使用segfaults可以完成这种事情,但是在Java中并不需要这样做。
请记住,Java字节码不会直接由机器执行——在它被JIT编译成真正的机器指令之前,它会被解释和分析以确定何时将其编译,这已经涉及对每个字节码指令执行大量的机器指令。在此期间检查静态初始化的所有条件也没有问题。
字节码还可以编译为带有检查的机器代码,这些代码在首次执行检查后会被重写或打补丁。这种情况也会发生在许多其他情况下,例如自动内联和逃逸分析,因此像这样进行静态初始化检查并不是什么大问题。
简而言之,有很多方法,但关键点是当您运行Java程序时,除了您实际编写的代码外,还有很多其他内容。

1

对于静态常量(final)字段,.class文件直接定义了常量值,因此JVM可以在加载类时分配它。

对于非常量静态字段,编译器将合并任何初始化程序与自定义静态初始化程序块以生成一段代码的单个静态初始化程序块,在加载类时JVM可以执行该代码块。

示例:

public final class Test {
    public static double x = Math.random();
    static {
        x *= 2;
    }
    public static final double y = myInit();
    public static final double z = 3.14;
    private static double myInit() {
        return Math.random();
    }
}

字段z是一个常量,而xy是运行时的值,并将与静态初始化块(x *= 2)合并。

如果使用javap -c -p -constants Test.class反汇编字节码,您将获得以下结果。我已添加空白行以分隔静态初始化块(static {})的合并部分。

Compiled from "Test.java"
public final class test.Test {
  public static double x;

  public static final double y;

  public static final double z = 3.14d;

  static {};
    Code:
       0: invokestatic  #15                 // Method java/lang/Math.random:()D
       3: putstatic     #21                 // Field x:D

       6: getstatic     #21                 // Field x:D
       9: ldc2_w        #23                 // double 2.0d
      12: dmul
      13: putstatic     #21                 // Field x:D

      16: invokestatic  #25                 // Method myInit:()D
      19: putstatic     #28                 // Field y:D

      22: return

  public test.Test();
    Code:
       0: aload_0
       1: invokespecial #33                 // Method java/lang/Object."<init>":()V
       4: return

  private static double myInit();
    Code:
       0: invokestatic  #15                 // Method java/lang/Math.random:()D
       3: dreturn
}

请注意,这也表明编译器创建了一个默认构造函数,并且该构造函数调用了超类(Object)的默认构造函数。

更新

如果您在javap命令后添加-v(详细)参数,您将看到常量池,其中存储了上面列出的定义这些引用的值,例如对于上面列出的Math.random()调用,即#15,相关的常量为:

#15 = Methodref          #16.#18        // java/lang/Math.random:()D
#16 = Class              #17            // java/lang/Math
#17 = Utf8               java/lang/Math
#18 = NameAndType        #19:#20        // random:()D
#19 = Utf8               random
#20 = Utf8               ()D

正如您所看到的,Math类有一个类常量(#16),该常量被定义为字符串"java/lang/Math"
当第一次使用引用#16时(即执行invokestatic #15时),JVM将其解析为一个实际的类。如果该类已经被加载,它将直接使用该加载的类。
如果尚未加载该类,则会调用loadClass()来加载该类的ClassLoader,该方法将字节码作为参数传递给defineClass()方法。在此加载过程中,通过自动分配常量值并执行先前确定的静态初始化器代码块来初始化该类。
正是由JVM执行的这个类引用解析过程触发了静态字段的初始化。这基本上就是发生的情况,但是这个过程的确切机制是JVM实现特定的,例如通过JIT(即时编译为机器代码)。

非常感谢,但是如果我理解正确的话,您写的是关于被调用方而不是调用方的,因为在Java代码(上面)中没有静态数据的引用(除了静态初始化程序)。然而,您刚刚向我展示了如何检查调用方... :-) 谢谢您提供的这个想法! - greenoldman
@ greenoldman,我不是在谈论调用方/被调用方,而是展示生成的类文件如何包含静态字段的初始化代码,因此JVM可以在加载类时简单地执行该代码。 JVM不需要复杂的逻辑来进行初始化,因为该逻辑由类文件提供(具有常量的特殊处理)。 这就是“如何”初始化静态字段的答案,这也是您问题的关键所在。 - Andreas
也许我没有表达清楚,Java 在静态数据初始化方面是按需工作的(简而言之),所以我想知道每个 SomeClass.myStaticField 引用(调用者)是否在内部包装了 if(SomeClass.alreadyInitialized...) - greenoldman
更新,正如@Matt所写的那样,JVM使用相当高级的命令,我使用了您的代码来检查引用--引用静态数据被转换为“getstatic”。所以一个问题引出另一个问题:-)。 - greenoldman
1
@greenoldman 答案已更新,以解释字节码中的静态引用如何触发初始化逻辑。 - Andreas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接