<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GOBE</title>
    <description>The latest articles on DEV Community by GOBE (@gobe).</description>
    <link>https://dev.to/gobe</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F10478%2F457382d7-8dd2-40af-a237-be4825227db7.png</url>
      <title>DEV Community: GOBE</title>
      <link>https://dev.to/gobe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gobe"/>
    <language>en</language>
    <item>
      <title>Java Dark Memory</title>
      <dc:creator>Raúl González</dc:creator>
      <pubDate>Wed, 02 Apr 2025 04:42:36 +0000</pubDate>
      <link>https://dev.to/gobe/java-dark-memory-okh</link>
      <guid>https://dev.to/gobe/java-dark-memory-okh</guid>
      <description>&lt;h2&gt;
  
  
  Índice
&lt;/h2&gt;

&lt;p&gt;Si quieres ir directamente al grano, pasa al apartado de Espacios de memoria en Java. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.  Una nueva era&lt;/li&gt;
&lt;li&gt;2.  El reto&lt;/li&gt;
&lt;li&gt;3.  El síntoma&lt;/li&gt;
&lt;li&gt;4.  Conceptos sobre memoria&lt;/li&gt;
&lt;li&gt;5.  Espacios de memoria en Java&lt;/li&gt;
&lt;li&gt;6.  Espacios pequeños&lt;/li&gt;
&lt;li&gt;7.  Native Memory Tracking&lt;/li&gt;
&lt;li&gt;8.  Conclusiones intermedias. Y seguimos&lt;/li&gt;
&lt;li&gt;9.  Malloc y fragmentación&lt;/li&gt;
&lt;li&gt;10. Buffers y Direct Memory Allocation&lt;/li&gt;
&lt;li&gt;11. Threads, Buffers y Arenas&lt;/li&gt;
&lt;li&gt;12. Soluciones&lt;/li&gt;
&lt;li&gt;13. Soluciones "alternativas"&lt;/li&gt;
&lt;li&gt;14. La fórmula para calcular la memoria&lt;/li&gt;
&lt;li&gt;14. Conclusiones finales&lt;/li&gt;
&lt;li&gt;15. Referencias y herramientas&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. Una nueva era (y Java sobrevive) &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;En la era de los &lt;strong&gt;contenedores&lt;/strong&gt; y ascenso de soluciones &lt;strong&gt;cloud&lt;/strong&gt;, no fueron pocos los que dieron por hecho la caída de &lt;strong&gt;Java&lt;/strong&gt; como lenguaje predilecto de &lt;em&gt;backend&lt;/em&gt; (dejo al margen Cobol o Natural, los lenguajes de mainframe, eso es otro universo paralelo). Incluso predijeron su final, una muerte lenta como una estrella de neutrones en la fase final de su vida.&lt;/p&gt;

&lt;p&gt;En general, podemos afirmar que se equivocaron. Si bien es cierto que perdió su hegemonía, más de la una década después, Java sigue siempre en los puestos altos de cualquier ranking. Y en el podio, muchas veces en la primera posición, si hablamos de soluciones empresariales a gran escala y con grandes cargas transaccionales.&lt;/p&gt;

&lt;p&gt;Sin duda, hay al menos &lt;strong&gt;dos factores&lt;/strong&gt; que contribuyen a su posicionamiento en el universo cloud. En primer lugar, está la existencia de &lt;strong&gt;frameworks de desarrollo&lt;/strong&gt; como &lt;em&gt;Spring&lt;/em&gt;, o su hermano pequeño &lt;em&gt;Quarkus&lt;/em&gt; (cada vez menos pequeño). Estos permiten la implementación de soluciones empresariales complejas, con un alto nivel de madurez, capacidades de integración con todo tipo de middlewares, interoperabilidad, y una enorme y solvente comunidad detrás de ellos. En segundo lugar, tenemos el auge de las &lt;strong&gt;arquitecturas de microservicios&lt;/strong&gt;, impulsado con fuerza por la aparición de los &lt;strong&gt;orquestadores de contenedores&lt;/strong&gt;. La adaptación de &lt;em&gt;Spring&lt;/em&gt; como &lt;strong&gt;Spring Boot&lt;/strong&gt;, y el propio crecimiento de &lt;strong&gt;Quarkus&lt;/strong&gt; y &lt;strong&gt;Micronaut&lt;/strong&gt; como frameworks de microservicios (y el declive del pionero y divertido framework de &lt;em&gt;Netflix&lt;/em&gt;) sostuvieron la comunidad Java en este &lt;em&gt;bosque oscuro&lt;/em&gt;, hostil y despiadado, del nuevo universo cloud recién descubierto. &lt;/p&gt;

&lt;p&gt;Parece lógico después de todo. Las arquitecturas empresariales requieren soluciones sólidas, seguras, escalables y eficientes. Las arquitecturas de microservicios aún más, ya que introducen varios grados adicionales de complejidad y nuevos retos: resiliencia, eventos, orquestación/coreografía, &lt;em&gt;fault tolerance&lt;/em&gt;, amén de la explosión de patrones &lt;em&gt;CQRS&lt;/em&gt; y &lt;em&gt;CDC&lt;/em&gt;, entre muchos otros. Sólo frameworks maduros, sólidos y muy ricos en funcionalidad pueden aguantar este envite.&lt;/p&gt;

&lt;p&gt;Pero dejemos ya la evolución de los lenguajes en el mundo cloud, eso sin duda da para otro post, o una saga completa sobre el tema. Java sobrevivió, y los javeros tenemos un nuevo e importante reto, que es meter nuestras soluciones &lt;strong&gt;Java en contenedores&lt;/strong&gt;. Vamos a ello.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Java en contenedores, un auténtico reto &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Ejecutar cargas basadas en la &lt;strong&gt;máquina virtual Java&lt;/strong&gt; (&lt;strong&gt;JVM&lt;/strong&gt; a partir de ahora), corriendo en contenedores, no es una tarea sencilla. ¿Cuántos contenedores han caído, caen y seguirán cayendo en entornos productivos con códigos de error por &lt;em&gt;OOMKilled&lt;/em&gt; (Out of Memory Killed) en entornos &lt;em&gt;Kubernetes&lt;/em&gt;? Siguiendo las metáforas sobre el universo, incontables como las estrellas. Bueno, vale, no tantos, pero muchos. Seguro que más de lo deseado.&lt;/p&gt;

&lt;p&gt;En los siguientes apartados vamos a explorar este reto en profundidad, y vamos a dar soluciones al mismo... ¡al menos eso espero!&lt;/p&gt;

&lt;p&gt;Las entrañas de la máquina virtual Java y su &lt;em&gt;Java Memory Model&lt;/em&gt; (JMM) son poco conocidas, incluso entre la comunidad javera, o entre los operadores de sistemas al cargo de los sistemas productivos. En parte esto era &lt;em&gt;"lógico"&lt;/em&gt; antes de la era de los contenedores, ya que la ejecución de JVMs en &lt;strong&gt;máquinas virtuales o hosts&lt;/strong&gt;, donde había un &lt;strong&gt;amplio margen para el alojamiento de memoria off-heap&lt;/strong&gt;, hacía que los developers no tuvieran que preocuparse demasiado por estos problemas. Porque sí, es cierto: la JVM aloja grandes cantidades de memoria off-heap, &lt;strong&gt;más grandes que el heap&lt;/strong&gt; en la mayoría de las ocasiones, de las que no somos conscientes. Estos hosts tienen a su disposición grandes cantidades de memoria (reservadas para el sistema operativo, jeje). Los grandes &lt;strong&gt;clústeres de servidores de aplicaciones JEE&lt;/strong&gt; como &lt;em&gt;WebSphere&lt;/em&gt; o &lt;em&gt;JBoss&lt;/em&gt; son un claro ejemplo. Cada nodo de esos clústers son máquinas virtuales o hosts (bare metal) donde menos de la mitad de la memoria era para los servidores de aplicaciones, y el resto para el sistema operativo. O eso creían. La observación del consumo de memoria de estos sistemas, sin embargo, ofrecía que los consumos de los procesos de las JVM eran siempre más altos de los esperado. Primer síntoma de la &lt;strong&gt;memoria oscura&lt;/strong&gt;... Las soluciones entonces eran simples: pongo más memoria a los hosts y listo. O un reinicio programado de los diferentes nodos del clúster un par de veces por semana, lo que mágicamente liberaba memoria que teóricamente no debería ser tan alta.&lt;/p&gt;

&lt;p&gt;Cuando pasamos de una máquina virtual o host, a &lt;strong&gt;un universo mucho más limitado en recursos como son los contenedores&lt;/strong&gt;, los problemas crecen. En un contenedor, el host es el propio contenedor, y está restringido por los &lt;strong&gt;límites&lt;/strong&gt; del mismo. Los límites de memoria de un contenedor son importantes, y tienden a ser mínimos. Es decir, la &lt;em&gt;talla&lt;/em&gt; de memoria de mi contenedor debe ser la mínima posible para ejecutar el proceso Java, para dejar recursos a los otros contenedores del ecosistema. No podemos levantar contenedores tan grandes, hablando de memoria, como los antiguos hosts, sería un desperdicio enorme de recursos. Así que levantamos contenedores pequeños. Y ahí es donde se hacen evidentes las necesidades de ajuste fino de los procesos Java. Los contenedores se caen por falta de memoria, pero no por el conocido heap, sino por el desconocido y difícil de observar off-heap. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo9u0mof3w4kkdixk6oit.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo9u0mof3w4kkdixk6oit.png" alt="Image description" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vamos a llamar a esta memoria, la &lt;strong&gt;memoria oscura&lt;/strong&gt;, que da título a este post. Como la materia oscura del universo, está ahí, interactúa con la materia visible y en conjunto explican el funcionamiento del universo, es decir, de nuestros contenedores.&lt;/p&gt;

&lt;p&gt;Vamos a adentrarnos en la zona oscura...&lt;/p&gt;

&lt;h2&gt;
  
  
  3. El síntoma de la memoria oscura &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F15y6ckj4vqyexc3rpbg9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F15y6ckj4vqyexc3rpbg9.png" alt="Image description" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vamos a suponer que nuestros contenedores se ejecutan en algún &lt;strong&gt;orquestador de contenedores&lt;/strong&gt;. Este será el caso de cualquier sistema productivo &lt;em&gt;"serio"&lt;/em&gt; basado en contenedores. A día de hoy ese orquestador será &lt;em&gt;Kubernetes&lt;/em&gt; en algunos de sus sabores y colores (Openshift, EKS, AKS, GKS...). Dejamos para otro día la batalla de los orquestadores, que sin duda nos daría para otro post (me lo apunto, ya van dos...). Nuestros contenedores en kubernetes, se incluyen en otro tipo de contenedor que son los &lt;strong&gt;PODs&lt;/strong&gt;, que pueden tener uno o varios contenedores. Para simplificar, durante el resto del post vamos asumir que nuestro contenedor con JVM, es el único contenedor de un POD.&lt;/p&gt;

&lt;p&gt;Cuando la memoria oscura de la JVM desborda los límites establecidos para el contenedor/POD, obtendremos el archiconocido error &lt;strong&gt;OOMKilled&lt;/strong&gt;, con código de salida &lt;strong&gt;137&lt;/strong&gt;. Os suena ¿verdad? Para ilustrarlo de forma sencilla En la imagen de arriba, he forzado un contenedor cuyo &lt;em&gt;limit&lt;/em&gt; está por debajo del &lt;em&gt;request&lt;/em&gt;, lo que hace que el contenedor muera nada más arrancar. El request es la memoria mínima que declaramos para arrancar, y el limit es la memoria máxima hasta la que permitimos expandirse nuestro contenedor.&lt;/p&gt;

&lt;p&gt;El orquestador intentará levantar varias veces el contenedor, no lo conseguirá y se rendirá después de cinco o seis intentos.&lt;/p&gt;

