Un análisis técnico detalla por qué una llamada aparentemente inocente a Arrays.fill se ejecuta 265 veces más despacio bajo G1GC que bajo ParallelGC, a pesar de que el código Java, el JDK y la máquina son idénticos. La investigación, realizada con JMH y un perfilador a nivel de ensamblador (xctraceasm) sobre un Apple M4 Max con Corretto JDK 25, parte de un benchmark que llena dos arrays de objetos con un marcador y descarta de entrada la recolección de basura como causa, ya que la prueba no genera asignaciones.
El culpable es la barrera de escritura del GC, el conjunto de instrucciones que el JIT inserta tras cada asignación de referencia para mantener actualizado el card table (tabla de tarjetas) que el recolector generational usa para localizar punteros entre generaciones. Bajo ParallelGC, esa barrera se reduce a un par de instrucciones simples: un desplazamiento y un strb que marca la tarjeta como sucia. Bajo G1GC, sin embargo, la barrera es mucho más compleja e incluye, en su ruta lenta, una barrera StoreLoad (dmb ish), una valla de memoria completa que obliga a la CPU a esperar a que todas las escrituras pendientes sean visibles para todos los núcleos antes de continuar.
El artículo reconstruye el camino desde la observación inicial hasta el ensamblador ARM64 exacto que genera el JIT, explica paso a paso cómo se decodifican las instrucciones implicadas y aclara que la lógica es idéntica en x86-64 aunque las instrucciones difieran. También contextualiza la función del card table en los recolectores generacionales: sin este mecanismo, una colección del espacio joven tendría que escanear toda la generación vieja para encontrar referencias, anulando la ventaja de la recolección generacional.
