Это приняло удивительный оборот.
Первое, что можно сказать (относительно точно), это то, что эффект вызван JIT. Я объединил фрагменты кода в этот MCVE:
public class CounterJitTest
{
private static class Counter
{
private int count;
public void increase()
{
count++;
}
public void decrease()
{
count--;
}
public int getCount()
{
return count;
}
}
private static class Person extends Thread
{
private Counter c;
public Person(Counter c)
{
this.c = c;
}
@Override
public void run()
{
for (int i = 0; i < 1000000; i++)
{
c.increase();
c.decrease();
}
}
}
public static void main(String[] args) throws InterruptedException
{
for (int i = 0; i < 10; i++)
{
Counter c = new Counter();
Person p1 = new Person(c);
Person p2 = new Person(c);
p1.start();
p2.start();
p1.join();
p2.join();
System.out.println("run " + i + ": " + c.getCount());
}
}
}
Запуск с
java CounterJitTest
вызывает вывод, который был упомянут в вопросе:
run 0: 6703
run 1: 178
run 2: 1716
run 3: 0
run 4: 0
run 5: 0
run 6: 0
run 7: 0
run 8: 0
run 9: 0
Отключение JIT с помощью -Xint
(интерпретированный режим), то есть запуск с
java -Xint CounterJitTest
приводит к следующим результатам:
run 0: 38735
run 1: 53174
run 2: 86770
run 3: 27244
run 4: 61885
run 5: 1746
run 6: 32458
run 7: 52864
run 8: 75978
run 9: 22824
Чтобы глубже погрузиться в то, что JIT на самом деле делает , я начал все это с дизассемблера VM HotSpot, чтобы взглянуть на сгенерированную сборку. Однако время выполнения было настолько быстрым, что я подумал: ну, я просто увеличу счетчик в for
-loop:
for (int i = 0; i < 1000000; i++)
Но даже увеличение его до 100000000
привело к немедленному завершению программы. Это уже вызвало подозрение. После генерации разборки с
java -server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly -XX:+PrintInlining CounterJitTest
Я посмотрел на скомпилированные версии методов increase
и decrease
, но не нашел ничего очевидного. Тем не менее, метод run
, казалось, был виновником здесь. Изначально сборка метода run
содержала ожидаемый код (здесь размещены только наиболее важные части):
Decoding compiled method 0x0000000002b32fd0:
Code:
[Entry Point]
[Constants]
# {method} {0x00000000246d0f00} 'run' '()V' in 'CounterJitTest$Person'
...
[Verified Entry Point]
...
0x0000000002b33198: je 0x0000000002b33338 ;*iconst_0
; - CounterJitTest$Person::run@0 (line 35)
0x0000000002b3319e: mov $0x0,%esi
0x0000000002b331a3: jmpq 0x0000000002b332bc ;*iload_1
; - CounterJitTest$Person::run@2 (line 35)
0x0000000002b331a8: mov 0x178(%rdx),%edi ; implicit exception: dispatches to 0x0000000002b3334f
0x0000000002b331ae: shl $0x3,%rdi ;*getfield c
; - CounterJitTest$Person::run@9 (line 37)
0x0000000002b331b2: cmp (%rdi),%rax ;*invokevirtual increase
; - CounterJitTest$Person::run@12 (line 37)
; implicit exception: dispatches to 0x0000000002b33354
...
0x0000000002b33207: je 0x0000000002b33359
0x0000000002b3320d: mov 0xc(%rdi),%ebx ;*getfield count
; - CounterJitTest$Counter::increase@2 (line 9)
; - CounterJitTest$Person::run@12 (line 37)
0x0000000002b33210: inc %ebx
0x0000000002b33212: mov %ebx,0xc(%rdi) ;*putfield count
; - CounterJitTest$Counter::increase@7 (line 9)
; - CounterJitTest$Person::run@12 (line 37)
...
0x0000000002b3326f: mov %ebx,0xc(%rdi) ;*putfield count
; - CounterJitTest$Counter::decrease@7 (line 14)
; - CounterJitTest$Person::run@19 (line 38)
...
Я не совсем "понимаю" это, по общему признанию, но можно видеть, что он выполняет getfield c
и некоторые вызовы (частично встроенных?) increase
и decrease
методов.
Тем не менее, final скомпилированная версия метода run
такова:
Decoding compiled method 0x0000000002b34590:
Code:
[Entry Point]
[Constants]
# {method} {0x00000000246d0f00} 'run' '()V' in 'CounterJitTest$Person'
# [sp+0x20] (sp of caller)
0x0000000002b346c0: mov 0x8(%rdx),%r10d
0x0000000002b346c4:
<writer thread='2060'/>
[Loaded java.lang.Shutdown from C:\Program Files\Java\jre1.8.0_131\lib\rt.jar]
<writer thread='5944'/>
shl $0x3,%r10
0x0000000002b346c8: cmp %r10,%rax
0x0000000002b346cb: jne 0x0000000002a65f60 ; {runtime_call}
0x0000000002b346d1: data32 xchg %ax,%ax
0x0000000002b346d4: nopw 0x0(%rax,%rax,1)
0x0000000002b346da: nopw 0x0(%rax,%rax,1)
[Verified Entry Point]
0x0000000002b346e0: mov %eax,-0x6000(%rsp)
0x0000000002b346e7: push %rbp
0x0000000002b346e8: sub $0x10,%rsp ;*synchronization entry
; - CounterJitTest$Person::run@-1 (line 35)
0x0000000002b346ec: cmp 0x178(%rdx),%r12d
0x0000000002b346f3: je 0x0000000002b34701
0x0000000002b346f5: add $0x10,%rsp
0x0000000002b346f9: pop %rbp
0x0000000002b346fa: test %eax,-0x1a24700(%rip) # 0x0000000001110000
; {poll_return}
0x0000000002b34700: retq
0x0000000002b34701: mov %rdx,%rbp
0x0000000002b34704: mov $0xffffff86,%edx
0x0000000002b34709: xchg %ax,%ax
0x0000000002b3470b: callq 0x0000000002a657a0 ; OopMap{rbp=Oop off=80}
;*aload_0
; - CounterJitTest$Person::run@8 (line 37)
; {runtime_call}
0x0000000002b34710: int3 ;*aload_0
; - CounterJitTest$Person::run@8 (line 37)
0x0000000002b34711: hlt
0x0000000002b34712: hlt
0x0000000002b34713: hlt
0x0000000002b34714: hlt
0x0000000002b34715: hlt
0x0000000002b34716: hlt
0x0000000002b34717: hlt
0x0000000002b34718: hlt
0x0000000002b34719: hlt
0x0000000002b3471a: hlt
0x0000000002b3471b: hlt
0x0000000002b3471c: hlt
0x0000000002b3471d: hlt
0x0000000002b3471e: hlt
0x0000000002b3471f: hlt
[Exception Handler]
[Stub Code]
0x0000000002b34720: jmpq 0x0000000002a8c9e0 ; {no_reloc}
[Deopt Handler Code]
0x0000000002b34725: callq 0x0000000002b3472a
0x0000000002b3472a: subq $0x5,(%rsp)
0x0000000002b3472f: jmpq 0x0000000002a67200 ; {runtime_call}
0x0000000002b34734: hlt
0x0000000002b34735: hlt
0x0000000002b34736: hlt
0x0000000002b34737: hlt
Это полная сборка метода! И это ... ну, в принципе ничего.
Чтобы подтвердить мое подозрение, я явно отключил встраивание метода increase
, начиная с
java -XX:CompileCommand=dontinline,CounterJitTest$Counter.increase CounterJitTest
И результат снова оказался ожидаемым:
run 0: 3497
run 1: -71826
run 2: -22080
run 3: -20893
run 4: -17
run 5: -87781
run 6: -11
run 7: -380
run 8: -43354
run 9: -29719
Итак, мой вывод:
JIT содержит методы increase
и decrease
. Они только увеличивают и уменьшают одно и то же значение. И после встраивания JIT достаточно умен, чтобы понять, что последовательность вызовов
c.increase();
c.decrease();
по сути является no-op , и, следовательно, просто делает именно это: ничего.