&lt;p&gt;Divertido de observar... e ilustrativo. En un espacio de tiempo mucho más reducido, esto es lo que pasa con nuestros contenedores expuestos a la memoria oscura. En la vida real tardaremos mucho más tiempo en notar los efectos OOMKilled, quizá minutos, horas, o días en el mejor de los casos, pero seguro que aparecerán.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Conceptos básicos: memoria virtual, residente y nativa &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Empieza un poco de clase teórica, es inevitable... pero lo haremos rápido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsf6vevahbj8dlwcojbw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsf6vevahbj8dlwcojbw.png" alt="Image description" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Todo contenedor en ejecución es un proceso Linux&lt;/strong&gt;. Ok, sí, es cierto: existen los &lt;strong&gt;contenedores windows&lt;/strong&gt;, pero vamos a obviarlos (ese post lo dejo para otro valiente).&lt;/p&gt;

&lt;p&gt;Bien, pues todo proceso Linux, hablando de su memoria, tiene la misma pinta. Repasemos algunos conceptos que utilizaremos como referencia durante el post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;La &lt;strong&gt;memoria virtual&lt;/strong&gt;, es la cantidad de memoria que un proceso &lt;em&gt;"piensa"&lt;/em&gt; que tiene a su disposición. Y &lt;strong&gt;no está limitado por el mundo real&lt;/strong&gt;. Es decir, que el tamaño de la memoria física real, ya sea la del host o la del contenedor (&lt;em&gt;limit&lt;/em&gt;) no influye en la &lt;em&gt;reserva&lt;/em&gt; de este espacio cuando un proceso arranca. En el mundo de los contenedores y Java, &lt;strong&gt;la memoria virtual puede (y suele) ser mayor que el límite del propio contenedor&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;La &lt;strong&gt;memoria residente&lt;/strong&gt; (&lt;em&gt;RSS&lt;/em&gt; en muchas métricas como las de &lt;em&gt;Prometheus&lt;/em&gt;), es la memoria física real que un proceso se reserva. Puede estar siendo utilizada, o no. Aquí aparece el concepto de &lt;strong&gt;&lt;em&gt;memoria comiteada&lt;/em&gt;&lt;/strong&gt;, que es la memoria física real utilizada. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;La &lt;strong&gt;memoria nativa&lt;/strong&gt;, es la memoria utilizada por mi proceso. Simplificando, es igual a la RSS (con algunos matices importantes). Introducimos aquí este concepto porque suele causar controversia. Muchos llaman memoria nativa a todo los que no es heap. Otros usan este término para referirse a la memoria residente que no es ni heap ni off-heap, pervirtiendo así la definición de off-heap. En la imagen, sería ese espacio encima del off-heap. De todo esto hablaremos en profundidad enseguida.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Por ahora, para tratar de mantener un lenguaje ubicuo y simple, nos referiremos a los términos de arriba. Virtual, y residente=nativa. Y dentro de la nativa, heap y off-heap en un proceso Java. Mis disculpas por adelantado si yo mismo me salto estas reglas más adelante, intentaré no hacerlo....&lt;/p&gt;

&lt;p&gt;De estas definiciones, seguro que surgen rápidamente varias preguntas para el lector. Vamos a dar las respuestas rápidas, y los detalles vendrán después en las siguientes secciones.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Puedo tener más memoria virtual que física? &lt;strong&gt;Sí&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;¿Puedo tener más memoria virtual que residente? &lt;strong&gt;Sí&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;¿Puedo &lt;em&gt;limitar&lt;/em&gt; las memorias virtuales y físicas? &lt;strong&gt;Sí&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;¿Puedo &lt;em&gt;medir&lt;/em&gt; las memorias virtuales y físicas? &lt;strong&gt;Sí&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Espacios de memoria Java &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;La memoria de nuestro proceso Java, es decir de la JVM que ejecuta nuestra aplicación Java, según la especificación &lt;em&gt;JMM&lt;/em&gt; (Java Memory Model), se divide en &lt;strong&gt;"espacios"&lt;/strong&gt;. Casi toda, porque hay otras zonas de memoria que no siguen esta regla, y que abordaremos más adelante. Por ahora nos quedamos con este concepto de &lt;strong&gt;espacio de memoria&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgsl7lecy3i2l4r2ria5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgsl7lecy3i2l4r2ria5d.png" alt="Image description" width="800" height="628"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hay muchos espacios en la &lt;em&gt;JVM&lt;/em&gt;, pero todos siguen las mismas reglas respecto a su dimensionamiento y uso:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Existe un &lt;strong&gt;límite máximo&lt;/strong&gt; de tamaño de espacio, llamado &lt;strong&gt;max&lt;/strong&gt;. Este valor indica el tamaño de la memoria virtual reservada en ese espacio. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;El proceso en ejecución irá &lt;em&gt;"llenando"&lt;/em&gt; ese espacio virtual con memoria física, esto es, &lt;strong&gt;comiteando&lt;/strong&gt; páginas físicas de memoria. Este valor es la memoria física real usada por el espacio, es decir, &lt;strong&gt;la memoria residente (RSS)&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La memoria comiteada se reserva por bloques&lt;/strong&gt;. Esto quiere decir que si mi proceso necesita 1MB &lt;em&gt;"real"&lt;/em&gt; de memoria física, y el espacio no tiene el tamaño suficiente para alojarlo, el proceso reservará un bloque que será mayor, pongamos 20MB. Estos 20MB se comitean como bloque, aunque de esos 20MB, sólo está usando 1MG. Si inmediatamente después el proceso necesita alojar otro MB, este ya &lt;em&gt;cabe&lt;/em&gt; en el bloque comiteado anteriormente y no habrá necesidad de comitear más espacio. Esta es la diferencia entre lo que las métricas nos muestran como &lt;em&gt;commited&lt;/em&gt; y &lt;em&gt;used&lt;/em&gt; dentro de un espacio. &lt;strong&gt;Commited&lt;/strong&gt; es la memoria física comiteada en grandes bloques. Used es la memoria realmente usada dentro de esos bloques comiteados. Por lo tanto &lt;em&gt;used&lt;/em&gt; será siempre menor que &lt;em&gt;commited&lt;/em&gt;, y a su vez &lt;em&gt;commited&lt;/em&gt; menor que &lt;em&gt;max&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Por último, es posible, aunque no obligatorio, que un espacio tenga un valor &lt;strong&gt;min&lt;/strong&gt; o &lt;strong&gt;start&lt;/strong&gt;. Este valor indica el valor mínimo de memoria comiteada (residente) con el que se crea un espacio. Podríamos decir que es equivalente en concepto al &lt;em&gt;request&lt;/em&gt; de un POD/contenedor.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;De nuevo, seguro que se disparan muchas preguntas en la mente del lector... Respondemos brevemente, y en la siguiente sección vamos con los detalles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vale pero... ¿Cuántos espacios hay? &lt;strong&gt;Muchos... y no son siempre iguales entre diferentes implementaciones de la JVM. Actualmente hay decenas de implementaciones, y cada una de ellas interpreta la especificación &lt;em&gt;JMM&lt;/em&gt; a su manera, completando las definiciones ambiguas o incompletas de la especificación a su discreción. La mayoría de las implementaciones coinciden en los espacios más grandes y conocidos, pero suelen diferir en muchos otros espacios "pequeños". De media podemos decir que existen entre 15 y 20.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;¿Y si la suma de los &lt;strong&gt;max&lt;/strong&gt; (&lt;em&gt;virtual&lt;/em&gt;) supera el límite del contenedor, ¿qué pasa? &lt;strong&gt;No pasa nada hasta que no se comitean.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Y si un espacio se va llenando de manera que commited (RSS) llega hasta max, ¿qué pasa? &lt;strong&gt;Se producirá un desbordamiento en ese espacio. Y tendremos una excepción de OutOfMemoryError en runtime. Este caso es "benigno", porque de forma clara sabremos que uno de los espacios lo tenemos que redimensionar, pero sabemos qué espacio es exactamente. El proceso Java a partir de ese momento ya no es fiable, no puede operar con normalidad y probablemente se auto terminará como el bueno de Arnold, con un exit code 3.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Y si la &lt;em&gt;suma de los commited&lt;/em&gt; de todos los espacios (RSS) es mayor el límite del contenedor, ¿qué pasa? &lt;strong&gt;OOMKilled, exit code 137. Es el síntoma de la memoria oscura...&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;¿Y puedo establecer &lt;strong&gt;start&lt;/strong&gt; y &lt;strong&gt;max&lt;/strong&gt; para todos estos espacios, de forma que no se produzca &lt;strong&gt;nunca&lt;/strong&gt; el OOMKilled? &lt;strong&gt;Pues por desgracia, no para todos. Sí para muchos de ellos, los más grandes e importantes, que incluso toman valores por defecto "válidos" en muchas ocasiones, pero no para todos. Y aunque se pudiera, que no se puede, aún hay zonas de memoria no que siguen la lógica de los espacios...&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Se empieza a adivinar la dimensión del problema, ¿verdad?&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Los espacios "pequeños" &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Para no empeorar nuestra salud mental, y no abordar el problema individualmente en cada uno de estos 10 o 20 espacios, vamos a crear un grupo de espacios con este nombre, y meteremos allí aquellos que cumplen los siguientes requisitos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Son de tamaño &lt;strong&gt;"pequeño"&lt;/strong&gt;. En comparación con los espacios grandes claro, como &lt;em&gt;Heap&lt;/em&gt; o &lt;em&gt;Metaspace&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;No tienen &lt;strong&gt;“max”&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Reservan espacio virtual en base a la necesidad que tengan, y la &lt;strong&gt;ergonomía&lt;/strong&gt; de la &lt;em&gt;JVM&lt;/em&gt;. Dicha ergonomía (&lt;em&gt;Java Ergonomics&lt;/em&gt;) son un conjunto de decisiones heurísticas que de forma automática toma un proceso Java en ejecución, en base la "visibilidad" del contexto de ejecución: memoria visible, CPU visible, cantidad de memoria disponible, etc.&lt;/li&gt;
&lt;li&gt;Su crecimiento no es lineal respecto a la carga transaccional del proceso, suelen alcanzar un &lt;strong&gt;límite&lt;/strong&gt; y no aumentan más allá.&lt;/li&gt;
&lt;li&gt;Su tamaño es &lt;strong&gt;previsible y con poco riesgo&lt;/strong&gt; de desbordamiento.&lt;/li&gt;
&lt;li&gt;Todos ellos sumados &lt;strong&gt;ocupan un espacio que ya no es “pequeño”&lt;/strong&gt; y debe computarse para dimensionar correctamente los límites del contenedor/proceso.&lt;/li&gt;
&lt;li&gt;Pueden “solaparse” con otros espacios, dependiendo de la implementación de la &lt;em&gt;JVM&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vemos algunos ejemplos.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxfb1h4f0ee8a8p47jije.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxfb1h4f0ee8a8p47jije.png" alt="Image description" width="343" height="736"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;En la imagen de arriba, podemos ver una lista (incompleta) de espacios documentados por Oracle en JDK (OpenJDK).&lt;/p&gt;

&lt;p&gt;Aquellos que tienen un número rojo, son estos &lt;em&gt;espacios pequeños&lt;/em&gt; de los que estamos hablando, y el valor del número indica el tamaño en MB medido para ese espacio, en un microservicio desarrollado con Spring Boot y ejecutado como contenedor.&lt;/p&gt;

