Подтвердить что ты не робот

Проблема с выбором времени/памяти Java 8

Я столкнулся с довольно странной проблемой, которую я могу создать при запуске Java 8. Проблема возникает, как будто какая-то ошибка синхронизации происходит в самой JVM. Он носит прерывистый характер, но легко воспроизводится (по крайней мере, в пределах моей тестовой среды). Проблема в том, что заданное значение массива уничтожается и заменяется на 0.0 при определенных обстоятельствах. В частности, в приведенном ниже коде array[0] оценивается до 0,0 после строки new Double(r.nextDouble());. Затем, если вы снова просмотрите содержимое array[0] снова, оно теперь показывает, что значение будет правильным значением 1.0. Пример вывода из этого тестового примера:

claims array[0] != 1.0....array[0] = 1.0
claims array[0] now == 1.0...array[0] = 1.0`

Я запускаю 64-разрядную Windows 7 и могу воспроизвести эту проблему как внутри Eclipse, так и при компиляции из командной строки с JDK 1.8_45, 1.8_51 и 1.8_60. Я не могу создать проблему с запуском версии 1.7_51. Те же результаты были продемонстрированы и в другом 64-битном окне Windows 7.

Эта проблема появилась в большом нетривиальном программном обеспечении, но мне удалось сконденсировать ее до нескольких строк кода. Ниже приведен небольшой тестовый пример, демонстрирующий проблему. Это довольно странно выглядящий тестовый пример, но, похоже, все это необходимо для возникновения ошибки. Использование Random не требуется - я могу заменить все r.nextDouble() на любое двойное значение и продемонстрировать проблему. Интересно, что если someArray[0] = .45; заменено на someArray[0] = r.nextDouble();, я не смог бы воспроизвести проблему (хотя нет ничего особенного в .45). Отладка Eclipse также не поможет - она ​​меняет время настолько, что этого больше не происходит. Даже корректное выражение System.err.println() приведет к тому, что проблема больше не появится.

Опять же, проблема прерывистая, поэтому, чтобы воспроизвести проблему, нужно несколько раз запустить этот тестовый пример. Я думаю, что больше всего мне приходилось запускать его примерно в 10 раз, прежде чем я получу результат, показанный выше. В Eclipse я даю ему секунду или два после запуска, а затем убиваю его, если этого не произошло. Из командной строки то же самое - запустите его, если не произойдет CTRL+C, чтобы выйти и повторить попытку. Похоже, что если это произойдет, это произойдет довольно быстро.

В прошлом я сталкивался с такими проблемами, но все они были проблемы с потоками. Я не могу понять, что здесь происходит - я даже посмотрел на байт-код (который был между 1.7_51 и 1.8_45, между прочим).

Любые идеи о том, что здесь происходит?

import java.util.Random;

public class Test { 
    Test(){
        double array[] = new double[1];     
        Random r = new Random();

        while(true){
            double someArray[] = new double[1];         
            double someArray2 [] = new double [2];

            for(int i = 0; i < someArray2.length; i++) {
                someArray2[i] = r.nextDouble();
            }

            // for whatever reason, using r.nextDouble() here doesn't seem
            // to show the problem, but the # you use doesn't seem to matter either...

            someArray[0] = .45;

            array[0] = 1.0;

            // commented out lines also demonstrate problem
            new Double(r.nextDouble());
            // new Float(r.nextDouble();
            // double d = new Double(.1) * new Double(.3);
            // double d = new Double(.1) / new Double(.3);
            // double d = new Double(.1) + new Double(.3);
            // double d = new Double(.1) - new Double(.3);

            if(array[0] != 1.0){
                System.err.println("claims array[0] != 1.0....array[0] = " + array[0]);

                if(array[0] != 1.0){
                    System.err.println("claims array[0] still != 1.0...array[0] = " + array[0]);
                }else {
                    System.err.println("claims array[0] now == 1.0...array[0] = " + array[0]);
                }

                System.exit(0);
            }else if(r.nextBoolean()){
                array = new double[1];
            }
        }
    }

    public static void main(String[] args) {
        new Test();
    }
}
4b9b3361

Ответ 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.

Ответ 2

Я могу воспроизвести эту ошибку в Zulu (сертифицированная сборка OpenJDK) с кодом, опубликованным в http://www.javaspecialists.eu/archive/Issue234.html

С помощью виртуальной машины oracle я могу воспроизвести эту ошибку только после того, как я запустил код в Zulu. Похоже, Zulu загрязняет общий кэш доступа. Решение в этом случае - запустить код с -XX: -EnableSharedLookupCache.