El siguiente código,
public class TestFastThrow {
public static void main(String[] args) {
int count = 0;
int exceptionStackTraceSize = 0;
Exception exception = null;
do {
try {
throwsNPE(1);
}
catch (Exception e) {
exception = e;
if (exception.getStackTrace().length != 0) {
exceptionStackTraceSize = exception.getStackTrace().length;
count++;
}
}
}
while (exception.getStackTrace().length != 0);
System.out.println("Iterations to fastThrow :" + count + ", StackTraceSize :" + exceptionStackTraceSize);
}
static void throwsNPE(int callStackLength) {
throwsNPE(callStackLength, 0);
}
static void throwsNPE(int callStackLength, int count) {
if (count == callStackLength) {
((Object) null).getClass();
}
else {
throwsNPE(callStackLength, count + 1);
}
}
}
da el siguiente resultado después de ejecutar varias veces,
Iterations to fastThrow :5517, StackTraceSize :4
Iterations to fastThrow :2825, StackTraceSize :5
Iterations to fastThrow :471033, StackTraceSize :6
Iterations to fastThrow :1731, StackTraceSize :7
Iterations to fastThrow :157094, StackTraceSize :10
.
.
.
Iterations to fastThrow :64587, StackTraceSize :20
Iterations to fastThrow :578, StackTraceSize :29
detalles de la MV
Java HotSpot(TM) 64-Bit Server VM (11.0.5+10-LTS) for bsd-amd64 JRE (11.0.5+10-LTS)
-XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly
Lo que es sorprendente es la razón por qué el JIT se necesita mucho más iteraciones para Optimizar si el seguimiento de la pila es de longitud impar?
He activado JIT Registros y analizado a través de jitwatch, pero no pude ver nada útil, al igual que la línea de tiempo de compilación cuando el C1 y C2 que parece estar pasando después por las stacktraces tamaño aún.
Línea de tiempo es algo como esto, (mirando cuando java.lang.Throwable.getStackTrace()
se compila)
| StackSize | 10 | 11 |
|---------------|-------|-------|
| Queued for C1 | 1.099 | 1.012 |
| C1 | 1.318 | 1.162 |
| Queued for C2 | 1.446 | 1.192 |
| C2 | 1.495 | 1.325 |
¿Por qué es esto ocurra? Y lo que hace el uso heurística JIT para tiro rápido?
Este efecto es el resultado de difícil compilación niveles y la política de inclusión entre líneas .
Voy a explicar en el ejemplo simplificado:
public class TestFastThrow {
public static void main(String[] args) {
for (int iteration = 0; ; iteration++) {
try {
throwsNPE(2);
} catch (Exception e) {
if (e.getStackTrace().length == 0) {
System.out.println("Iterations to fastThrow: " + iteration);
break;
}
}
}
}
static void throwsNPE(int depth) {
if (depth <= 1) {
((Object) null).getClass();
}
throwsNPE(depth - 1);
}
}
Por simplicidad, voy a excluir a todos los métodos de compilación, excepto throwsNPE
.
-XX:CompileCommand=compileonly,TestFastThrow::throwsNPE -XX:+PrintCompilation
HotSpot utiliza gradas de la compilación por defecto. Aquí
throwsNPE
se compila primero en el Nivel 3 (C1 con perfiles). Perfilado en C1 hace posible volver a compilar el método más tarde por C2.OmitStackTraceInFastThrow
optimización funciona sólo en C2 código compilado. Por lo tanto, cuanto antes se compila el código de C2 - los menos iteraciones pasarán antes de que termine el bucle.Cómo perfiles en las obras código compilado-C1: se incrementa el contador en cada invocación de métodos y en cada rama hacia atrás (sin embargo, no hay ramas hacia atrás en
throwsNPE
el método). Cuando el contador alcanza cierto umbral configurable, la política de recopilación JVM decide si las necesidades método actual para volver a compilar.throwsNPE
es un método recursivo. HotSpot puede inline llamadas recursivas hasta-XX:MaxRecursiveInlineLevel
(valor por defecto es 1).La frecuencia con qué frecuencia las llamadas C1 código compilado atrás a la política de recopilación de JVM, es diferente para las invocaciones regulares frente a invocaciones inline. A notifica método regular JVM cada 2 10 invocaciones (
-XX:Tier3InvokeNotifyFreqLog=10
), mientras que los notifica inline método JVM mucho más raramente: cada 2 20 invocaciones (-XX:Tier23InlineeNotifyFreqLog=20
).Para el número par de llamadas recursivas, todas las invocaciones siguen
Tier23InlineeNotifyFreqLog
parámetro. Cuando el número de llamadas es impar, procesos en línea no funciona para la última llamada de sobra, y esta última invocación siguienteTier3InvokeNotifyFreqLog
parámetro.Este medio, cuando la profundidad llamada es par,
throwsNPE
se volverá a compilar sólo después de 2 20 llamadas, es decir, después de 2 19 iteraciones del bucle. Eso es exactamente lo que usted verá cuando se ejecuta el código anterior conthrowNPE(2)
:Iterations to fastThrow: 524536
524 536 está muy cerca de 2 19 = 524288
Ahora, si ejecuta la misma aplicación con
-XX:Tier23InlineeNotifyFreqLog=15
el número de iteraciones será cercano a 2 14 = 16.384.Iterations to fastThrow: 16612
Ahora vamos a cambiar el código para llamar
throwsNPE(1)
. El programa terminará muy rápidamente, sin tener en cuentaTier23InlineeNotifyFreqLog
el valor. Esto se debe a la diferente opción gobierna ahora. Pero si yo vuelva a ejecutar el programa con-XX:Tier3InvokeNotifyFreqLog=20
el bucle finalizará no antes de después de 2 20 iteraciones:Iterations to fastThrow: 1048994
Resumen
Optimización de tiro rápido se aplica sólo al código compilado-C2. Debido a un nivel de inclusión entre líneas ( -XX:MaxRecursiveInlineLevel
), compilación C2 se activa antes (después de 2 Tier3InvokeNotifyFreqLog invocaciones, si el número de llamadas recursivas es impar), o más tarde (después de 2 Tier23InlineeNotifyFreqLog invocaciones, si todas las llamadas recursivas están cubiertos por inline).