&lt;p&gt;No vamos a entrar en la definición y uso de cada uno de ellos. Sólo algunas consideraciones importantes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;El &lt;strong&gt;tamaño puede variar bastante en función del uso que hagamos de ellos&lt;/strong&gt;, indirectamente, a través de nuestro programa Java. Por ejemplo, es espacio &lt;strong&gt;Symbol&lt;/strong&gt; almacena información como nombres de campos de clases, signaturas de métodos o Strings. Así que su tamaño dependerá de mi programación.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;El &lt;strong&gt;tamaño también puede variar bastante debido a la configuración&lt;/strong&gt; o ergonomía del proceso. Por ejemplo, el espacio de &lt;strong&gt;GC&lt;/strong&gt; (espacio interno utilizado por el Garbage Collector para su correcto funcionamiento), puede ser relativamente "pequeño" si usamos collector de tipo &lt;em&gt;Serial&lt;/em&gt; (el más simple), y puede requerir mucha más memoria si usamos un collector concurrente de tipo &lt;em&gt;G1&lt;/em&gt;, por ejemplo. Y además el tamaño siempre será proporcional al tamaño que hayamos dispuesto para el heap. En el ejemplo, esos 15MB corresponden a &lt;em&gt;Serial&lt;/em&gt;. Si lo cambiamos a G1, podría ocupar 80 o 100Mb perfectamente.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para no alargar demasiado al artículo, no vamos a hablar aquí en profundidad de los otros grandes espacios conocidos: &lt;strong&gt;heap, metaspace, code y thread&lt;/strong&gt;.&lt;br&gt;
En su lugar os dejo &lt;strong&gt;links&lt;/strong&gt; a otros artículos que forman parte de esta serie, donde podréis encontrar el detalle. Podéis leerlos, o no. Haremos referencia a ellos más adelante, ya que hay relaciones entre estos espacios y la problemática general. Siempre podéis parar, ir al otro artículo de la serie y volver una vez revisada la referencia. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/gobe/java-dark-memory-heap-space-49l4"&gt;Java Dark Memory: Heap Space&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gobe/java-dark-memory-code-space-4c97"&gt;Java Dark Memory: Code Space&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gobe/java-dark-memory-class-space-1f0k"&gt;Java Dark Memory: Class Space&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gobe/java-dark-memory-thread-space-1o2"&gt;Java Dark Memory: Thread Space&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  7. Native Memory Tracking, luz en la oscuridad &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Afortunadamente, disponemos de una poderosa herramienta para aportar luz en el &lt;strong&gt;uso y tamaño de los espacios de memoria&lt;/strong&gt;, es &lt;strong&gt;NMT&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NMT&lt;/strong&gt; (&lt;em&gt;Native Memory Tracking&lt;/em&gt;) es una característica de Java Hotspot VM que rastrea el uso de la memoria interna para una HotSpot JVM.&lt;/li&gt;
&lt;li&gt;Se puede acceder a los datos NMT utilizando el comando &lt;strong&gt;jcmd&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;NMT no realiza un seguimiento de las asignaciones de memoria de código nativo de terceros ni de las bibliotecas nativas de la propia JDK.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NMT no está habilitada por defecto, así tenemos que añadir un parámetro de arranque a nuestros procesos Java:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-XX:NativeMemoryTracking=[off | summary | detail]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Donde:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;off&lt;/strong&gt;: valor por defecto, deshabilitado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;summary&lt;/strong&gt;: este modo nos ofrece información global sobre los espacios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;detail&lt;/strong&gt;: mucha más información sobre los espacios, quizá demasiada en la mayoría de los casos. Sólo usar en casos extremos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Una vez habilitada podemos acceder a la información con jcmd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jcmd &amp;lt;pid&amp;gt; VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Donde:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;pid&lt;/strong&gt;: número del proceso Java. Si estamos ejecutando la JVM en un contenedor, este pid es habitualmente el proceso 1.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;summary&lt;/strong&gt;|&lt;strong&gt;detail&lt;/strong&gt;: si hemos habilitado NMT en modo &lt;em&gt;summary&lt;/em&gt;, podemos pedir el resultado en modo resumen, el &lt;em&gt;summary&lt;/em&gt;, pero no el &lt;em&gt;detail&lt;/em&gt;. Si hemos habilitado NMT en modo &lt;em&gt;detail&lt;/em&gt;, podemos pedir el resumen, el &lt;em&gt;summary&lt;/em&gt; y/o el &lt;em&gt;detail&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;baseline&lt;/strong&gt;: podemos pedir a NMT que guarde una &lt;strong&gt;instantánea o snapshot&lt;/strong&gt; del estado de mis espacios de memoria, para ver su evolución en el tiempo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;summary.diff|detail.diff&lt;/strong&gt;: si tenemos guardada una instantánea (baseline) podemos pedir un informe de &lt;strong&gt;como han evolucionado&lt;/strong&gt; los espacios desde la última foto.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;scale&lt;/strong&gt;: para pedir los informes en diferentes &lt;strong&gt;unidades de medida&lt;/strong&gt;: Kbyes, Mbytes o Gbytes&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Veamos un ejemplo. Vamos lanzar NMT sobre un contenedor Java en el que se está ejecutando una aplicación Spring Boot, con NMT habilitada. La versión de la JVM es la implementación de Red Hat para HotSpot 17. Entraremos a nuestro POD/contenedor por ssh (en este caso desplegado en Openshift), y en el terminal ejecutamos el siguiente comando&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jcmd 1 VM.native_memory summary scale=MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo que nos dará la siguiente información, vamos a interpretarla:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdsh9n52lc3rc5tsr5sg4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdsh9n52lc3rc5tsr5sg4.png" alt="Image description" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;En primer lugar, veremos la línea de &lt;strong&gt;Total&lt;/strong&gt;. En ella vemos los valores para &lt;strong&gt;reserved&lt;/strong&gt; y &lt;strong&gt;committed&lt;/strong&gt;, ya sabemos lo que significa:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;reserved&lt;/strong&gt;: la &lt;strong&gt;memoria virtual&lt;/strong&gt;, el &lt;strong&gt;max&lt;/strong&gt; del espacio&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;commited&lt;/strong&gt;: la &lt;strong&gt;memoria residente RSS&lt;/strong&gt; que ha sido comiteada, es decir, la realmente usada &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Justo debajo vemos la misma información, pero relativa a &lt;strong&gt;cada uno de los espacios&lt;/strong&gt;. Comentemos algunos aspectos interesantes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El propio &lt;strong&gt;NMT una vez habilitado requiere su propio espacio&lt;/strong&gt; para funcionar. En la imagen vemos que se reserva 12MB, y los ha usado todos.&lt;/li&gt;
&lt;li&gt;Los espacios con una reserva (memoria virtual) &lt;strong&gt;menores de 1MB no aparecen en el informe&lt;/strong&gt;. En la imagen podemos comprobar que los espacios "pequeños" de Logging, Arguments y Module no se muestran al no llegar a este mínimo.&lt;/li&gt;
&lt;li&gt;Aparecen algunos &lt;strong&gt;espacios duplicados o solapados&lt;/strong&gt;. En la imagen podemos ver que &lt;strong&gt;Metaspace&lt;/strong&gt; aparece dos veces. Ya hemos visto en &lt;a href="https://dev.to/gobe/java-dark-memory-class-space-1f0k"&gt;Java Dark Memory: Class Space&lt;/a&gt; que &lt;em&gt;Metaspace&lt;/em&gt; es un sub-espacio dentro de &lt;em&gt;Class&lt;/em&gt;, que contiene &lt;em&gt;Metaspace&lt;/em&gt; y &lt;em&gt;CompressedClass-space&lt;/em&gt;. Aquí, &lt;em&gt;Metaspace&lt;/em&gt; aparece dos veces, una dentro de &lt;em&gt;Class&lt;/em&gt;, y otra como si fuera otro espacio independiente. Esta información puede resultar difícil de interpretar las primeras veces, pero hay que acostumbrarse a estos pequeños defectos que existen en prácticamente todas las implementaciones de la JVM y que además pueden variar no sólo entre implementaciones de diferentes vendors (IBM, Red Hat, Oracle, Azure, etc.), sino también entre versiones del mismo vendor (11, 17, 21, etc.). Debemos adquirir experiencia con la JVM que utilicemos, para saber interpretarla. Podemos observar otra anomalía: &lt;em&gt;CompressedClass-space&lt;/em&gt; también aparece dos veces, en &lt;em&gt;Unknown&lt;/em&gt; y dentro de &lt;em&gt;Class&lt;/em&gt;. Lo dicho, hay que convivir con ello. ¡Al menos el Total está bien sumado!&lt;/li&gt;
&lt;li&gt;Todo lo que no sea &lt;strong&gt;Heap&lt;/strong&gt;, es &lt;strong&gt;Off-Heap&lt;/strong&gt; (o non-heap). En la imagen podemos ver que de los 586MB de memoria virtual o reservada, 170MB corresponden a Heap y 416MB a Off-Heap, un ratio de 30%-70% aproximadamente. Si hablamos de memoria comiteada o residente, tenemos 313MB totales, de los cuales 61MB corresponden a Heap y 252 a Off-Heap, un ratio de 20%-80% aprox.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Conclusiones (intermedias) &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Con lo visto hasta aquí vamos a hacer un pequeño resumen, a modo de preguntas y respuestas con dudas que seguro han ido surgiendo con la lectura. Vamos allá.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Me sorprende el &lt;em&gt;ratio de uso heap vs off-heap&lt;/em&gt; del ejemplo. ¿Eso es normal? &lt;strong&gt;Pues sí. Evidentemente todo depende del tipo de aplicación. En el marco de &lt;em&gt;microservicios&lt;/em&gt;, es decir, aplicaciones &lt;em&gt;state-less&lt;/em&gt; que no guardan datos de sesiones de usuario, el uso del heap tiende a ser ese 20 o 25%. Aplicaciones &lt;em&gt;con estado&lt;/em&gt;, o si metemos &lt;em&gt;cachés embebidas en memoria&lt;/em&gt; tenderán a usar mucho más heap, quizá un 50% o más. Pero ojo: subir el tamaño del heap no quiere decir que quitemos tamaño del off-heap. Si mi aplicación necesita un heap "grande", el off-heap no se va a reducir. Al contrario, cuanto más grande sea el heap, espacios indirectamente relacionados como el de &lt;em&gt;NMT&lt;/em&gt; o &lt;em&gt;GC&lt;/em&gt;, necesitarán más memoria off-heap para hacer su trabajo, que es precisamente operar sobre el heap. Así que &lt;em&gt;si subimos el heap, debemos subir el limit del contenedor/POD&lt;/em&gt; para que puede dar cabida al aumento de heap, sin disminuir el off-heap.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;En los datos de los informes de NMT, puedo ver memoria &lt;em&gt;virtual/reservada&lt;/em&gt;, &lt;em&gt;committed/RSS&lt;/em&gt;, pero no &lt;strong&gt;used&lt;/strong&gt;. &lt;strong&gt;Es correcto. Para ver memoria used, es decir, la memoria usada por el proceso dentro de un espacio, que será siempre menor a commited, debemos tirar de métricas como &lt;em&gt;Prometheus&lt;/em&gt; o &lt;em&gt;JMX&lt;/em&gt; como hemos visto en &lt;a href="https://dev.to/gobe/java-dark-memory-heap-space-49l4"&gt;Java Dark Memory: Heap Space&lt;/a&gt;. Pero este dato no es relevante, ya que los desbordamientos se producirán cuando el proceso intenta alojar un nuevo bloque para commited/RSS, y no pueda por exceder los límites del contenedor, ese el dato interesante.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;NMT&lt;/em&gt; está disponible en &lt;em&gt;JVMs&lt;/em&gt; de tipo &lt;em&gt;hostspot&lt;/em&gt;, ¿pero y si uso una JVM de tipo OpenJ9 como &lt;em&gt;temurin&lt;/em&gt;, &lt;em&gt;semeru&lt;/em&gt;, o &lt;em&gt;AdoptJDK&lt;/em&gt;? &lt;strong&gt;Pues por desgracia &lt;em&gt;NMT&lt;/em&gt; no está disponible &lt;em&gt;JVMs&lt;/em&gt; de tipo &lt;em&gt;OpenJ9&lt;/em&gt;. En este artículo nos centramos en las de tipo &lt;em&gt;hotspot&lt;/em&gt;, pero sería interesante otro artículo sobre cómo obtener métricas de memoria nativa en &lt;em&gt;OpenJ9&lt;/em&gt;, y en general las diferencias entre ambas. Me lo apunto... ¿cuántos van ya?&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Hemos visto el modo &lt;em&gt;summary&lt;/em&gt;, ¿pero que ofrece adicionalmente el modo &lt;em&gt;detail&lt;/em&gt;? &lt;strong&gt;Lo mismo que summary, pero añade el detalle de todas y cada una de las memory allocations que se han producido en cada espacio, su motivo, y el identificador del thread que ha generado el alojamiento. Es una cantidad ingente de información, y en mi opinión, sólo es útil en casos desesperados&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Entonces, ¿puedo configurar todos los espacios, con un mínimo y un máximo? &lt;strong&gt;No todos. Los espacios "grandes" como &lt;em&gt;heap&lt;/em&gt;, &lt;em&gt;class&lt;/em&gt; y &lt;em&gt;code&lt;/em&gt;, sí los puedo configurar como hemos detallado en &lt;a href="https://dev.to/gobe/java-dark-memory-heap-space-49l4"&gt;Java Dark Memory: Heap Space&lt;/a&gt;, &lt;a href="https://dev.to/gobe/java-dark-memory-class-space-1f0k"&gt;Java Dark Memory: Class Space&lt;/a&gt;, &lt;a href="https://dev.to/gobe/java-dark-memory-code-space-4c97"&gt;Java Dark Memory: Code Space&lt;/a&gt;. El espacio de &lt;em&gt;threads&lt;/em&gt;, como hemos visto en &lt;a href="https://dev.to/gobe/java-dark-memory-thread-space-1o2"&gt;Java Dark Memory: Thread Space&lt;/a&gt;, depende del tamaño de los pools de threads que use en mi contenedor. Si conozco los pools que utilizo y su tamaño máximo, puedo estimar con bastante precisión el tamaño máximo al que llegará este espacio. Los espacios pequeños no, la JVM toma decisiones en runtime sobre el tamaño de estos espacios. Pero lo que sí puedo y debo hacer es medirlos con NMT con una carga sostenida en mi contenedor, de manera que puedo saber cuánto ocupan realmente, y así ajustar el límite y el ratio heap/off-heap&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Vale entendido, entonces si tengo claros los tamaños de los espacios grandes y pequeños, los he medido y ajustado con una carga sostenida, he sumado su tamaño máximo de memoria residente de todos ellos y por lo tanto tengo claro el límite de mi contenedor, puedo estar seguro de que la memoria residente nunca producirá desbordamiento, ¡¡y evitaré todos los OOMKilled!! ¿¿verdad?? &lt;strong&gt;Buena pregunta. Pero por desgracia no es así. Hemos mencionado antes que aparte de los espacios de memoria grandes y pequeños, hay otros tipos de alojamientos, de los que vamos a hablar en los siguientes apartados. Y esta memoria es oscura de verdad, porque ni con NMT podemos verlos...&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vamos a adentrarnos en la zona oscura de verdad....&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Malloc. Sí, malloc. &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;La máquina virtual Java &lt;em&gt;JVM&lt;/em&gt; &lt;strong&gt;está implementada en lenguaje C&lt;/strong&gt;.&lt;br&gt;
Y como recordamos de nuestros tiempos en la universidad, la memoria en &lt;em&gt;C&lt;/em&gt; se reserva con la función &lt;strong&gt;malloc()&lt;/strong&gt;, (memory-allocation). Que recuerdos ¿verdad?&lt;/p&gt;

