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) {
}
}
Это похоже на классический тупик инициализации, но JSM HotSpot не зависает.Вместо этого он печатает:
Called from main
Called from Thread-2
Initialization complete
Почему это ошибка
JVMS §6.5 требует, чтобы при выполнении invokestatic
байт-код
класс или интерфейс, который объявил разрешенный метод, инициализируется, если этот класс или интерфейс еще не был инициализирован
Когда Thread-2
вызывает staticTarget
, основной класс StaticInit
явно неинициализирован (так как его статический инициализатор все еще работает).Это означает, что Thread-2
должен запустить процедуру инициализации класса, описанную в JVMS §5.5 .Согласно этой процедуре
Если объект Class для C указывает, что инициализация для C выполняется другим потоком, то отпустите LC и заблокируйте текущий поток, пока не будет сообщено, что текущая инициализация завершена
Однако Thread-2
не блокируется, несмотря на то, что класс находится в процессе инициализации потоком main
.
Как насчет других JVM
Я тестировал OpenJ9 и JET, и они оба ожидаемотупик в приведенном выше тесте.
Интересно, что HotSpot также зависает в режиме -Xcomp
, но не в -Xint
или смешанных режимах.
Как это происходит
Когда переводчик впервые встречаетсяinvokestatic
байт-код, он вызывает среду выполнения JVM для разрешения ссылки на метод.В рамках этого процесса JVM инициализирует класс, если это необходимо.После успешного разрешения разрешенный метод сохраняется в записи Constant Pool Cache.Кэш констант пула - это специфическая для HotSpot структура, в которой хранятся разрешенные постоянные значения пула.
В приведенном выше тесте байт-код invokestatic
, который вызывает staticTarget
, сначала разрешается потоком main
.Среда выполнения интерпретатора пропускает инициализацию класса, потому что класс уже инициализируется тем же потоком.Разрешенный метод сохраняется в кеше постоянного пула.В следующий раз, когда Thread-2
выполнит тот же invokestatic
, интерпретатор увидит, что байт-код уже разрешен, и использует постоянную запись в кэше пула без вызова среды выполнения и, таким образом, пропускает инициализацию класса.
Аналогичная ошибка для getstatic
/ putstatic
была исправлена давно - JDK-4493560 , но это исправление не коснулось invokestatic
.Я отправил новую ошибку JDK-8215634 для решения этой проблемы.
Что касается исходного примера,
, зависает он или нет, зависит от того, какой поток разрешает первымстатический звонок.Если это поток main
, программа завершается без тупика.Если статический вызов разрешен одним из потоков ForkJoinPool
, программа зависает.
Обновление
Ошибка подтверждена .Это исправлено в следующих выпусках: JDK 8u201, JDK 11.0.2 и JDK 12.