Cómo Monitorizar la Memoria No Heap en Java

Introducción

Al desarrollar aplicaciones en Java, uno de los problemas más comunes que enfrentamos es la gestión del consumo de memoria. Es crucial para el rendimiento y la estabilidad de las aplicaciones. Generalmente, los problemas relacionados con la memoria en Java se dividen en dos categorías: los que se basan en el uso de memoria heap y los que se refieren al uso de memoria no heap. En este artículo, revisaremos diferentes herramientas que podemos utilizar para monitorizar la utilización de memoria no heap.

1. Overview

Utilizando una aplicación Java, a veces nos encontramos con problemas de consumo de memoria. En general, podemos categorizar todos los problemas en aquellos basados en el uso de memoria heap y aquellos basados en el uso de memoria no heap. Este artículo se centra en cómo monitorizar la memoria no heap, que es esencial para entender y optimizar el rendimiento de nuestras aplicaciones.

2. jcmd

jcmd es una herramienta de JDK que podemos usar para enviar comandos de diagnóstico y solución de problemas a un proceso Java en ejecución. La utilizamos para inspeccionar propiedades del sistema, activar la recolección de basura, volcar información de heap e información de subprocesos, y gestionar la compilación JIT.

Para monitorizar la memoria no heap utilizando jcmd, necesitamos habilitar native memory tracking (NMT). Luego, podemos llamar a jcmd para obtener la información del proceso de memoria:

jcmd <pid> VM.native_memory

En la respuesta, podemos analizar todas las secciones no heap que necesitamos:

-                        GC (reserved=359872KB, committed=70440KB)
                            (malloc=17792KB #582)
                            (mmap: reserved=342080KB, committed=52648KB)

-                  Compiler (reserved=168KB, committed=168KB)
                            (malloc=4KB #29)
                            (arena=165KB #5)

-                  Internal (reserved=221KB, committed=221KB)
                            (malloc=157KB #1202)
                            (mmap: reserved=64KB, committed=64KB)

-                     Other (reserved=4KB, committed=4KB)
                            (malloc=4KB #2)

-                    Symbol (reserved=1217KB, committed=1217KB)
                            (malloc=857KB #3546)
                            (arena=360KB #1)

-    Native Memory Tracking (reserved=141KB, committed=141KB)
                            (malloc=5KB #69)
                            (tracking overhead=136KB)

En la salida, podemos ver el consumo de memoria heap, así como el consumo de todos los tipos de memoria no heap. La propiedad committed indica el consumo en el momento de la llamada a jcmd.

3. jconsole

jconsole es una herramienta de monitorización que cumple con la especificación JMX. Podemos usar jconsole para monitorizar y gestionar aplicaciones Java en tiempo real cuando solo tenemos acceso al JDK.

Jconsole proporciona una interfaz gráfica para rastrear el uso de memoria, actividad de hilos, consumo de CPU y MBeans. La herramienta se conecta a JVM locales o remotas, ayudándonos a diagnosticar problemas de rendimiento y optimizar el comportamiento de la aplicación.

Para ejecutar jconsole, ejecutamos el siguiente comando desde la carpeta bin del JDK:

<JDK_PATH>\bin\jconsole

En la primera pantalla, seleccionamos el proceso local o remoto al que queremos conectarnos. En la pestaña de Memoria, seleccionamos el gráfico de Uso de Memoria No Heap. Después de eso, podremos ver el gráfico del uso no heap.

4. VisualVM

VisualVM es otra herramienta visual que proporciona capacidades de monitorización de JVM. Usamos VisualVM para monitorizar la carga de CPU, el uso de memoria, la actividad de hilos y los volcaduras de heap. VisualVM también soporta el análisis de volcaduras de hilos y la creación de perfiles de la ejecución de métodos para identificar cuellos de botella en el rendimiento.

4.1. Monitorizar MetaSpace en Basic Monitor

En la configuración básica de VisualVM, podemos monitorizar el tamaño de metaspace utilizando la pestaña Monitor.

4.2. Monitorizar Memoria No Heap Usando el Plugin de MBeans

Al añadir el plugin de MBeans, veremos la nueva pestaña MBeans. En esta pestaña, podemos ver el uso actual de memoria no heap en general y para secciones de memoria específicas.

4.3. Monitorizar Buffers Directos Usando el Plugin de Pool de Buffers

Además, podemos usar el plugin Buffer Monitor VisualVM. Tras su instalación, veremos la nueva pestaña Buffer Pools, donde podemos observar el uso actual de memoria de buffers directos y archivos mapeados en memoria.

5. Java Mission Control

Java Mission Control es otra herramienta visual que proporciona capacidades de monitorización y diagnóstico de JVM. Utilizamos JMC para analizar el rendimiento de la aplicación, monitorizar el uso de memoria, inspeccionar la actividad de hilos y revisar el comportamiento de la recolección de basura.

5.1. Uso de Memoria No Heap Usando el Navegador de MBean

Conectémonos a nuestra aplicación utilizando el MBean Server. En la pestaña MBeans, podemos encontrar la utilización total de memoria no heap. Sin embargo, debemos ser conscientes de que este número no incluye el consumo de memoria de buffer directo.

5.2. Seguimiento de Memoria Nativa Usando Registros JFR

Desde Java 20, también podemos registrar los datos de seguimiento de memoria nativa de manera continua con Java Flight Recorder (JFR). Utilizando el siguiente comando, podemos preparar un archivo de grabación de vuelo con los eventos necesarios registrados:

java -XX:NativeMemoryTracking=detail -XX:StartFlightRecording=name=Profiling,filename=nmt-recording.jfr,settings=profile  -jar path/ourapp.jar

Luego, podemos abrir este archivo usando Java Mission Control:

En la pestaña Event Browser de la grabación, podemos encontrar el evento Total Native Memory Usage. Si lo seleccionamos, el lado derecho de la pantalla mostrará la cantidad total de memoria comprometida. Este número incluye tanto el consumo de memoria heap como no heap.

Adicionalmente, podemos elegir el evento uso de memoria nativa por tipo. En este caso, veremos un desglose del consumo para todos los tipos de memoria no heap.

6. JMX-DExporter

JMX-DExporter es una herramienta que expone métricas JMX en un formato compatible con Prometheus. Podemos usar JMX-DExporter para recopilar métricas de la JVM, como uso de memoria, actividad de hilo, estadísticas de recolección de basura y más.

6.1. Uso del Endpoint de Métricas

Para comenzar a usar JMX-DExporter, primero debemos descargar el agente JAR. Luego, configuramos nuestro exportador utilizando jmx_exporter_config.yml:

startDelaySeconds: 0
lowercaseOutputName: true
lowercaseOutputLabelNames: true

rules:
  - pattern: "java.lang:type=Memory"
    name: "jvm_memory_usage_bytes"
    labels:
      area: "$2"
    type: GAUGE

En este ejemplo, hemos configurado el patrón java.lang:type=Memory. Esto significa que queremos exportar los datos del MemoryMXBean a nuestro endpoint de métricas. Ahora, podemos iniciar nuestra aplicación con el JMX-DExporter adjunto:

java -javaagent:.path-to-agent-jar\jmx_prometheus_javaagent.jar=port:path-to-agent-jar\jmx_exporter_config.yml -jar .path-to-app\app.jar

En el comando de inicio, especificamos la ruta al JAR del agente descargado y el puerto en el que estará disponible el nuevo endpoint de métricas. Para ello, utilizamos el parámetro javaagent. Adicionalmente, especificamos la ruta al archivo de configuración y el JAR de nuestra aplicación.

Ahora, si llamamos al endpoint http://localhost:port/metrics, veremos la información completa proporcionada por el MBean de memoria, incluyendo los datos de uso de memoria no heap:

jvm_memory_committed_bytes{area="nonheap"} 1.8546688E7
# HELP jvm_memory_init_bytes Initial bytes of a given JVM memory area.
# TYPE jvm_memory_init_bytes gauge
jvm_memory_init_bytes{area="heap"} 5.32676608E8
jvm_memory_init_bytes{area="nonheap"} 7667712.0
# HELP jvm_memory_max_bytes Max (bytes) of a given JVM memory area.
# TYPE jvm_memory_max_bytes gauge
jvm_memory_max_bytes{area="heap"} 8.518631424E9
jvm_memory_max_bytes{area="nonheap"} -1.0

6.2. Integración Con Prometheus

Prometheus es un kit de herramientas de monitorización y alerta de código abierto que utilizamos para recopilar y almacenar métricas como datos de series temporales. Podemos integrar fácilmente nuestro endpoint de métricas con Prometheus.

Después de instalar la instancia de Prometheus, modificamos el archivo prometheus.yml con los siguientes valores:

scrape_configs:
  - job_name: "our_app"
    static_configs:
      - targets: ["localhost:port"]

En la propiedad job_name, especificamos cómo se llamará nuestro objetivo de Prometheus. En la propiedad targets, especificamos el host y el puerto de nuestro endpoint de métricas.

Después de realizar los cambios de configuración, necesitamos reiniciar la instancia de Prometheus y abrir la consola. En el campo de entrada, podemos escribir cualquier métrica disponible, incluyendo el uso de memoria no heap. Después de esto, el gráfico de la métrica de memoria se mostrará en la pantalla:

7. Conclusión

En este tutorial, revisamos un conjunto de herramientas que nos ayudan a monitorizar el uso de memoria no heap.

Para casos simples, cuando no necesitamos un informe detallado y tenemos acceso al JDK, podemos considerar herramientas básicas como utilizar directamente jcmd o jconsole. Para casos más complejos, donde se necesitan conocimientos detallados sobre tipos de memoria específicos, podemos utilizar grabaciones de vuelo y JMC.

Para integrar nuestra monitorización en métricas de Prometheus, podemos usar el agente JMX-Exporter. También debemos tener en cuenta el consumo de recursos adicionales de todas las herramientas de monitorización en cada caso específico.

Esperamos que esta guía te sea útil para mejorar el rendimiento y la eficiencia de tus aplicaciones Java. ¡No dudes en compartir tus experiencias y comentarios!