&lt;p&gt;Tranquilos javeros, no vamos a entrar en código &lt;em&gt;C&lt;/em&gt;, ni en teoría de gestión de memoria de sistemas operativos, sólo los conceptos básicos para entender el problema subyacente.&lt;/p&gt;

&lt;p&gt;La &lt;em&gt;JVM&lt;/em&gt; reserva memoria llamando a &lt;em&gt;malloc&lt;/em&gt;, parte de la librería nativa glibc, que aporta el system memory allocator para cualquier proceso linux, que es el allocator por defecto (hay otros...).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Malloc&lt;/em&gt; a su vez llamará a diferentes funciones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;mmap()&lt;/strong&gt;: para reservar grandes espacios de memoria&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;brk()&lt;/strong&gt; o &lt;strong&gt;sbrk()&lt;/strong&gt; para fragmentos pequeños, normalmente menores a 128KB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo importante para nosotros es saber que la reserva de espacios de memoria Java se realiza con &lt;strong&gt;mmap&lt;/strong&gt; (quizá &lt;em&gt;brk&lt;/em&gt; para espacios muy pequeños, no nos importa).&lt;/p&gt;

&lt;p&gt;Esto quiere decir que cuando la &lt;em&gt;JVM&lt;/em&gt; reserva espacio para &lt;em&gt;Metaspace&lt;/em&gt;, por ejemplo, se realiza un &lt;strong&gt;mmap()&lt;/strong&gt; con un tamaño inicial, el valor &lt;strong&gt;start&lt;/strong&gt; del del espacio.&lt;/p&gt;

&lt;p&gt;Cuando más adelante se realice un commit de memoria, siguiendo con el ejemplo sobre el Metaspace, para alojar mis definiciones de classes, una parte de esa memoria virtual reservada será comiteada, es decir pasa a ser &lt;strong&gt;memoria residente RSS&lt;/strong&gt;. Como los sistemas operativos modernos comitean siempre en bloques mínimos de 4KB, es inevitable que se produzca &lt;strong&gt;fragmentación interna&lt;/strong&gt;. Es decir que tenemos que asumir que &lt;strong&gt;la memoria residente RSS "real" puede ser mayor que la esperada, incluso mayor que el valor max que tenga configurado para el espacio&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Pero hay más. &lt;em&gt;mmap()&lt;/em&gt; reserva inicialmente, pongamos 20MB, si ese es el valor &lt;strong&gt;start&lt;/strong&gt; del espacio. Si durante la ejecución del proceso, necesitamos más de esos 20MB, se ampliará es espacio con &lt;strong&gt;otro mmap()&lt;/strong&gt;. Y esta nueva zona de memoria &lt;strong&gt;puede que no sea contigua a la anterior&lt;/strong&gt;, de hecho, es bastante probable. El &lt;em&gt;system allocator&lt;/em&gt; hará su máximo esfuerzo por obtener memoria contigua, pero en muchas ocasiones no lo conseguirá, porque no hay bloques contiguos libres lo suficientemente grandes. Esto implica que vamos a tener otro tipo de fragmentación, llamada &lt;strong&gt;fragmentación externa&lt;/strong&gt;, que igual que la interna hace &lt;strong&gt;la memoria residente sea mayor a la esperada&lt;/strong&gt;. En conjunto este efecto es llamado &lt;strong&gt;malloc overhead&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevpsc97gcqbgmxsi360z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevpsc97gcqbgmxsi360z.png" alt="Image description" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vamos con las preguntas que seguro os han surgido ahora....&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿En que afecta esta fragmentación al dimensionamiento de mi contenedor? &lt;strong&gt;Debo tenerlo en cuenta. Según mis observaciones, esta fragmentación puede oscilar entre un 10% y un 30%, pero hay que medirla empíricamente, y aplicar el factor de corrección al límite del contenedor.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;¿La fragmentación afecta a todos los espacios? &lt;strong&gt;Sí. Pero el caso del heap es algo diferente. Como hemos visto en el artículo &lt;a href="https://dev.to/gobe/java-dark-memory-heap-space-49l4"&gt;Java Dark Memory: Heap Space&lt;/a&gt;, los GCs modernos son capaces de compactar esta zona, manteniendo al mínimo la fragmentación interna. Si además hacemos un PreTouch del heap e igualamos el start al max, nos aseguramos que el mmap() utilizado para el heap va a ser una gran zona de memoria contigua, eliminando la fragmentación externa de este espacio. Es decir, que el factor de corrección lo podemos aplicar sólo a la suma de los espacios off-heap&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Esto es todo, o hay más... &lt;strong&gt;Si miras la barra de scroll, verás que el artículo continúa... sí, hay más memoria oscura a tener en cuenta...&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Tenemos que hablar de buffers. Direct Memory Allocation &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;En Java, &lt;strong&gt;NIO&lt;/strong&gt; (&lt;em&gt;non-blocking&lt;/em&gt;) es una &lt;strong&gt;colección de APIs&lt;/strong&gt; alrededor de la clase &lt;code&gt;java.nio.Buffer&lt;/code&gt;, que en su conjunto permiten el tratamiento de &lt;strong&gt;arrays de datos&lt;/strong&gt;, y su transmisión por &lt;strong&gt;canales de entrada/salida&lt;/strong&gt; de todo tipo (&lt;code&gt;package java.nio.channels&lt;/code&gt;), como peticiones web o manejo de ficheros. En general, es utilizada para enviar un array (buffer) que reside en memoria a un socket de salida, o al revés para leer un canal de entrada a memoria.&lt;/p&gt;

