2015-10-07 3 views
24

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

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

Я бегу 64-разрядной ОС Windows 7 и я в состоянии воспроизвести эту проблему, как внутри Eclipse, и при компиляции из командной строки, с JDKs 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(); 
    } 
} 
+0

Вы проверили наличие плохой памяти? – wero

+3

Я не могу воспроизвести это. Работает так, как ожидалось. – marstran

+2

'double' по своей сути не точен. Вы уверены, что это не ваша проблема? –

ответ

21

Update: кажется, что мой первоначальный ответ был неверным и OnStackReplacement только выявил проблему в данном конкретном случае, но оригинальная ошибка была в коде анализа побега. Анализ Escape - это подсистема компилятора, которая определяет, будет ли объект уклоняться от данного метода или нет. Неэкранированные объекты могут быть сканированы (вместо распределения по куче) или полностью оптимизированы. В нашем тестовом анализе побег имеет значение, поскольку несколько созданных объектов, несомненно, не избегают метода.

Я скачал и установил JDK 9 early access build 83 и заметил, что ошибка там исчезла. Однако в сборке раннего доступа JDK 9 он все еще существует. changelog между b82 и b83 показывает только одно релевантное исправление ошибки (исправьте меня, если я ошибаюсь): JDK-8134031 «Неправильная компиляция JIT сложного кода с анализом inlining и escape». Зафиксированный testcase несколько похож: большая петля, несколько ящиков (похожие на одноэлементные массивы в нашем тесте), которые приводят к внезапному изменению значения внутри поля, поэтому результат становится бесшумным (без сбоев, без исключения, просто неправильное значение).Как и в нашем случае, сообщалось, что проблема не появляется до 8u40. introduced fix очень короткий: всего лишь однострочное изменение в исходном аналитическом источнике.

По OpenJDK отслеживания ошибок, исправление уже backported в JDK 8u72 отрасли, которая scheduled будет выпущен в январе 2016 года Кажется, что это было слишком поздно, чтобы портировать исправления к предстоящей 8u66.

Предлагаемый способ обхода - отключить анализ эвакуации (-XX: -DoEscapeAnalysis) или отключить устранение оптимизации распределения (-XX: -EliminateAllocations). Таким образом, @apangin was actually closer ответом, чем я.

Ниже оригинальный ответ


Во-первых, я не могу воспроизвести проблему с JDK 8u25, но может на JDK 8u40 и 8u60: иногда он работает правильно (застрял в бесконечном цикле), иногда он выводит и выходы. Поэтому, если JDK понизится до 8u25, то вы можете это сделать. Обратите внимание, что если вам нужны более поздние исправления в javac (многие вещи, особенно связанные с lambdas, были зафиксированы в 1.8u40), вы можете скомпилировать его с более новым javac, но работать на более старой JVM.

Для меня кажется, что эта конкретная проблема, вероятно, является ошибкой в ​​механизме OnStackReplacement (когда OSR встречается на уровне 4). Если вы не знакомы с OSR, вы можете прочитать this answer. OSR, безусловно, происходит в вашем случае, но немного странно. Вот -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+TraceNMethodInstalls для неудавшегося запуска (% означает ЛАРН JIT, @ 28 означает ЛАРН положение байткодом, (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, но контекст обновляется с небольшой задержкой. Вполне вероятно, что перенос контекста ЛАР препятствует оптимизации «исключить распределения» (как отметил @apangin, отключение этой оптимизации помогает в вашем случае). Мой опыт недостаточен, чтобы копать дальше здесь, возможно, @apangin может комментировать.

В отличие от обычной в перспективе только один экземпляр уровня 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) 
... 

Так, кажется, что в этом случае ни одна раса между двумя ЛРН версий не происходит, и все работает отлично.

Проблема исчезает при перемещении тела внешнего контура в отдельный метод:

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.

+0

Отличная работа. Пожалуйста, включите это в отчет об ошибке, поскольку это может помочь разработчикам JDK решить проблему. Я бы дал +2, если бы мог ... :-) – Axel

+0

Да, замена на стеке помогает, если у вас есть большие длинные методы, код которых соответствует производительности, который является шаблоном, который не соответствует типичному прикладному коду, но типичный искусственный контрольный код. – Holger

+0

Ничего себе, отличная работа! Я уже представил отчет об ошибке, и он все еще рассматривается. Предполагая, что я могу добавить к нему информацию, когда/если она будет принята, я обязательно включу это. Еще раз спасибо! – bcothren

0

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

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

+1

Azul имеет 2 JVMs Zulu и Zing. Из ссылки, которую вы предоставляете (которая разбита), кажется, что вы ссылаетесь на Зулу, а не на Цзин. Zulu - это сборка OpenJDK, которая полностью содержит код OpenJDK, но проверена и поддерживается. Он должен демонстрировать то же поведение для сопоставимой версии. Цин - совсем другой зверь. –