La matriz de bytes Java de 1 MB o más ocupa el doble de RAM

14

Ejecutar el siguiente código en Windows 10 / OpenJDK 11.0.4_x64 produce como salida used: 197y expected usage: 200. Esto significa que las matrices de 200 bytes de un millón de elementos ocupan aprox. 200 MB de RAM. Todo bien

Cuando cambio la asignación de la matriz de bytes en el código de new byte[1000000]a new byte[1048576](es decir, a 1024 * 1024 elementos), se produce como salida used: 417y expected usage: 200. ¿Que demonios?

import java.io.IOException;
import java.util.ArrayList;

public class Mem {
    private static Runtime rt = Runtime.getRuntime();
    private static long free() { return rt.maxMemory() - rt.totalMemory() + rt.freeMemory(); }
    public static void main(String[] args) throws InterruptedException, IOException {
        int blocks = 200;
        long initiallyFree = free();
        System.out.println("initially free: " + initiallyFree / 1000000);
        ArrayList<byte[]> data = new ArrayList<>();
        for (int n = 0; n < blocks; n++) { data.add(new byte[1000000]); }
        System.gc();
        Thread.sleep(2000);
        long remainingFree = free();
        System.out.println("remaining free: " + remainingFree / 1000000);
        System.out.println("used: " + (initiallyFree - remainingFree) / 1000000);
        System.out.println("expected usage: " + blocks);
        System.in.read();
    }
}

Mirando un poco más profundo con visualvm, veo en el primer caso todo como se esperaba:

las matrices de bytes ocupan 200mb

En el segundo caso, además de las matrices de bytes, veo el mismo número de matrices int que ocupan la misma cantidad de RAM que las matrices de bytes:

Las matrices int ocupan 200 mb adicionales

Estas matrices int, por cierto, no muestran que estén referenciadas, pero no puedo recolectarlas de forma basura ... (Las matrices de bytes muestran muy bien dónde están referenciadas).

¿Alguna idea de lo que está pasando aquí?

Georg
fuente
Intente cambiar los datos de ArrayList <byte []> a byte [blocks] [], y en su bucle for: data [i] = new byte [1000000] para eliminar las dependencias de las
partes
¿Podría tener algo que ver con la JVM internamente usando un int[]para emular una byte[]localidad espacial grande y mejor?
Jacob G.
@JacobG. definitivamente parece algo interno, pero no parece haber ninguna indicación en la guía .
Kayaman
Solo dos observaciones: 1. Si resta 16 de 1024 * 1024, parece que funciona como se esperaba. 2. El comportamiento con un jdk8 parece ser diferente de lo que se puede observar aquí.
segundo
@second Sí, el límite mágico obviamente es si la matriz ocupa 1 MB de RAM o no. Supongo que si resta solo 1, entonces la memoria está acolchada para la eficiencia del tiempo de ejecución y / o la sobrecarga de administración para la matriz cuenta hasta 1 MB ... ¡Es curioso que JDK8 se comporte de manera diferente!
Georg

Respuestas:

9

Lo que esto describe es el comportamiento listo para usar del recolector de basura G1, que por defecto suele ser "regiones" de 1 MB y se convirtió en un valor predeterminado de JVM en Java 9. La ejecución con otros GC habilitados proporciona números variables.

cualquier objeto que tenga más de la mitad del tamaño de una región se considera "enorme" ... Para los objetos que son un poco más grandes que un múltiplo del tamaño de la región del montón, este espacio no utilizado puede hacer que el montón se fragmente.

Corrí java -Xmx300M -XX:+PrintGCDetailsy muestra que el montón está agotado por las regiones gigantescas:

[0.202s][info   ][gc,heap        ] GC(51) Old regions: 1->1
[0.202s][info   ][gc,heap        ] GC(51) Archive regions: 2->2
[0.202s][info   ][gc,heap        ] GC(51) Humongous regions: 296->296
[0.202s][info   ][gc             ] GC(51) Pause Full (G1 Humongous Allocation) 297M->297M(300M) 1.935ms
[0.202s][info   ][gc,cpu         ] GC(51) User=0.01s Sys=0.00s Real=0.00s
...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

Queremos que nuestro 1MiB byte[]sea ​​"menos de la mitad del tamaño de la región G1", por lo que agregar -XX:G1HeapRegionSize=4Mofrece una aplicación funcional:

[0.161s][info   ][gc,heap        ] GC(19) Humongous regions: 0->0
[0.161s][info   ][gc,metaspace   ] GC(19) Metaspace: 320K->320K(1056768K)
[0.161s][info   ][gc             ] GC(19) Pause Full (System.gc()) 274M->204M(300M) 9.702ms
remaining free: 100
used: 209
expected usage: 200

Descripción detallada de G1: https://www.oracle.com/technical-resources/articles/java/g1gc.html

Detalle aplastante de G1: https://docs.oracle.com/en/java/javase/13/gctuning/garbage-first-garbage-collector-tuning.html#GUID-2428DA90-B93D-48E6-B336-A849ADF1C552

Drekbour
fuente
Tengo los mismos problemas con el GC serie y con una matriz larga que ocupa 8 MB (y estaba bien con el tamaño 1024-1024-2) y el cambio de G1HeapRegionSize no hizo nada en mi caso
GotoFinal
No estoy claro sobre esto. ¿Puede aclarar la invocación de Java utilizada y la salida del código anterior con un largo []
drekbour
@GotoFinal, no observo ningún problema no explicado por lo anterior. long[1024*1024]Probé el código con el que da un uso esperado de 1600M con G1, que varía según -XX:G1HeapRegionSize[1M usado: 1887, 2M usado: 2097, 4M usado: 3358, 8M usado: 3358, 16M usado: 3363, 32M usado: 1682]. Con -XX:+UseConcMarkSweepGCusado: 1687. Con -XX:+UseZGCusado: 2105. Con -XX:+UseSerialGCusado: 1698
drekbour
gist.github.com/c0a4d0c7cfb335ea9401848a6470e816 solo codifica así, sin cambiar ninguna opción de GC, se imprimirá, used: 417 expected usage: 400pero si lo elimino-2 , cambiará a used: 470unos 50 MB, y 50 * 2 largos definitivamente son mucho menos de 50 MB
GotoFinal
1
La misma cosa. La diferencia es de ~ 50 MB, y tiene 50 bloques "enormes". Aquí está el detalle del GC: 1024 * 1024 -> [0.297s][info ][gc,heap ] GC(18) Humongous regions: 450->4501024 * 1024-2 -> [0.292s][info ][gc,heap ] GC(20) Humongous regions: 400->400Prueba que esos dos últimos largos obligan a G1 a asignar otra región de 1 MB solo para almacenar 16 bytes.
drekbour