&lt;p&gt;Este API es de suma importancia ya que aporta una abstracción de &lt;strong&gt;alto rendimiento en operaciones I/O&lt;/strong&gt; (entrada-salida). Y es masivamente utilizada en &lt;strong&gt;Java&lt;/strong&gt;, por ejemplo en:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La &lt;strong&gt;propia JDK&lt;/strong&gt; de forma interna. Y el API está disponible para developers, claro.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frameworks&lt;/strong&gt; como Spring Boot y Quarkus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka&lt;/strong&gt; usa masivamente NIO en productores y consumidores&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netty&lt;/strong&gt;, que es un framework que permite la gestión asíncrona de operaciones I/O, utilizando NIO en su core. Netty suele ser utilizado a su vez por otros frameworks o librerías, aunque un developer también puede hacer uso directo de él, pero esto es menos habitual.&lt;/li&gt;
&lt;li&gt;Y muchos más. Prácticamente &lt;strong&gt;cualquier librería o framework que realice operaciones I/O&lt;/strong&gt;, usará NIO directa o indirectamente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No vamos a profundizar en las entrañas de este API. Lo importante para nuestro problema es que está relacionado con la memoria oscura. Vamos a tener &lt;em&gt;Buffers&lt;/em&gt; en nuestras aplicaciones, muchos, aunque no los creemos nosotros directamente. Y debemos entender cómo afectan al uso de la memoria. Como adelanto, diremos que muchos de estos buffers, &lt;strong&gt;se alojan en la zona off-heap&lt;/strong&gt;, pero no en ninguno de los espacios conocidos y descritos hasta aquí. Vamos paso a paso.&lt;br&gt;
Primero veremos los &lt;strong&gt;tipos de buffers principales de NIO&lt;/strong&gt;, son tres:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct Buffers&lt;/strong&gt; (&lt;code&gt;class DirectByteBuffer&lt;/code&gt;): áreas de memoria nativa off-heap reservadas con malloc. Son un gran aliado ya que permiten a los threads realizar operaciones I/O directamente de la memoria a los canales/sockets, pero &lt;strong&gt;ocupan su propio espacio de memoria en la Arenas de glibc&lt;/strong&gt;, de las que hablaremos a continuación. De momento sólo diremos que la memoria física de estos buffers, una vez comiteada, &lt;strong&gt;nunca es devuelta al sistema operativo&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-Direct Buffers&lt;/strong&gt; (&lt;code&gt;class ByteBuffer&lt;/code&gt;): son arrays de bytes que &lt;strong&gt;están en heap&lt;/strong&gt;. Cualquier array que creemos directa o indirectamente a través de algún helper (&lt;code&gt;new byte[]&lt;/code&gt;) es un buffer on-heap. El problema es que &lt;strong&gt;no se pueden utilizar directamente en operaciones I/O&lt;/strong&gt;. Si nuestro array lo queremos escribir en un canal de salida, un fichero, un socket, una &lt;em&gt;response&lt;/em&gt; de una &lt;em&gt;request&lt;/em&gt; http, etc. lo que hace la &lt;strong&gt;JVM es primero "copiarlo" a un DirectBuffer en off-heap&lt;/strong&gt; para poder realizar la operación de lectura o escritura del canal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory Mapped Buffers&lt;/strong&gt; (&lt;code&gt;class MappedByteBuffer&lt;/code&gt;): similares a los direct buffers, pero alojados con &lt;em&gt;mmap&lt;/em&gt;, no con &lt;em&gt;malloc&lt;/em&gt; en &lt;strong&gt;Arenas&lt;/strong&gt;. Son regiones de &lt;strong&gt;memoria para manejo de ficheros&lt;/strong&gt;, directamente mapeadas en memoria. Es un API realmente eficiente. No suponen un problema de memoria oscura, ya que una vez finalizado su uso, toda la región mmap() &lt;strong&gt;es devuelta al sistema operativo&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seguro que os asaltan mil dudas ahora mismo, todas relacionadas en cómo afectan estos buffers realmente al dimensionamiento de la memoria. En el siguiente apartado vamos a ver en conjunto el problema, el uso de espacios y arenas, y después vamos a tratar de solucionarlo.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Threads, Buffers y Arenas &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Vamos a ver una secuencia de un proceso Java en ejecución.&lt;br&gt;
Supongamos que a nuestra aplicación que expone un endpoint REST/HTTP, llega una petición. Pasarán muchas cosas dentro de los espacios grandes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Se creará mi &lt;strong&gt;thread y el stack&lt;/strong&gt; correspondiente en el &lt;strong&gt;espacio de threads&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Los &lt;strong&gt;objetos&lt;/strong&gt; que cree durante la ejecución irán naturalmente al &lt;strong&gt;espacio heap&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Si las &lt;strong&gt;classes&lt;/strong&gt; de esos objetos no estaban cargadas ya, se crearán en el &lt;strong&gt;Metaspace&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;JIT compiler&lt;/strong&gt; compilará y optimizará los métodos utilizados durante la ejecución, por lo que se usará el &lt;strong&gt;espacio Code&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y más:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;En los &lt;strong&gt;espacios pequeños&lt;/strong&gt; se comiteará memoria utilizada por &lt;strong&gt;GC, NMT,&lt;/strong&gt; los String irán a &lt;strong&gt;Symbol&lt;/strong&gt;, etc.&lt;/li&gt;
&lt;li&gt;Y si además manejo algún &lt;strong&gt;fichero&lt;/strong&gt; con &lt;em&gt;mapped files&lt;/em&gt;, en &lt;strong&gt;off-heap&lt;/strong&gt; se creará una reserva con mmap(), &lt;strong&gt;fuera de los espacios visibles con NMT&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm26otd3q333cl1na1y62.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm26otd3q333cl1na1y62.png" alt="Image description" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vale, hasta aquí todo más o menos controlado, pero ¿y los direct buffers?&lt;/p&gt;

