Java 静态数据是如何初始化的?

Java 静态数据是如何初始化的?,java,static,Java,Static,“什么时候”有很多很好的答案,就像在这个帖子里——现在我的问题是“如何”。以下是作者对答案的引用 类的静态初始化通常在 第一次发生下列事件之一时: 创建该类的一个实例 调用该类的静态方法 指定了类的静态字段 使用非恒定静态场,或 对于顶级类,执行词汇嵌套在类中的assert语句 那么它是如何在内部完成的呢?每个可能触发初始化的指令都用if包装?任何工作的详细信息:-)我都可以接受 我用“Java”来标记这个问题,但如果我没有弄错的话,C#和Swift也会根据需要初始化静态数据。对于静态常量(

“什么时候”有很多很好的答案,就像在这个帖子里——现在我的问题是“如何”。以下是作者对答案的引用

类的静态初始化通常在 第一次发生下列事件之一时:

  • 创建该类的一个实例
  • 调用该类的静态方法
  • 指定了类的静态字段
  • 使用非恒定静态场,或
  • 对于顶级类,执行词汇嵌套在类中的assert语句
那么它是如何在内部完成的呢?每个可能触发初始化的指令都用
if
包装?任何工作的详细信息:-)我都可以接受

我用“Java”来标记这个问题,但如果我没有弄错的话,C#和Swift也会根据需要初始化静态数据。

对于静态常量(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
是一个常量,而
x
y
是运行时值,将与静态初始值设定项块合并(
x*=2

如果您使用
javap-c-p-constants Test.class
反汇编字节码,您将得到以下结果。我添加了空行来分隔静态初始值设定项块的合并部分(
static{}

如您所见,
Math
类有一个类常量(#16),它被定义为字符串
“java/lang/Math”

第一次使用引用#16时(在执行
invokestatic#15
时发生),JVM将把它解析为实际的类。如果该类已经加载,它将只使用加载的类

如果尚未加载该类,则调用以加载该类(),该类反过来调用
defineClass()
方法,将字节码作为参数。在此加载过程中,通过自动分配常量值并执行先前标识的静态初始值设定项代码块,初始化该类


正是JVM执行的这个类引用解析过程触发了静态字段的初始化。基本上就是这样,但是这个过程的确切机制是特定于JVM实现的,例如通过JIT(对机器代码的即时编译)。

如注释中所述,这种事情可以通过segfaults来完成,但对于Java来说,这并不是真正必要的

请记住,Java字节码不是由机器直接执行的——在它被JIT编译成真正的机器指令之前,它会被解释和分析以确定何时编译它,这已经涉及到为每个字节码指令执行大量的机器指令。在此期间检查静态初始化的所有条件没有问题

字节码也可以编译成带有检查的机器码,在第一次执行检查后重写或修补。这种事情的发生还有很多其他原因,比如自动内联和转义分析,所以像这样做静态初始化检查并没有什么大问题


简而言之,有很多方法,但关键点是,当您运行Java程序时,除了您实际编写的代码之外,还有很多方法在进行。

这取决于实现。我知道Hotspot使用了一些技巧,比如引发SEGFULTS,以懒散地检测某些情况。知道“如何”的问题是,它可能随时发生变化,因此您实际上无法依赖它。最重要的细节是,初始化是线程安全的,除此之外,您可能会对可能并不总是正确的实现进行假设。@PeterLawrey,我对它是如何完成的很感兴趣,仅此而已。Segfaults——分段错误或总线错误——已经使用了很长时间。它们基本上是为了支持虚拟内存而开发的。每次操作系统加载一页虚拟内存时,它都会首先生成一个SEGFULT。如果虚拟机被设置为将未使用的类指向未使用的内存,那么这将生成一个segfault。去看一看。非常感谢,但是如果我读对了,您写的是被调用方,而不是调用方,因为在Java代码(上面)中没有对静态数据的引用(静态初始值设定项除外)。但你刚才告诉我如何检查来电端…:-)谢谢你的主意@greenoldman我没有写关于被调用方/调用方的内容,而是展示了生成的类文件如何包含静态字段的初始化代码,以便JVM可以在加载类时简单地执行该代码。JVM不需要复杂的逻辑进行初始化,因为该逻辑由类文件提供(带有常量的特殊处理)。这是“如何”初始化静态字段的答案,这就是你的问题所在。也许我没有说清楚,Java在静态数据初始化时(简而言之)是按需工作的,所以我想知道是否每个
SomeClass.myStaticField
reference(调用者)都在内部用
if包装(SomeClass.alreadyInitialized…
.Update,由于@Matt编写的JVM使用了相当高的命令,我使用您的代码检查引用——引用静态数据被转换为
getstatic
。因此一个问题导致另一个问题:-)更新了@greenoldman Answer,以解决字节码中的静态引用如何触发初始值设定项逻辑。
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
}
#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