Как получить Java-пакеты, когда JVM не может достичь безопасного места

Недавно у нас была ситуация, когда одно из наших JVM-продуктов произвольно замерзало. Процесс Java сжигал CPU, но вся видимая активность прекращалась: никакого выхода журнала, ничего не записывалось в журнал GC, никакого ответа на какой-либо сетевой запрос и т.д. Процесс будет сохраняться в этом состоянии до перезапуска.

Оказалось, что класс org.mozilla.javascript.DToA при вызове на определенных входах запутается и вызовет BigInteger.pow с огромными значениями (например, 5 ^ 2147483647), что вызывает зависание JVM. Я предполагаю, что некоторый большой цикл, возможно, в java.math.BigInteger.multiplyToLen, был JIT'ed без проверки безопасности в цикле. В следующий раз, когда JVM необходимо приостановить сборку мусора, она замерзнет, ​​потому что поток, выполняющий код BigInteger, не достигнет безопасного места в течение очень долгого времени.

Мой вопрос: в будущем, как я могу диагностировать проблему с безопасностью, подобную этой? kill -3 не производил никакого выхода; Я полагаю, он полагается на safepoints для создания точных стеков. Есть ли какой-либо производственный безопасный инструмент, который может извлекать стеки из работающей JVM без ожидания safepoint? (В этом случае мне повезло и удалось захватить набор трассировок стека сразу после вызова BigInteger.pow, но до того, как он проделал свой путь до достаточно большого входа, чтобы полностью вклинить JVM. Без этого удачи, я "Не знаю, как бы мы когда-либо диагностировали проблему.)

Изменить: следующий код иллюстрирует проблему.

// Spawn a background thread to compute an enormous number.
new Thread(){ @Override public void run() {
  try {
    Thread.sleep(5000);
  } catch (InterruptedException ex) {
  }
  BigInteger.valueOf(5).pow(100000000);
}}.start();

// Loop, allocating memory and periodically logging progress, so illustrate GC pause times.
byte[] b;
for (int outer = 0; ; outer++) {
  long startMs = System.currentTimeMillis();
  for (int inner = 0; inner < 100000; inner++) {
    b = new byte[1000];
  }

  System.out.println("Iteration " + outer + " took " + (System.currentTimeMillis() - startMs) + " ms");
}

Это запускает фоновый поток, который ждет 5 секунд, а затем запускает огромное вычисление BigInteger. На переднем плане он затем многократно выделяет серию из 100 000 блоков 1K, регистрируя прошедшее время для каждой серии 100 МБ. В течение 5 секунд каждая серия из 100 Мбайт работает примерно на 20 миллисекунд на моем MacBook Pro. Как только начнутся вычисления BigInteger, мы начнем чередоваться с длинными паузами. В одном тесте паузы были последовательно 175 мс, 997 мс, 2927 мс, 4222 мс и 22617 мс (после чего я прервал тест). Это согласуется с BigInteger.pow(), вызывающим серию все более крупных операций умножения, каждый из которых занимает больше времени, чтобы достичь безопасного места.

Ответ 1

Твоя проблема меня очень заинтересовала. Вы были правы насчет JIT. Сначала я пытался играть с типами GC, но это не имело никакого эффекта. Затем я попытался отключить JIT, и все сработало нормально:

java -Djava.compiler=NONE Tests

Затем распечатайте компиляции JIT:

java -XX:+PrintCompilation Tests

И заметил, что проблема начинается после некоторых компиляций в классе BigInteger, я пытался исключить методы один за другим из компиляции и, наконец, нашел причину:

java -XX:CompileCommand=exclude,java/math/BigInteger,multiplyToLen -XX:+PrintCompilation Tests

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

Ответ 2

Возможно, что опция "-F" jstack подходит для:

OPTIONS
  -F
     Force a stack dump when 'jstack [-l] pid' does not respond.

Я всегда задавался вопросом, почему это может помочь.