Un array di byte Java di almeno 1 MB occupa il doppio della RAM


14

L'esecuzione del codice seguente su Windows 10 / OpenJDK 11.0.4_x64 produce come output used: 197e expected usage: 200. Ciò significa che le matrici di 200 byte di un milione di elementi occupano circa. 200 MB di RAM. Tutto bene.

Quando cambio l'allocazione di array di byte nel codice da new byte[1000000]a new byte[1048576](ovvero a 1024 * 1024 elementi), produce come output used: 417e expected usage: 200. Che diamine?

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();
    }
}

Guardando un po 'più a fondo con visualvm, vedo nel primo caso tutto come previsto:

gli array di byte occupano 200 MB

Nel secondo caso, oltre agli array di byte, vedo lo stesso numero di array int che occupano la stessa quantità di RAM degli array di byte:

le matrici int occupano ulteriori 200mb

Questi array int, a proposito, non mostrano che sono referenziati, ma non riesco a raccoglierli nell'immondizia ... (Gli array di byte mostrano bene dove sono referenziati.)

Qualche idea su cosa sta succedendo qui?


Prova a cambiare i dati da ArrayList <byte []> in byte [blocchi] [] e nel tuo ciclo for: data [i] = nuovo byte [1000000] per eliminare le dipendenze dall'interno di ArrayList
jalynn2

Potrebbe avere qualcosa a che fare con la JVM internamente usando un int[]per emulare una grande byte[]località spaziale migliore?
Jacob G.

@JacobG. sembra sicuramente qualcosa di interno, ma non sembra esserci alcuna indicazione nella guida .
Kayaman,

Solo due osservazioni: 1. Se sottrai 16 da 1024 * 1024 sembra che funzioni come previsto. 2. Il comportamento con un jdk8 sembra essere diverso da quello che si può osservare qui.
secondo

Sì, il limite magico ovviamente è se l'array occupa 1 MB di RAM o meno. Suppongo che se si sottrae solo 1, la memoria viene riempita per l'efficienza di runtime e / o l'overhead di gestione per l'array conta per 1 MB ... Divertente che JDK8 si comporti in modo diverso!
Georg,

Risposte:


9

Ciò che descrive è il comportamento immediato del Garbage Collector G1, che per impostazione predefinita porta a "regioni" da 1 MB e diventa un predefinito JVM in Java 9. L'esecuzione con altri GC abilitati fornisce numeri variabili.

qualsiasi oggetto di dimensioni superiori alla metà di una regione viene considerato "enorme" ... Per oggetti leggermente più grandi di un multiplo della dimensione della regione dell'heap, questo spazio inutilizzato può causare la frammentazione dell'heap.

Ho corso java -Xmx300M -XX:+PrintGCDetailse mostra che il mucchio è esaurito da regioni gigantesche:

[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

Vogliamo che il nostro 1MiB byte[]sia "meno della metà della dimensione della regione G1", quindi l'aggiunta -XX:G1HeapRegionSize=4Mdà un'applicazione funzionale:

[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

Panoramica approfondita di G1: https://www.oracle.com/technical-resources/articles/java/g1gc.html

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


Ho gli stessi problemi con GC seriale e con array lungo che richiede 8 MB (e andava bene con le dimensioni 1024-1024-2) e la modifica di G1HeapRegionSize non ha fatto nulla nel mio caso
GotoFinal

Non sono chiaro su questo. Puoi chiarire l'invocazione Java utilizzata e l'output del codice sopra riportato con un lungo []
drekbour,

@GotoFinal, non osservo alcun problema non spiegato da quanto sopra. Ho testato il codice con long[1024*1024]cui si prevede un utilizzo previsto di 1600M Con G1, che varia di -XX:G1HeapRegionSize[1M usato: 1887, 2M usato: 2097, 4M usato: 3358, 8M usato: 3358, 16M usato: 3363, 32M usato: 1682]. Con -XX:+UseConcMarkSweepGCusato: 1687. Con -XX:+UseZGCusato: 2105. Con -XX:+UseSerialGCusato: 1698
drekbour il

gist.github.com/c0a4d0c7cfb335ea9401848a6470e816 solo codice del genere, senza modificare alcuna opzione GC verrà stampato used: 417 expected usage: 400ma se rimuoverò -2cambierò in used: 470modo che siano spariti circa 50 MB e che i long 50 * 2 siano decisamente inferiori a 50 MB
GotoFinal

1
Stessa cosa. La differenza è ~ 50 MB e hai 50 blocchi "giganteschi". Ecco il dettaglio 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->400Dimostra che questi ultimi due longs costringono G1 a allocare un'altra regione da 1 MB solo per memorizzare 16 byte in.
drekbour
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.