Ответ 1
Обновление: кажется, что мой первоначальный ответ был неправильным, и OnStackReplacement просто выявил проблему в этом конкретном случае, но исходная ошибка была в коде анализа escape. Анализ Escape - это подсистема компилятора, которая определяет, будет ли объект уклоняться от данного метода или нет. Неэкранированные объекты могут быть сканированы (вместо распределения по куче) или полностью оптимизированы. В нашем тестовом анализе побег имеет значение, поскольку несколько созданных объектов, несомненно, не избегают метода.
Я загрузил и установил сборник раннего доступа JDK 9 и заметил, что ошибка там исчезла. Однако в сборке раннего доступа JDK 9 он все еще существует. changelog между b82 и b83 показывает только одно исправление ошибки (исправьте меня, если я ошибаюсь): JDK-8134031 "Неправильная компиляция JIT сложного кода с анализом вставки и эвакуации". Обозначенный testcase несколько похож: большая петля, несколько ячеек (похожие на одноэлементные массивы в нашем тесте), которые приводят к внезапному изменению значение внутри поля, поэтому результат становится бесшумным (без сбоев, исключений, просто неправильного значения). Как и в нашем случае, сообщалось, что проблема не появляется до 8u40. введено исправление очень короткое: просто однострочное изменение в исходном файле анализа.
В соответствии с OpenJDK отладчиком ошибок исправление уже backported для ветки JDK 8u72, которая запланировано, который будет выпущен в январе 2016 года. Похоже, что было слишком поздно, чтобы закрепить это исправление на предстоящем 8u66.
Рекомендуемая работа - отключить анализ эвакуации (-XX: -DoEscapeAnalysis) или отключить устранение оптимизации распределения (-XX: -EliminateAllocations). Таким образом, @apangin был действительно ближе к ответу, чем мне.
Ниже приведен оригинальный ответ
Во-первых, я не могу воспроизвести проблему с JDK 8u25, но могу на JDK 8u40 и 8u60: иногда он работает правильно (застрял в бесконечном цикле), иногда он выводит и выдает. Поэтому, если JDK понизится до 8u25, то вы можете это сделать. Обратите внимание, что если вам нужны более поздние исправления в javac (многие вещи, особенно связанные с lambdas, были зафиксированы в 1.8u40), вы можете скомпилировать их с более новым javac, но работать с более старыми JVM.
Для меня кажется, что эта конкретная проблема, вероятно, является ошибкой в OnStackReplacement механизмом (когда OSR происходит на уровне 4). Если вы не знакомы с OSR, вы можете прочитать этот ответ. OSR, безусловно, происходит в вашем случае, но немного странно. Здесь -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+TraceNMethodInstalls
для неудачного прогона (%
означает, что OSR JIT, @ 28
означает позицию байт-кода OSR, (3)
и (4)
означает уровень уровня):
...
91 37 % 3 Test::<init> @ 28 (194 bytes)
Installing osr method (3) Test.<init>()V @ 28
93 38 3 Test::<init> (194 bytes)
Installing method (3) Test.<init>()V
94 39 % 4 Test::<init> @ 16 (194 bytes)
Installing osr method (4) Test.<init>()V @ 16
102 40 % 4 Test::<init> @ 28 (194 bytes)
103 39 % 4 Test::<init> @ -2 (194 bytes) made not entrant
...
Installing osr method (4) Test.<init>()V @ 28
113 37 % 3 Test::<init> @ -2 (194 bytes) made not entrant
claims array[0] != 1.0....array[0] = 1.0
claims array[0] now == 1.0...array[0] = 1.0
Таким образом, OSR на уровне 4 возникает для двух разных смещений байт-кода: смещение 16 (которое является точкой входа цикла while
) и смещением 28 (которое является вложенной точкой входа петли for
). Похоже, что какое-то условие гонки происходит во время переноса контекста между обеими версиями метода, скомпонованными OSR, что приводит к нарушению контекста. Когда выполнение передается методу OSR, он должен передать текущий контекст, включая значения локальных переменных, таких как array
и r
, в метод OSR'ed. Что-то плохое происходит здесь: возможно, на короткое время <init>@16
работает версия OSR, затем она заменяется на <init>@28
, но контекст обновляется с небольшой задержкой. Вероятно, что перенос контекста OSR препятствует оптимизации "исключить распределения" (как отметил @apangin, отключение этой оптимизации помогает в вашем случае). Мой опыт недостаточен, чтобы копать дальше здесь, возможно, @apangin может комментировать.
В отличие от обычного запуска создается и устанавливается только одна копия метода OSR 4-го уровня:
...
Installing method (3) Test.<init>()V
88 43 % 4 Test::<init> @ 28 (194 bytes)
Installing osr method (4) Test.<init>()V @ 28
100 40 % 3 Test::<init> @ -2 (194 bytes) made not entrant
4592 44 3 java.lang.StringBuilder::append (8 bytes)
...
Похоже, что в этом случае нет гонки между двумя версиями OSR, и все работает отлично.
Проблема также исчезает, если вы перемещаете тело внешнего контура на отдельный метод:
import java.util.Random;
public class Test2 {
private static void doTest(double[] array, Random r) {
double someArray[] = new double[1];
double someArray2[] = new double[2];
for (int i = 0; i < someArray2.length; i++) {
someArray2[i] = r.nextDouble();
}
... // rest of your code
}
Test2() {
double array[] = new double[1];
Random r = new Random();
while (true) {
doTest(array, r);
}
}
public static void main(String[] args) {
new Test2();
}
}
Также ручное разворачивание вложенного цикла for
устраняет ошибку:
int i=0;
someArray2[i++] = r.nextDouble();
someArray2[i++] = r.nextDouble();
Чтобы попасть в эту ошибку, кажется, что у вас должно быть как минимум два вложенных цикла в одном и том же методе, поэтому OSR может возникать в разных положениях байт-кода. Таким образом, для проблемы с работой в вашем конкретном фрагменте кода вы можете просто сделать то же самое: извлечь тело цикла в отдельный метод.
Альтернативное решение - полностью отключить OSR с помощью -XX:-UseOnStackReplacement
. Это редко помогает в производственном коде. Счетчики циклов все еще работают, и если ваш метод с циклом с множеством итераций вызывается как минимум дважды, второй запуск будет скомпилирован JIT. Также даже если ваш метод с длинным циклом не скомпилирован JIT из-за отключенной OSR, любые методы, которые он вызывает, все равно будут скомпилированы JIT.