&lt;p&gt;Ahora mi &lt;em&gt;thread&lt;/em&gt; empezará a necesitar usar Direct Buffers. Y no necesariamente porque estén en mi código. La propia JDK, y los frameworks y librerías los crearán por mí:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Para &lt;strong&gt;leer del socket&lt;/strong&gt; de entrada la &lt;strong&gt;request&lt;/strong&gt; HTTP &lt;strong&gt;a memoria&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Para &lt;strong&gt;escribir la response&lt;/strong&gt; HTTP de memoria al &lt;strong&gt;canal de salida&lt;/strong&gt; del socket.&lt;/li&gt;
&lt;li&gt;Si he generado algún &lt;strong&gt;evento&lt;/strong&gt; a un topic &lt;em&gt;Kafka&lt;/em&gt; como parte del procesamiento.&lt;/li&gt;
&lt;li&gt;Si escribo o leo de algún &lt;strong&gt;fichero&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Si llamo de forma síncrona a otro servicio HTTP durante mi ejecución, esa request-response debe ser copiada del socket a memoria.&lt;/li&gt;
&lt;li&gt;Y muchos más...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;¿Y dónde se alojan esos buffers? Veamos la secuencia:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cada vez que se crea un buffer, la JVM utilizará el system allocator del sistema operativo, &lt;strong&gt;glibc&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Este, tomará decisiones. Si es el &lt;strong&gt;primer buffer&lt;/strong&gt; creado en todo el proceso, con una llamada a &lt;em&gt;malloc()-&amp;gt;mmap()&lt;/em&gt; va a crear una &lt;strong&gt;reserva virtual de un área llamada Arena&lt;/strong&gt;. &lt;strong&gt;Dicha arena tiene una reserva de 64MB&lt;/strong&gt;. Una vez creada, dentro de la Arena hay una estructura interna de punteros enlazados para comitear "pequeños" bloques con llamadas &lt;em&gt;brk()&lt;/em&gt; y &lt;em&gt;mprotect()&lt;/em&gt;. Ahí vivirán nuestros buffers. En Arenas, en listas enlazadas que saben donde empieza, donde termina y hasta donde está ocupado un buffer (&lt;em&gt;offset&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Si la arena ya existe&lt;/strong&gt;, glibc utilizará de nuevo &lt;em&gt;malloc()-&amp;gt;brk()-mprotect()&lt;/em&gt; para buscar un "&lt;strong&gt;hueco libre"&lt;/strong&gt; para alojar el buffer.&lt;/li&gt;
&lt;li&gt;Si el buffer es grande (decisión tomada por glibc), podría tomar la decisión de no alojarlo en una Arena, sino crear un nuevo mmap para gestionar sólo ese buffer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Una vez terminada la ejecución:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tendremos una &lt;strong&gt;Arena&lt;/strong&gt;, un nuevo espacio off-heap que habrá comiteado (&lt;em&gt;memoria residente&lt;/em&gt;) la cantidad necesaria para los buffers.&lt;/li&gt;
&lt;li&gt;Esta memoria ocupada &lt;strong&gt;nunca es devuelta al sistema operativo&lt;/strong&gt;. Los "bloques" dentro de la arena se marcarán como "libres" para ser reutilizados, pero la memoria comiteada, aunque no sea usada, nunca se libera.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcov6xlka3smmrp37iz7y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcov6xlka3smmrp37iz7y.png" alt="Image description" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pero claro, nuestro servicio va a recibir &lt;strong&gt;más peticiones&lt;/strong&gt;, no sólo una...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;En una carga concurrente, &lt;em&gt;glibc&lt;/em&gt;, para favorecer el &lt;strong&gt;procesamiento en paralelo y el rendimiento&lt;/strong&gt;, puede decidir &lt;strong&gt;abrir más arenas&lt;/strong&gt;. &lt;strong&gt;Dos threads concurrentes pueden trabajar con dos Arenas diferentes&lt;/strong&gt;. Y si hay más threads, pues más Arenas.&lt;/li&gt;
&lt;li&gt;Cada Arena maneja los "bloques" libres u ocupados como una lista enlazada. Si necesito un bloque de 4KB para un buffer, pero todos los "bloques libres" son más pequeños, abriré otro bloque (&lt;em&gt;commit RSS&lt;/em&gt;). &lt;strong&gt;Expuesto a larga duración&lt;/strong&gt;, pasarán lo siguiente: &lt;strong&gt;habrá mucha fragmentación interna&lt;/strong&gt;, es decir, aunque realmente solo necesite, pongamos por ejemplo, 4MB de direct buffers, probablemente glibc haya comiteado el doble o el triple a la Arena, en ese juego de búsqueda de bloques libres por tamaño. Y por último, &lt;strong&gt;la Arena terminará expandiéndose hasta ese límite de 64MB&lt;/strong&gt; altamente fragmentado. Y en ese momento, &lt;strong&gt;glibc abrirá otra arena&lt;/strong&gt;, otra reserva de 64MB, que expuesta a larga duración, acabará llenándose... ¿se ve el problema verdad?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Atentos ahora:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El número total de Arenas por defecto es de 8 x número visible de CPU cores&lt;/strong&gt;. Si a mi contenedor le he dado &lt;strong&gt;3 cores&lt;/strong&gt;, tendré potencialmente 24 Arenas, cada una de ellas con un tamaño máximo de 64MB. &lt;strong&gt;Y tarde o temprano, esas arenas se llenarán&lt;/strong&gt;, se expandirán hasta su límite. Así que... &lt;strong&gt;necesito 24 x 64MB = 1536MB!!!!! Un giga y medio.&lt;/strong&gt; Sí, es cierto.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Así que, si nuestro servicio está expuesto a carga sostenida en el tiempo, vamos a tener una &lt;strong&gt;línea ascendente de memoria residente/comiteada&lt;/strong&gt;, con un claro patrón de &lt;strong&gt;memory leak&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Una breve búsqueda en internet nos permite ver la dimensión del problema, y también sentir que no estamos solos. Hay cientos, miles de issues, post, blogs hablando del tema:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flkhxa7n4hxkyl6smflv1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flkhxa7n4hxkyl6smflv1.png" alt="Image description" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;¿¡Y que puedo hacer!?&lt;br&gt;
Vamos con las soluciones.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Soluciones. Taming the beast &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Para empezar, os dejo una &lt;strong&gt;ficha resumen&lt;/strong&gt;, y después hablamos de cada ajuste o solución:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztpj53l6eu1z1tdlp3yk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztpj53l6eu1z1tdlp3yk.png" alt="Image description" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Activa NMT&lt;/strong&gt; para poder ver lo que pasa con los espacios de memoria.&lt;/li&gt;
&lt;li&gt;En primer lugar, especifica un &lt;strong&gt;valor máximo para todos los espacios grandes&lt;/strong&gt;: &lt;em&gt;heap, code, class y threads&lt;/em&gt;. &lt;strong&gt;Mide en carga sostenida&lt;/strong&gt; como se van llenando &lt;strong&gt;con NMT&lt;/strong&gt; y encuentra en el valor máximo. Puedes ver en los artículos &lt;a href="https://dev.to/gobe/java-dark-memory-heap-space-49l4"&gt;Java Dark Memory: Heap Space&lt;/a&gt;, &lt;a href="https://dev.to/gobe/java-dark-memory-class-space-1f0k"&gt;Java Dark Memory: Class Space&lt;/a&gt;, &lt;a href="https://dev.to/gobe/java-dark-memory-code-space-4c97"&gt;Java Dark Memory: Code Space&lt;/a&gt;, &lt;a href="https://dev.to/gobe/java-dark-memory-thread-space-1o2"&gt;Java Dark Memory: Thread Space&lt;/a&gt; como hacerlo con precisión. El que más te costará es el espacio de threads, porque tendrás que jugar con properties específicas de los frameworks que utilices: Undertow, Jetty, Netty...&lt;/li&gt;
&lt;li&gt;Para los &lt;strong&gt;espacios pequeños&lt;/strong&gt;, &lt;strong&gt;mide&lt;/strong&gt; como se expanden con NMT. Anota la &lt;strong&gt;suma total&lt;/strong&gt; de todas ellas.&lt;/li&gt;
&lt;li&gt;Y para todos los espacios, grandes y pequeños, aplica un &lt;strong&gt;factor para la fragmentación&lt;/strong&gt; externa e interna de los mismos. Podrá oscilar entre el 10 y el 30% aproximadamente.&lt;/li&gt;
&lt;li&gt;Establece &lt;strong&gt;límites a la CPU&lt;/strong&gt;. Como hemos visto en &lt;a href="https://dev.to/gobe/java-dark-memory-code-space-4c97"&gt;Java Dark Memory: Code Space&lt;/a&gt;, el hecho de tener muchos cores disponibles hará que JIT tienda a abrir muchos más threads que consumen grandes cantidades de memoria para la compilación y optimización de código. Otros frameworks que seguro que utilizas, también toman &lt;strong&gt;decisiones en base a los cores visibles&lt;/strong&gt;, que tienden a aumentar el consumo de memoria. Un valor de 2 ó 3 como máximo debería ser suficiente.&lt;/li&gt;
&lt;li&gt;No te fíes del flag &lt;code&gt;UseContainerSupport&lt;/code&gt; en contenedores. Aunque su nombre parece indicar que es una ayuda para ajustar precisamente valores de arranque y flags (java &lt;em&gt;ergonomics&lt;/em&gt;), &lt;strong&gt;su utilidad es prácticamente nula&lt;/strong&gt;. De hecho, dependiendo de la implementación, suele ajustar tamaños de heap en base al límite de memoria del contenedor, muchas veces el 50%, lo que como ya hemos visto es excesivo en aplicaciones stateless y microservicios. Por defecto está activada. Puedes desactivarla con &lt;code&gt;-XX:-UseContainerSupport&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Para el heap utiliza &lt;code&gt;AlwaysPreTouch&lt;/code&gt; e iguala el &lt;em&gt;start&lt;/em&gt; al &lt;em&gt;max&lt;/em&gt; (Xms=Xms), para conseguir &lt;strong&gt;mínima fragmentación en el heap&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usa &lt;code&gt;ExplicitGCInvokesConcurrent&lt;/code&gt;&lt;/strong&gt;. En muchos artículos sobre buenas prácticas se recomienda desactivarlo, para evitar llamadas directas de un developer a &lt;code&gt;System.gc()&lt;/code&gt;. Esto permite, por código, disparar el full GC. Si está desactivado, esto tiene consecuencias. Si mi proceso necesita alojar un direct buffer, y este excede el tamaño máximo establecido para estos direct buffers con &lt;code&gt;MaxDirectMemorySize&lt;/code&gt;, aunque haya espacio disponible en las arenas, no podrá realizarse la reserva de memoria. Entonces el GC, va a llamar explícitamente a Symtem.gc() para buscar en el heap referencias no utilizadas a objetos DirectBuffer, y recolectarlos. Una vez hecho, se hace un &lt;code&gt;Thread.sleep()&lt;/code&gt;, y se vuelve a intentar la reserva de memoria para el buffer. Si desactivamos esta opción, podemos tener errores de runtime al no poder alojar memoria (aunque haya memoria física disponible por debajo del límite). La probabilidad del error es proporcional al tamaño del heap.  Así que &lt;strong&gt;nunca lo desactives&lt;/strong&gt; con &lt;code&gt;-XX:+DisableExplicitGC&lt;/code&gt;, y &lt;strong&gt;actívalo explícitamente&lt;/strong&gt; con &lt;code&gt;-XX:+ExplicitGCInvokesConcurrent&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limita los threads de JIT compiler&lt;/strong&gt; con &lt;code&gt;-XX:CICompilerCount&lt;/code&gt;. Una valor de 2 es recomendable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limita el tamaño máximo de Direct Buffers&lt;/strong&gt; con &lt;code&gt;MaxDirectMemorySize&lt;/code&gt;. Con esta opción, podemos poner un límite a la cantidad de memoria reservada para todos los direct buffers. Pero ojo, &lt;strong&gt;este límite no va a impedir que se produzca la expansión de las arenas&lt;/strong&gt;. Lo que pasa cuando se supera este límite, es que el GC va a intentar liberar referencias no usadas, lo que va a hacer que en las Arenas, los "bloques" se marquen como "libres", pero no se devuelve la memoria física residente al sistema operativo. &lt;strong&gt;Este flag ayudará a que la fragmentación de las arenas sea menor&lt;/strong&gt;, y que la expansión de la Arena hasta su máximo de 64MB sea más lenta, lo que es una ayuda. Pero no implica que si pongo 20MB de límite, la arena no se vaya a expandir más allá de esos 20MB. La fragmentación siempre ocurrirá. El valor por tanto de este flag debe ser menor a la suma de todas la arenas. Si es mayor, se producirán errores. Mi recomendación es que sea del 50% de estas. Por ejemplo, si tengo 2 Arenas, es decir 128MB, pon un límite de 64MB para MaxDirectMemorySize. De esta forma "favorecemos" la expansión lenta de la arenas reduciendo la fragmentación, y contenemos posibles &lt;em&gt;OOMKilled&lt;/em&gt;. Puedes establecer este límite con &lt;code&gt;-XX:MaxDirectMemorySize&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;La &lt;strong&gt;propiedad del sistema&lt;/strong&gt; &lt;code&gt;jdk.nio.maxCachedBufferSize&lt;/code&gt; permite para &lt;strong&gt;limitar&lt;/strong&gt; la memoria utilizada por la &lt;strong&gt;caché temporal de direct buffers&lt;/strong&gt;. Esta caché es una caché por thread de memoria directa utilizada por la implementación de NIO para soportar aplicaciones que realizan I/O con buffers creados por arrays en el heap de Java. El valor de esta propiedad indica la capacidad máxima de un buffer directo que se puede almacenar en caché. Si no se establece la propiedad, no se limita el tamaño de los búferes almacenados en caché. No es estrictamente necesario poner este límite si usamos MaxDirectMemorySize, ya que esta última ya establece un límite global. Pero es &lt;strong&gt;muy recomendable&lt;/strong&gt;, ya que si no lo ponemos, la caché de buffers en cada thread se puede "comer" todo el espacio de direct buffers y producir &lt;strong&gt;memory leaks&lt;/strong&gt;, ya que necesitamos espacio para otros buffers utilizados por mi aplicación. Nuestra recomendación es que poner un valor de 256KB, y observar el comportamiento. Un valor muy pequeño puede tener impacto negativo en el rendimiento de la aplicación. Un valor grande o ilimitado, puede producir OOMKilled. Puedes establecer el valor con &lt;code&gt;-Djdk.nio.maxCachedBufferSize&lt;/code&gt; (es una system property, no un flag de tipo &lt;code&gt;XX&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Establece la propiedad&lt;/strong&gt; &lt;code&gt;io.netty.maxDirectMemory&lt;/code&gt; del framework Netty (utilizado habitualmente como dependencia de otros frameworks) &lt;strong&gt;a valor 0 (cero)&lt;/strong&gt;. De esta manera, se "fuerza" a Netty a utilizar el espacio de buffers directos propio de la JDK, y respetar sus límites (&lt;code&gt;MaxDirectMemorySize&lt;/code&gt;). Un valor negativo, en la práctica significa que Netty se reserva un espacio de buffers que es dos veces mayor a MaxDirectMemorySize. Un valor positivo fija el espacio de buffers utilizado por Netty. Es una system property, así que la puedes establecer con &lt;code&gt;-Dio.netty.maxDirectMemory&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utiliza la variable de entorno&lt;/strong&gt; &lt;code&gt;MALLOC_ARENA_MAX&lt;/code&gt; para limitar el número de Arenas. Recomendamos utilizar 2 ó 4. Ten en cuenta que cada arena supone 64MB off-heap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utiliza la variable de entorno&lt;/strong&gt; &lt;code&gt;MALLOC_MMAP_THRESHOLD_&lt;/code&gt; (sí, con el guion bajo al final). Antes hemos mencionado que para reservar memoria para un buffer, malloc puede decidir si hace un &lt;em&gt;mprotect()&lt;/em&gt; dentro de una arena (más eficiente en rendimiento, pero favorece la fragmentación) o un mmap (menor rendimiento, es más costoso en CPU y tiempo, pero produce menos fragmentación). Precisamente este valor &lt;em&gt;MALLOC_MMAP_THRESHOLD&lt;/em&gt;_ es el usado por &lt;em&gt;glibc&lt;/em&gt; para decidir cuando hacer una cosa o la otra. Si no se establece, este valor es por defecto 128KB, pero ojo, no es un valor fijo, glibc puede decidir subir o bajar el threshold según sus algoritmos internos. Si fijamos un valor, este ya no será cambiado por glibc. No recomendamos valores "grandes" ya que producirán mucha fragmentación interna. Los valores bajos mantienen a raya la fragmentación, pero con un coste (más mmap). Mide y prueba en tu aplicación lo que sea más óptimo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  13. Soluciones alternativas &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Llevamos un buen rato hablando del problema de la fragmentación (malloc overhead) inherente a glibc. Existen alternativas, que permiten sustituir el system allocator por defecto que es glibc, vamos a destacar dos de ellas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;jemalloc&lt;/strong&gt;: es una implementación de malloc centrada en evitar la fragmentación y maximizar la concurrencia. Se dice que garantiza que la fragmentación no supera el 20%, aunque es un dato difícil de probar. Muy conocido y utilizado por la comunidad. Requiere un esfuerzo extra, que es compilarlo y meterlo en la imagen base de nuestros contenedores. Una vez compilado, con la variable de entorno LD_PRELOAD=/usr/local/lib/libjemalloc.so, sustituimos el malloc por defecto de glibc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;tcmalloc&lt;/strong&gt;: otra alternativa a la implementación por defecto malloc, esta vez de Google, que se centra en maximizar el rendimiento de los alojamientos de memoria, y también en prevenir la fragmentación.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;async-profiler&lt;/strong&gt;: no es una alternativa a malloc como las anteriores. Es un profiler creado entre otros por mi admirado Andrei Pangin. Es el único profiler existente que permite rastrear allocations nativas (off-heap) y detección de leaks. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cabe mencionar que jemalloc, la alternativa a malloc mencionada arriba tiene también su propio profiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. La fórmula para calcular la memoria&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;¡Vamos con la fórmula final!&lt;/p&gt;

&lt;p&gt;Pero antes, un último espacio de memoria. El último, de verdad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Java Native Interface&lt;/strong&gt; permite a los programadores escribir &lt;em&gt;métodos nativos en lenguaje C&lt;/em&gt; para manejar situaciones en las que una aplicación no puede escribirse completamente en el lenguaje de programación Java.&lt;br&gt;
Por ejemplo, si tenemos una biblioteca escrita en C o C++, podemos generar binarios &lt;code&gt;.os&lt;/code&gt; y llamarlo desde Java.&lt;br&gt;
La JVM en sí, está escrita en C y utiliza librerías nativas.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No se puede monitorizar o medir&lt;/strong&gt;, salvo con el profiler de jemalloc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No se puede limitar&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debemos tenerla en cuenta&lt;/strong&gt; para ajustar el tamaño de nuestros contenedores&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6thk376z61jkgzrdd5ll.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6thk376z61jkgzrdd5ll.png" alt="Image description" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Y ahora sí, la fórmula final. Vale su peso en oro:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwp1sccp6proa63puej5n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwp1sccp6proa63puej5n.png" alt="Image description" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Si, ya sé. Esta fórmula tiene un problema de base. Hay cosas que no podemos medir con métricas exactas ni siquiera con NMT, por lo que la fórmula no nos vale directamente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;malloc overhead&lt;/li&gt;
&lt;li&gt;arenas&lt;/li&gt;
&lt;li&gt;mapped files&lt;/li&gt;
&lt;li&gt;JNI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;¿Y entonces cómo lo hacemos? Pues le damos la vuelta a la fórmula.&lt;br&gt;
Lo más práctico es utilizar métricas que nos permitan medir la memoria residente de nuestro proceso, incluidas todas las zonas oscuras.&lt;/p&gt;

&lt;p&gt;Para ello, en un entorno &lt;strong&gt;Kubernetes&lt;/strong&gt;, disponemos de dos &lt;strong&gt;métricas prometheus&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;container_memory_rss&lt;/code&gt; = CMRSS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;container_memory_working_set_bytes&lt;/code&gt; = CMWSB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hay sutiles diferencias entre ellas y el tipo de memoria que miden, pero no entraremos en ese debate. En la práctica su valor es idéntico el 99% del tiempo, así que &lt;strong&gt;las podemos considerar iguales&lt;/strong&gt; en términos de nuestros cálculos. &lt;strong&gt;Y en nuestro cálculo estas métricas nos dan el valor total de la memoria residente (RSS)&lt;/strong&gt;, en decir la memoria total utilizada.&lt;/p&gt;

&lt;p&gt;Y lo importante de estas dos métricas, es que &lt;strong&gt;si su valor supera el límite del contenedor&lt;/strong&gt;, &lt;strong&gt;¡tendremos un OOM Killed!&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Así que podemos &lt;em&gt;"despejar"&lt;/em&gt; en nuestra fórmula la variable de la memoria oscura:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxl0ltuwheple229avjpm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxl0ltuwheple229avjpm.png" alt="Image description" width="800" height="132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Conociendo este valor, podemos fijar con bastante precisión los límites de nuestro contenedor para evitar &lt;em&gt;OOM Killers&lt;/em&gt;, y desbordamientos en cada uno de los espacios.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Conclusiones finales&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;En nuestras aplicaciones Java en contenedores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ajustar los límites de los procesos Java en contenedores no es un trabajo trivial&lt;/strong&gt; y hay que dedicarle el tiempo suficiente.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Es necesario &lt;strong&gt;medir y observar&lt;/strong&gt;, sacar conclusiones, &lt;strong&gt;ajustar y volver a medir y observar&lt;/strong&gt;, &lt;strong&gt;hasta que las pruebas de carga ofrezcan valores estables para todos los espacios&lt;/strong&gt;, incluida la memoria oscura.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;En algunos casos será necesario investigar en profundidad para encontrar las causas de un leak o un comportamiento no esperado.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Con paciencia, perseverancia y algo de suerte, &lt;strong&gt;podremos crear tallas de contenedores predeterminadas&lt;/strong&gt; para nuestros servicios Java.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Espero que este artículo os haya servido de ayuda, ese es el principal objetivo de recopilar por escrito la experiencia aculada estos años. Muchas gracias por llegar hasta aquí, le hemos dedicado mucho tiempo y cariño. Sé que el artículo ha quedado un poco largo, aunque a veces pienso que ha quedado corto en muchos aspectos en los que se puede profundizar aún mucho más. Seguramente continuaremos la serie con algún artículo extra...&lt;/p&gt;

&lt;p&gt;Si te ha servido de ayuda (o no), o simplemente quieres preguntar o abrir debate sobre algún punto, por favor deja tus comentarios a continuación. ¡Gracias y hasta la próxima!&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Referencias y herramientas&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr007.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr007.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/java/javase/22/core/java-nio.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/en/java/javase/22/core/java-nio.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Java_memory_model#:%7E:text=The%20Java%20Memory%20Model%20(JMM,consistent%20and%20reliable%20Java%20applications" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Java_memory_model#:~:text=The%20Java%20Memory%20Model%20(JMM,consistent%20and%20reliable%20Java%20applications&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jcp.org/en/jsr/detail?id=133" rel="noopener noreferrer"&gt;https://jcp.org/en/jsr/detail?id=133&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://netty.io/" rel="noopener noreferrer"&gt;https://netty.io/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/spring-apps/basic-standard/concepts-for-java-memory-management" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/spring-apps/basic-standard/concepts-for-java-memory-management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.alibabacloud.com/blog/598081" rel="noopener noreferrer"&gt;https://www.alibabacloud.com/blog/598081&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://technology.blog.gov.uk/2015/12/11/using-jemalloc-to-get-to-the-bottom-of-a-memory-leak/" rel="noopener noreferrer"&gt;https://technology.blog.gov.uk/2015/12/11/using-jemalloc-to-get-to-the-bottom-of-a-memory-leak/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.redhat.com/articles/2021/09/09/how-jvm-uses-and-allocates-memory#" rel="noopener noreferrer"&gt;https://developers.redhat.com/articles/2021/09/09/how-jvm-uses-and-allocates-memory#&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jeffgriffith/native-jvm-leaks" rel="noopener noreferrer"&gt;https://github.com/jeffgriffith/native-jvm-leaks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.arkey.fr/2020/11/30/off-heap-reconnaissance/" rel="noopener noreferrer"&gt;https://blog.arkey.fr/2020/11/30/off-heap-reconnaissance/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/libc/manual/html_node/Malloc-Tunable-Parameters.html" rel="noopener noreferrer"&gt;https://www.gnu.org/software/libc/manual/html_node/Malloc-Tunable-Parameters.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bric3/java-pmap-inspector" rel="noopener noreferrer"&gt;https://github.com/bric3/java-pmap-inspector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@daniyal.hass/how-glibc-memory-handling-affects-java-applications-the-hidden-cost-of-fragmentation-8e666ee6e000" rel="noopener noreferrer"&gt;https://medium.com/@daniyal.hass/how-glibc-memory-handling-affects-java-applications-the-hidden-cost-of-fragmentation-8e666ee6e000&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/c755fFv1Rnk" rel="noopener noreferrer"&gt;https://youtu.be/c755fFv1Rnk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/async-profiler/async-profiler" rel="noopener noreferrer"&gt;https://github.com/async-profiler/async-profiler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jemalloc.net/" rel="noopener noreferrer"&gt;https://jemalloc.net/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/google/tcmalloc" rel="noopener noreferrer"&gt;https://github.com/google/tcmalloc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>kubernetes</category>
      <category>containers</category>
      <category>memory</category>
    </item>
    <item>
      <title>Java Dark Memory: Class Space</title>
      <dc:creator>Raúl González</dc:creator>
      <pubDate>Wed, 02 Apr 2025 04:33:23 +0000</pubDate>
      <link>https://dev.to/gobe/java-dark-memory-class-space-1f0k</link>
      <guid>https://dev.to/gobe/java-dark-memory-class-space-1f0k</guid>
      <description>&lt;p&gt;Este artículo es un anexo del artículo principal &lt;strong&gt;&lt;a href="https://dev.to/gobe/java-dark-memory-okh"&gt;Java Dark Memory&lt;/a&gt;&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Índice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1.  ¿Qué es?&lt;/li&gt;
&lt;li&gt;2.  Observabilidad&lt;/li&gt;
&lt;li&gt;3.  Límites&lt;/li&gt;
&lt;li&gt;4.  Consideraciones&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. ¿Qué es? &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Este espacio, más conocido como Metaspace almacena meta-información de las clases (classloading)&lt;br&gt;
Su espacio depende de la cantidad de clases cargadas, puede rondar entre los 150-250MB.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Representación interna de una Java class&lt;/li&gt;
&lt;li&gt;Métodos con su bytecode&lt;/li&gt;
&lt;li&gt;Descriptores de campos (fields)&lt;/li&gt;
&lt;li&gt;Pools de constantes&lt;/li&gt;
&lt;li&gt;Symbols (constantes, strings, descriptores)&lt;/li&gt;
&lt;li&gt;Anotaciones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Este espacio está dividido en dos áreas: Metaspace y Compressed Class Space.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fup23mtd3zk5prv3y0v2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fup23mtd3zk5prv3y0v2e.png" alt="Image description" width="250" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Observabilidad &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Con NMT podemos ver la cantidad de memoria virtual y residente/comiteada de todo el espacio.&lt;/li&gt;
&lt;li&gt;Con JMX y Mbeans podemos consultar información sobre este espacio y su estado.&lt;/li&gt;
&lt;li&gt;Con métricas Prometheus, como por ejemplo las emitidas por SpringBoot:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;jvm_memory_&amp;lt;commited|used|max&amp;gt;_bytes {area=“nonheap”, id=“Metaspace|Compressed Class Space”}&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Con herramientas como Memory Analyzer (MAT), basadas en Eclipse, podemos obtener información detallada tras un HeapDump o un CoreDump.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fziai4acsvyxk75ssqygj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fziai4acsvyxk75ssqygj.png" alt="Image description" width="717" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Límites &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;A continuación podemos ver los diferentes flags de la JVM disponibles para este espacio:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21mj2armjaqv5r63y2cl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21mj2armjaqv5r63y2cl.png" alt="Image description" width="407" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Consideraciones &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;El tamaño de esta espacio sólo puede determinarse tras la observación en runtime. Debemos observar y medir en carga sostenida y ver el espacio máximo que necesita nuetra aplicación para poder por nuestro límite.&lt;/li&gt;
&lt;li&gt;Muchos frameworks modernos “crean” clases en runtime, lo que puede complicar el ajuste&lt;/li&gt;
&lt;li&gt;Es aconsejable dejar un margen "holgado" para este espacio. Los frameworks modernos crean código en runtime, classes, interfaces y proxies que irán llenando este espacio.&lt;/li&gt;
&lt;li&gt;Si el espacio está limitado (max) y no se puede alojar una nueva clase, se lanzará una RuntimeException &lt;code&gt;java.lang.OutOfMemoryError: Metaspace/Compressed Class Space&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>kubernetes</category>
      <category>containers</category>
      <category>memory</category>
    </item>
    <item>
      <title>Java Dark Memory: Code Space</title>
      <dc:creator>Raúl González</dc:creator>
      <pubDate>Wed, 02 Apr 2025 04:32:57 +0000</pubDate>
      <link>https://dev.to/gobe/java-dark-memory-code-space-4c97</link>
      <guid>https://dev.to/gobe/java-dark-memory-code-space-4c97</guid>
      <description>&lt;p&gt;Este artículo es un anexo del artículo principal &lt;strong&gt;&lt;a href="https://dev.to/gobe/java-dark-memory-okh"&gt;Java Dark Memory&lt;/a&gt;&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Índice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1.  ¿Qué es?&lt;/li&gt;
&lt;li&gt;2.  Observabilidad&lt;/li&gt;
&lt;li&gt;3.  Límites&lt;/li&gt;
&lt;li&gt;4.  Consideraciones&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. ¿Qué es? &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Espacio utilizado para la compilación e interpretación de código.&lt;br&gt;
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):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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&lt;/li&gt;
&lt;li&gt;Estas tareas se hacen en Threads separados a los de aplicación, sin interferir o bloquear la ejecución&lt;/li&gt;
&lt;li&gt;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)&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para almacenar este código interpretado, mejorado, compilado a nativo y sustituido, internamente JIT maneja diferentes subespacios dentro del code space:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2sa93n5imf8k2zq5ugy0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2sa93n5imf8k2zq5ugy0.png" alt="Image description" width="241" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;non-method&lt;/strong&gt;: non-method code, compiler buffers y bytecode interpreters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;profiled&lt;/strong&gt;: métodos ligeramente perfilados y optimizados, con tiempo corto de vida&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;non-profiled&lt;/strong&gt;: métodos completamente optimizados con tiempo de vida largo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JIT se divide en dos compiladores C1 y C2&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C1&lt;/strong&gt; realiza optimizaciones en tres niveles de profundidad, dejando los resultados en su &lt;em&gt;layer&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C2&lt;/strong&gt; compila a código nativo, dejando el resultado en &lt;em&gt;layer 4&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Figng16b6f0ek9fh1cjsx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Figng16b6f0ek9fh1cjsx.png" alt="Image description" width="255" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Observabilidad &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Con &lt;strong&gt;NMT&lt;/strong&gt; podemos ver la cantidad de memoria virtual y residente/comiteada de todo el espacio.&lt;/li&gt;
&lt;li&gt;Con JMX y Mbeans.&lt;/li&gt;
&lt;li&gt;Con métricas Prometheus: por ejemplo, Spring Boot ofrece la siguiente métrica:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;jvm_memory_&amp;lt;commited|used|max&amp;gt;_bytes {area=“nonheap”, id=“CodeHeap ‘profiled methods’|’non profiled methods’|’non-methods’”}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8blns2hvrpkkvf3q128y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8blns2hvrpkkvf3q128y.png" alt="Image description" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Límites &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Valor por defecto (depende de la JVM): 240MB&lt;/li&gt;
&lt;li&gt;Valor start: &lt;code&gt;-XX:InitialCodeCacheSize&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Valor max: &lt;code&gt;-XX:ReservedCodeCacheSize&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si acotamos demasiado el espacio, podemos llegar a ver este mensaje de advertencia de la la JVM en runtime:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;VM warning: CodeCache is full. The compiler has been disabled&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Consideraciones &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No debemos despreciar el espacio utilizado por JIT.&lt;/li&gt;
&lt;li&gt;Podemos aproximar un espacio entre un 10-30% de la off-heap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl25sxe01x3awhgtpvgvx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl25sxe01x3awhgtpvgvx.png" alt="Image description" width="571" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Limitar el número de threads concurrentes, tiene un impacto directo en la memoria del proceso, veamos un ejemplo.&lt;/p&gt;

