TL; DR
这是HotSpot错误JDK-8215634
可以通过一个根本没有种族的简单测试用例来重现该问题:
public class StaticInit { static void staticTarget() { System.out.println("Called from " + Thread.currentThread().getName()); } static { Runnable r = new Runnable() { public void run() { staticTarget(); } }; r.run(); Thread thread2 = new Thread(r, "Thread-2"); thread2.start(); try { thread2.join(); } catch (Exception ignore) {} System.out.println("Initialization complete"); } public static void main(String[] args) { }}这看起来像经典的初始化死锁,但是HotSpot JVM不会挂起。而是打印:
Called from mainCalled from Thread-2Initialization complete
为什么这是一个错误
JVMS§6.5要求在执行
invokestatic字节码时
如果尚未初始化声明已解析方法的类或接口,则该类或接口尚未初始化
当
Thread-2通话
staticTarget,主类
StaticInit显然是未初始化(因为它的静态初始化仍在运行)。这意味着
Thread-2必须启动JVMS§5.5中描述的类初始化过程。按照这个程序
- 如果C的Class对象指示其他线程正在对C进行初始化,则释放LC并阻塞当前线程,直到得知正在进行的初始化已完成
但是,
Thread-2尽管该类正在通过thread进行初始化,但不会被阻塞
main。
那其他JVM呢
我测试了OpenJ9和JET,预期它们在上述测试中都陷入僵局。
有趣的是,HotSpot也可以挂在
-Xcomp模式下,但不能挂在
-Xint或混合模式下。
怎么发生的
解释器首次遇到
invokestatic字节码时,它将调用JVM运行时来解析方法引用。作为此过程的一部分,JVM会在必要时初始化该类。成功解决后,解决的方法将保存在“常量池缓存”条目中。常量池缓存是特定于HotSpot的结构,用于存储解析的常量池值。
在上面的测试
invokestatic字节码中,调用
staticTarget首先由
main线程解析。解释器运行时将跳过类的初始化,因为该类已经被同一线程初始化。解决的方法保存在常量池缓存中。下次
Thread-2执行相同的操作时
invokestatic,解释器会看到字节码已被解析,并使用常量池高速缓存条目而不调用运行时,从而跳过了类初始化。
类似的bug对
getstatic/
putstatic不久前固定-
JDK-4493560,但修复没有触及
invokestatic。我已提交了新的错误JDK-8215634以解决此问题。
至于原来的例子
是否挂起取决于哪个线程首先解析了静态调用。如果它是
main线程,则程序将完成而不会出现死锁。如果通过
ForkJoinPool线程之一解决了静态调用,程序将挂起。
更新资料
该错误已确认。在即将发布的版本中已修复该问题:JDK 8u201,JDK 11.0.2和JDK 12。



