Работая над ByteWatcher (см. Мой последний пост ), я наткнулся на что-то довольно странное.
Это фактический фрагмент кода, который используется для определения объема выделенных ресурсов в определенном потоке:
1
2
3
4
5
6
|
return ( long ) mBeanServer.invoke( name, GET_THREAD_ALLOCATED_BYTES, PARAMS, SIGNATURE ); |
- Для полного контекста смотрите здесь .
(Способ работы ByteWatcher состоит в том, чтобы периодически вызывать этот метод для отслеживания распределения.)
Следует отметить один важный момент, особенно при желании предоставить точное число для выделения программы, заключается в том, что вызов самого кода выше — вызывает распределение.
Выделение, вызванное этим вызовом, должно быть вычтено из возвращенного числа, чтобы мы изолировали выделение, вызванное программой, то есть вызов meanBeanServer = выделение потока программы + накладные расходы вызова
То, что я заметил, было то, что этот объем выделения обычно составлял 336 байтов. Однако когда я вызвал этот метод в цикле, я обнаружил кое-что интересное. Время от времени это будет выделять различную сумму.
Для этого теста:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
@Test public void testQuietMeasuringThreadAllocatedBytes() { ByteWatcherSingleThread am = new ByteWatcherSingleThread(); System.out.println( "MeasuringCostInBytes = " + am.getMeasuringCostInBytes()); long [] marks = new long [1_000_000]; for ( int i = 0 ; i < 1_000_000; i++) { marks[i] = am.threadAllocatedBytes(); } long prevDiff = - 1 ; for ( int i = 1 ; i < 1_000_000; i++) { long diff = marks[i] - marks[i - 1 ]; if (prevDiff != diff) System.out.println( "Allocation changed at iteration " + i + "->" + diff); prevDiff = diff; } } |
Это был типичный результат:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
MeasuringCostInBytes = 336 Allocation changed at iteration 1->336 Allocation changed at iteration 12->28184 Allocation changed at iteration 13->360 Allocation changed at iteration 14->336 Allocation changed at iteration 1686->600 Allocation changed at iteration 1687->336 Allocation changed at iteration 2765->672 Allocation changed at iteration 2766->336 Allocation changed at iteration 5458->496 Allocation changed at iteration 5459->336 Allocation changed at iteration 6213->656 Allocation changed at iteration 6214->336 Allocation changed at iteration 6535->432 Allocation changed at iteration 6536->336 Allocation changed at iteration 6557->8536 Allocation changed at iteration 6558->336 Allocation changed at iteration 7628->576 Allocation changed at iteration 7629->336 Allocation changed at iteration 8656->4432 Allocation changed at iteration 8657->336 Allocation changed at iteration 9698->968 Allocation changed at iteration 9699->336 Allocation changed at iteration 11881->1592 Allocation changed at iteration 11882->336 Allocation changed at iteration 12796->1552 Allocation changed at iteration 12797->336 Allocation changed at iteration 13382->456 Allocation changed at iteration 13383->336 Allocation changed at iteration 14844->608 Allocation changed at iteration 14845->336 Allocation changed at iteration 36685->304 Allocation changed at iteration 52522->336 Allocation changed at iteration 101440->400 Allocation changed at iteration 101441->336 |
Учитывая, что в программе определенно не было никакого распределения, для меня было загадкой, почему один и тот же вызов иногда выделял разные суммы.
Таким образом, более 1 000 000 прогонов программа выделяла разные суммы примерно в 25 раз. Примечательно, что после итераций по 100 тысяч пиков не было.
Я поделился этой проблемой с Хайнцем Кабуцем и Крисом Ньюлендом. Крис заметил, что это связано с джиттером компиляции JIT. Это очень хорошо видно, если повторно запустить тест с флагом -Xint (запускается только в интерпретированном режиме, т.е. без компиляции JIT). Теперь было только 2 шипа.
1
2
3
4
5
|
MeasuringCostInBytes = 336 Allocation changed at iteration 1->336 Allocation changed at iteration 12->28184 Allocation changed at iteration 13->360 Allocation changed at iteration 14->336 |
Аналогично работает с флагом -Xcomp (только компиляция):
1
2
3
4
5
|
MeasuringCostInBytes = 336 Allocation changed at iteration 1->336 Allocation changed at iteration 12->29696 Allocation changed at iteration 13->360 Allocation changed at iteration 14->336 |
Так что теперь мы можем быть достаточно уверены, что именно джиттер компиляции JIT вызывает мошенническое распределение.
Я не совсем понимаю, почему это так, но, думаю, это понятно. Чтобы компенсировать это, я ввел фазу калибровки в конструкторе ByteWatcher, которая была доработана Хайнцем.
Вы можете увидеть код калибровки здесь, но он состоит из нескольких этапов:
- Вызовите метод, чтобы выяснить, сколько потока выделено в узком цикле (мы называем его 100 000 раз) — позволяет JIT правильно подогревать код, чтобы он был скомпилирован
- Подождите 50 миллисекунд — это дает JVM шанс завершить джиттер компиляции
С этим кодом в конструкторе, даже если он работает без специальных флагов, пиковых значений выделения нет.
Вывод
- JIT компиляция вызывает дрожание
- Запуск программы без дрожания компиляции (т. Е. Только с интерпретацией или только компиляцией) значительно уменьшает это распределение, но не полностью устраняет его.
- После запуска 100 тыс. Разрядов выделение останавливается, что указывает на то, что для остановки дрожания требуется 100 тыс. Пробежек. Это интересно, потому что мы знаем, что код должен компилироваться после 10 000 итераций.
Ссылка: | Неожиданное распределение — JIT-компиляция Jitter от нашего партнера по JCG Дэниела Шая из блога Rational Java . |