&lt;p&gt;¿En que se consume la memoria de microservicio spring-boot (uno sencillo, de tipo web con un endpoint REST, en la fase de arranque?&lt;/p&gt;

&lt;p&gt;Activando el profiler de jemalloc, este el diagrama que obtenemos:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frnssfijoa5n9s7huhjnz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frnssfijoa5n9s7huhjnz.png" alt="Image description" width="800" height="1223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Analizando el diagrama, vemos un nodo relacionado con el compiler C2 de JIT, que se lleva el 43% del alojamiento de memoria:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fusjii8tppunigoup7777.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fusjii8tppunigoup7777.png" alt="Image description" width="430" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Es decir, que del total de los 320MB aproximadamente de memoria residente de la secuencia de arranque, 150MB son de JIT compiler!&lt;/p&gt;

&lt;p&gt;Utilizando el flag &lt;code&gt;-XX:CICompilerCount=2&lt;/code&gt;, obtenemos una reducción de unos 120MB en este proceso de arranque lo cual no es nada despreciable.&lt;/p&gt;

&lt;p&gt;Así que limita la CPU de los contenedores como buena práctica, siempre. Y opcionalmente, usa el flag &lt;code&gt;-XX:CICompilerCount&lt;/code&gt; 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. &lt;/p&gt;

</description>
      <category>java</category>
      <category>kubernetes</category>
      <category>containers</category>
      <category>memory</category>
    </item>
    <item>
      <title>Java Dark Memory: Heap Space</title>
      <dc:creator>Jose A. Beltran Marquez</dc:creator>
      <pubDate>Wed, 02 Apr 2025 04:32:15 +0000</pubDate>
      <link>https://dev.to/gobe/java-dark-memory-heap-space-49l4</link>
      <guid>https://dev.to/gobe/java-dark-memory-heap-space-49l4</guid>
      <description>&lt;p&gt;Este artículo es un anexo del artículo principal &lt;strong&gt;&lt;a href="https://dev.to/gobe/java-dark-memory-okh"&gt;Java Dark Memory&lt;/a&gt;&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Índice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1.  ¿Qué es?&lt;/li&gt;
&lt;li&gt;2.  Observabilidad&lt;/li&gt;
&lt;li&gt;3.  Límites&lt;/li&gt;
&lt;li&gt;4.  Consideraciones&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. ¿Qué es? &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;El Heap es la zona de memoria que almacena los objetos que se van creando en tiempo de ejecución y que es gestionada por el garbage collector.&lt;/p&gt;

&lt;p&gt;El heap se divide en 3 areas principales: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68jt39tottwv59vryqx7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68jt39tottwv59vryqx7.png" alt="Image description" width="334" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Young Generation&lt;/strong&gt;: es el destino inicial para los nuevos objetos. Cuando los objetos son usados en el código estos se situan en un subespacio dentro de este area que se llama Eden. &lt;br&gt;
Cuando esta subzona se llena, hay un proceso de recolección menor que pasa objetos de esta zona a otra subsección llamada S0 dentro de la zona Survivor (dividida en S0 y S1). Los objetos que ya no estan referenciados serán desalojados por este mecanismo. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Old Generation&lt;/strong&gt;: esta zona podemos definirla como "el repositorio" de los objetos de larga duración. Básicamente aqui estarán los objetos que han pasado ya por varios ciclos de GC de la Young Generation. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Permanent Generation&lt;/strong&gt;: en esta zona NO estarán almacenados los objetos procedentes de la old generation. Esta zona no funciona así. &lt;br&gt;
Esta zona contiene metadatos de clases y metodos de la aplicación en tiempo de ejecución. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Observabilidad &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Con JMX y Mbeans, podemos ver el estado de cada unos de los espacios generacionales&lt;/li&gt;
&lt;li&gt;Con NMT podemos ver la memoria virtual y residente de todo el espacio&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Con métricas prometheus: por ejemplo spring boot ofrece la siguiente métrica:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jvm_memory_&amp;lt;commited|used|max&amp;gt;_bytes{area="heap"}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5vcn12arlyvtbumn53jy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5vcn12arlyvtbumn53jy.png" alt="Image description" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Con herramientas como Eclipse Memory Analyzer (MAT), podemos analizar en detalle todos los objetos alojados en los espacios generacionales y los motivos de su retención, para ello debemos gererar antes un HeapDump.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Límites &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Valor &lt;strong&gt;Start&lt;/strong&gt;: &lt;code&gt;-Xms&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Valor &lt;strong&gt;Max&lt;/strong&gt;: &lt;code&gt;-Xmx&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Consideraciones &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;En microservicios ocupará entre un 15% y un 25%, en stateful “hay que medirlos”.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Un heap salubable, expuesto a larga duración, gráficamente ofrecerá un patrón de "sierra", lo que indica que los objetos pueden ser recolectados por el GC manteniendo el heap en niveles óptimos de ocupación&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ouhefdodd52beym8nxf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ouhefdodd52beym8nxf.png" alt="Image description" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un heap no saludable, con patrón de escalera, es un idicador de memory leak. Los objetos se acumulan en la zona de tenured, y el GC no es capaz de recolectarlos. Se producirá una &lt;em&gt;RuntimeException&lt;/em&gt; &lt;code&gt;java.lang.OutOfMemoryError: Java Heap Space&lt;/code&gt;, cuando el GC no pueda desalojar y used=max.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F905zrqb4rbws6wi1si27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F905zrqb4rbws6wi1si27.png" alt="Image description" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Podemos configurar el tipo y algoritmos de Garbage Collection, y es recomendable hacerlo. Si tenemos heap pequeños y CPU limita en un proceso, openjdk tomará decisiones (java ergonomics) sobre el GC y el heap. En muchas ocasiones esta decisión es utilizar el GC Parallel o pero aún, el Serial. Recomendamos siempre utilizar G1, aunque el heap sea pequeño. Aunque "gaste" un poco más de memoria en su propio espacio, a la larga es mucho más óptimo, y eficaz en la recolección de objetos relacionados con la memoria oscura y los direct buffers, que son el tema principal del artículo. Además G1 es capaz de realizar compactación del heap, reduciendo la fragmentación interna del espacio al mínimo (lo que también ahorra memoria residente).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>memory</category>
      <category>jdk</category>
      <category>productivity</category>
      <category>containers</category>
    </item>
    <item>
      <title>Java Dark Memory: Thread Space</title>
      <dc:creator>Raúl González</dc:creator>
      <pubDate>Wed, 02 Apr 2025 04:30:21 +0000</pubDate>
      <link>https://dev.to/gobe/java-dark-memory-thread-space-1o2</link>
      <guid>https://dev.to/gobe/java-dark-memory-thread-space-1o2</guid>
      <description>&lt;p&gt;Este artículo es un anexo del artículo principal &lt;strong&gt;&lt;a href="https://dev.to/gobe/java-dark-memory-okh"&gt;Java Dark Memory&lt;/a&gt;&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Índice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1.  ¿Qué es?&lt;/li&gt;
&lt;li&gt;2.  Observabilidad&lt;/li&gt;
&lt;li&gt;3.  Límites&lt;/li&gt;
&lt;li&gt;4.  Consideraciones&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. ¿Qué es? &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Espacio de memoria&lt;/strong&gt; utilizado por la JVM guardar información de la ejecución de cada uno de los &lt;strong&gt;hilos creados por un proceso&lt;/strong&gt; java:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Variables locales o intermedias&lt;/li&gt;
&lt;li&gt;Punteros a métodos llamados&lt;/li&gt;
&lt;li&gt;Pila de ejecución: el stack del thread&lt;/li&gt;
&lt;li&gt;ThreadLocal variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cada hilo tiene su propio &lt;strong&gt;Stack&lt;/strong&gt; y la propia JVM utiliza hilos para su ejecución, como mínimo el hilo pricipal o &lt;code&gt;main&lt;/code&gt;.&lt;br&gt;
Es habitual que los frameworks y librerías utilizadas creen pools de threads para su reutilización a través del API ThreadExecutor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwbukwr443xvo8iqv9op3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwbukwr443xvo8iqv9op3.png" alt="Image description" width="470" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Observabilidad &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Con &lt;strong&gt;NMT&lt;/strong&gt; podemos ver la cantidad de memoria virtual y residente/comiteada de todo el espacio, es decir, la suma de lo que ocupan todos nuestros en ejecución en un momento dado.&lt;/li&gt;
&lt;li&gt;Con JMX y Mbeans podemos consultar información sobre pools y su estado.&lt;/li&gt;
&lt;li&gt;Con metricas Prometheus, podemos ver el número total de hilos, aunque no la memoria utilizada.&lt;/li&gt;
&lt;li&gt;Con herramientas como Memory Analyzer (MAT), basadas en Eclipse, podemos obtener información detallada tras un HeapDump o un CoreDump.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Límites &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Se puede establecer el tamaño máximo del stack por hilo, a través del flag &lt;code&gt;-Xss&lt;/code&gt;. Por defecto este valor es de 1MB, que el tamaño máximo que alcanzará un stack individual.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sin embargo no se puede establecer máximo número de hilos ni tamaño máximo de esta zona, a través de flags de la JVM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pro sí podemos mediante configuración (properties) de cada framework o librería, establecer el tamaño máximo de los diferentes pools de threads, de modo que podemos calcular el límite máximo, sabiendo que cada stack ocupará 1MB.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Por ejemplo, si estamos creando una aplicación web con Spring Boot, probablemente utilizaremos Undertow como implementación non-blocking de HTTPServer y ServletServer. La documentación de Undertow nos permite estas dos configuraciones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;server.undertow.threads.io: Número de I/O threads a crear. El valor por defecto es derivado del número de CPUs disponibles..&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;server.undertow.threads.worker: Número de worker threads. El valor por defecto es 8 veces el número de I/O threads.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Consideraciones &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Como vemos con más detalle en el artículo principal Java Dark Memory, los DirectBuffers utilizados por los threads y la caché por thread de buffers (bufferpool), introducen una problemática adicional, reservando memoria fuera del espacio de threads y los stacks, en zonas de difícil observabilidad. La relación entre direct buffers y threads es uno de los principales problemas de la memoria oscura.&lt;/p&gt;

&lt;p&gt;Es vital conocer cuántol pools de threads utilizan las aplicaciones que creemos. Para ello debemos estudiar los frameworks y librerías que utilizamos directa o indirectamente, para poder configurar y limitar los tamaños de estos pools. La suma total de los pools nos indicará el tamaño total de este espacio.&lt;/p&gt;

</description>
      <category>java</category>
      <category>kubernetes</category>
      <category>containers</category>
      <category>memory</category>
    </item>
  </channel>
</rss>
