不是的.JVM跟踪"类初始化已开始",并且永远不会再次启动它.这是why整个InstanceHolder
首歌曲和舞蹈 routine 的首要工作:您使用的是JVM的并发门控机制,它应用于类加载,否则其中的none将是完全有用的.
因此,您是absolutely guaranteed类A.InstanceHolder
的静态初始化式序列中的new A()
最多运行一次ever.
这就提出了一个显而易见的问题:好吧,那么,当A.InstanceHolder
的静态初始化过程已经开始,但还没有完成时,如果一个上下文访问A.InstanceHolder
类,会发生什么?假设JVM保证不会重新运行初始化器,那么答案不可能是‘stackoverflow’,但是does会发生什么呢?
这可以分为两种不同的情况:
需要A.InstanceHolder
才能初始化的代码正在不同的线程中运行.也就是说,another thread处于初始化过程中间(仍在经历实现new A()
所需字节码序列,这是A.InstanceHolder
的静态初始化码).在这种情况下,该线程将简单地阻塞.它几乎在各个方面都像synchronized
.
需要A.InstanceHolder
才能初始化的代码已在此线程中运行.换句话说,我们需要A.InstanceHolder
来初始化in the middle of字节码序列.正在进行我们现在所依赖的初始化!在本例中,JVM规范是明确的:在初始化时,所有字段都被初始化为null
/0
/false
,然后进行初始化.在这些字段可用之前,任何从它们读取的try 都只返回它们拥有的值(null
/0
/false
),even if they are final.这确实导致了有点奇怪的情况,private static final String x = someMethodThatMakesX();
仍然可以让x
持有两个不同的值(null
和实际值),即使它是最终的.我们可以简单地见证这一点:
> cat ExampleOfClassInitWeirdness.java
public class ExampleOfClassInitWeirdness {
private static final String y = hi();
private static final String x = test();
private static final String z = bye();
public static String hi() {
System.out.println(x); // compiles. Prints 'null'
return null;
}
public static String test() {
System.out.println(x); // compiles. Prints 'null'
return "hello";
}
public static String bye() {
System.out.println(x); // compiles. Prints '"hello"'.
return null;
}
public static void main(String[] args) {}
}
> java ExampleOfClassInitWeirdness.java
null
null
hello
这三个打印来自不同的地方,存在完全相同的Java代码:System.out.println(x);
,假设x
是final
,它如何打印不同的值呢?
是的,它可以--JVM规范是明确的,即使结果有点奇怪.
同样的原理解释了为什么不会出现堆栈溢出错误.顺序如下.我假设您在Java代码中的某个地方写了:A.getInstance()
.然后:
- ClassLoader判断是否加载了
A
,以及其初始化状态是什么.
- ClassLoader得出结论认为它没有加载,因此init状态为
NEEDED
.
- ClassLoader从JAR文件或其他文件中找到字节码,并使用它创建类.
- ClassLoader现在开始初始化过程.它自动执行此操作:判断状态是否仍为
NEEDED
.因此,它将更新为INITIALIZING
,并跟踪该线程正在执行初始化的事实.初始化微不足道,甚至根本不存在;没有static{}
个块,也没有初始化静态字段的表达式.我将跳过解释B
是如何初始化的;它同样令人厌烦.S的状态变成了INITIALIZED
岁.
- 请记住,我们正在运行操作码
INVOKESTATIC com.foo.pkg.A getInstance()Lcom/foo/pkg/A;
,作为其中的一部分,我们必须确保在继续之前加载和初始化A.那么,让我们继续.A.getInstance()
现在可以运行了.这运行操作码GETSTATIC com.foo.pkg.A$InstanceHolder a Lcom/foo/pkg/A;
,因此,这需要加载和初始化A$InstanceHolder
.不是的,所以...转到JAR文件,我们找到字节码并加载它,然后是初始化它的时间;它的状态为INITIALIZING
,该线程作为初始化器.所有静态字段均初始化为null
/0
/false
.
- 有事情要做--运行代码
new A()
.
- 这需要运行代码
B.getInstance()
(这是另一个INVOKESTATIC
操作码).
- 这导致需要加载和初始化
B
.正如所讨论的,无聊,这种情况时有发生.
- 跑
B.getInstance()
英里.这导致GETSTATIC
在B$InstanceHolder
上,这意味着需要加载和初始化B$InstanceHolder
.不是的.
- 开始初始化过程.运行静态初始值设定项,这里是
new B()
.
new B()
除以A.getInstance()
得INVOKESTATIC
- 这需要加载和初始化
A
个.它是.
- 我们跑了
A.getInstance()
英里.这是来自A$InstanceHolder
的GETSTATIC
s,它需要加载和初始化A$InstanceHolder
.它不是;不完全是-它是上了膛的,是的.但其状态为INITIALIZING
,未初始化.我们在这里是堆栈的几层,但这都是由初始化A$InstanceHolder
引起的事件链.
- 注册为执行初始化的线程就是这个线程,所以我们不会阻塞,我们只是继续,不做任何修改.我们的行为就好像事情已经初始化了一样.所以,我们只是...执行
GETSTATIC
操作码.它尽职尽责地返回当前值.也就是null
.
- 现在已经完成了
B.getInstance()
个.此实例的a
字段现在为null
.
- 现在已经完成了
new B()
个.这样,A$InstanceHolder
的静态初始化序列现在就完成了.它生成的A
实例有一个类型为B
的字段;字段保存的B
实例有一个保存A
实例的字段.那个场地可容纳null
人.
A.getInstance()
结束并返回that.getB().getA()
为null
的实例.
要理解的一件事是,final
是mostly,javac
是虚构的.JVM大多不关心最终结果,并将其视为一种注释.原因是:
public class Example {
private static final Instant NOW = Instant.now();
public static void breakThings() {
NOW = null;
}
}
不编译是因为javac拒绝编译它.在javac中的某个地方有一个if
语句,它说:嗯,第NOW
号是最终版本,您可能会try 多次写入它,所以,不是.
但只需从Java代码中删除If,它就会编译并运行.
与泛型不同,从JVM的Angular 来看,泛型实际上是entirely个注释(它不会影响JVM所做的事情,无论如何,关心泛型的是entirely个javac,这解释了为什么您不能在运行时从实例中读取它们),final
只是mostly个注释.某些并发规则和保证以及某些热点优化的工作方式有所不同.