Este artículo es un anexo del artículo principal Java Dark Memory, para aportar detalle sobre este espacio concreto de memoria. En el artículo principal nos centramos en la observabilidad de espacios de memoria menos conocidos y más problemáticos.
Índice
1. ¿Qué es?
Espacio utilizado para la compilación e interpretación de código.
Si en tiempo de compilación, el compiler de la JDK se encarga de transformar código fuente en bytecode (.class), en runtime tenemos JIT (Just In Time compiler):
- JIT se encarga de ejecutar código java (bytecode .class), de forma interpretada y en ocasiones se vuelve a compilar a código máquina
- Estas tareas se hacen en Threads separados a los de aplicación, sin interferir o bloquear la ejecución
- Un método (o parte de él) que haya sido compilado a nativo, sustituye al código original en un proceso llamado on-stack replacement (OSR)
- Además, JIT realiza decenas de "mejoras" en el código que interpreta y que es más ejecutado en el proceso, este el corazón del sistema Hot-Spot de OpenJDK, pudiendo tranformar código recursivo en secuencial por ejemplo, o disminuyendo la complejidad ciclomática en general.
Para almacenar este código interpretado, mejorado, compilado a nativo y sustituido, internamente JIT maneja diferentes subespacios dentro del code space:
- non-method: non-method code, compiler buffers y bytecode interpreters
- profiled: métodos ligeramente perfilados y optimizados, con tiempo corto de vida
- non-profiled: métodos completamente optimizados con tiempo de vida largo
JIT se divide en dos compiladores C1 y C2
- C1 realiza optimizaciones en tres niveles de profundidad, dejando los resultados en su layer
- C2 compila a código nativo, dejando el resultado en layer 4
2. Observabilidad
- Con NMT podemos ver la cantidad de memoria virtual y residente/comiteada de todo el espacio.
- Con JMX y Mbeans.
- Con métricas Prometheus: por ejemplo, Spring Boot ofrece la siguiente métrica:
jvm_memory_<commited|used|max>_bytes {area=“nonheap”, id=“CodeHeap ‘profiled methods’|’non profiled methods’|’non-methods’”}
3. Límites
- Valor por defecto (depende de la JVM): 240MB
- Valor start:
-XX:InitialCodeCacheSize
- Valor max:
-XX:ReservedCodeCacheSize
Si acotamos demasiado el espacio, podemos llegar a ver este mensaje de advertencia de la la JVM en runtime:
VM warning: CodeCache is full. The compiler has been disabled
Si vemos este mensaje, es una señal de que necesitamos más espacio. Pero NO para la ejecución de la aplicación, simplemente JIT deja de realizar optimizaciones y compilación nativa, pasando a ser un intérprete de bytecode.
4. Consideraciones
- No debemos despreciar el espacio utilizado por JIT.
- Podemos aproximar un espacio entre un 10-30% de la off-heap.
JIT levanta threads concurrentes para realizar las compilaciones C1 y C2, y si no los limitamos crea "pools" de threads cuyo tamaño varía en función del número de cores de CPU visibles por el proceso:
Limitar el número de threads concurrentes, tiene un impacto directo en la memoria del proceso, veamos un ejemplo.
¿En que se consume la memoria de microservicio spring-boot (uno sencillo, de tipo web con un endpoint REST, en la fase de arranque?
Activando el profiler de jemalloc, este el diagrama que obtenemos:
Analizando el diagrama, vemos un nodo relacionado con el compiler C2 de JIT, que se lleva el 43% del alojamiento de memoria:
Es decir, que del total de los 320MB aproximadamente de memoria residente de la secuencia de arranque, 150MB son de JIT compiler!
Utilizando el flag -XX:CICompilerCount=2
, obtenemos una reducción de unos 120MB en este proceso de arranque lo cual no es nada despreciable.
Así que limita la CPU de los contenedores como buena práctica, siempre. Y opcionalmente, usa el flag -XX:CICompilerCount
para limitar aún más los threads de JIT. Evidentemente limitar los threads hará que JIT trabaje más lentamente, pero dado que queremos que nuestros procesos java tengan una larga vida en producción, sin fallos, al final las optimizaciones y la compilación nativa se realizarán igualmente cuando nuestro servicio vaya recicbiendo carga.
Gran artículo, buceando en la complejidad de la gestión de la memoria Java de manera sencilla.