Le tableau d'octets Java de 1 Mo ou plus occupe deux fois plus de RAM

14

L'exécution du code ci-dessous sur Windows 10 / OpenJDK 11.0.4_x64 produit en sortie used: 197et expected usage: 200. Cela signifie que des tableaux de 200 octets d'un million d'éléments occupent env. 200 Mo de RAM. Tout va bien.

Lorsque je modifie l'allocation du tableau d'octets dans le code de new byte[1000000]à new byte[1048576](c'est-à-dire à 1024 * 1024 éléments), il produit en sortie used: 417et expected usage: 200. Que diable?

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

En regardant un peu plus en profondeur avec visualvm, je vois dans le premier cas tout comme prévu:

les tableaux d'octets occupent 200 Mo

Dans le deuxième cas, en plus des tableaux d'octets, je vois le même nombre de tableaux int qui occupent la même quantité de RAM que les tableaux d'octets:

les baies int prennent 200 Mo supplémentaires

Soit dit en passant, ces tableaux int ne montrent pas qu'ils sont référencés, mais je ne peux pas les récupérer ... (Les tableaux d'octets montrent très bien où ils sont référencés.)

Des idées ce qui se passe ici?

Georg
la source
Essayez de changer les données d'ArrayList <byte []> en byte [blocks] [], et dans votre boucle for: data [i] = new byte [1000000] pour éliminer les dépendances sur les
composants
Cela pourrait-il avoir quelque chose à voir avec la machine virtuelle Java en utilisant un int[]pour émuler un grand byte[]pour une meilleure localité spatiale?
Jacob G.
@JacobG. il semble définitivement quelque chose interne, mais il ne semble pas y avoir d'indication dans le guide .
Kayaman
Juste deux observations: 1. Si vous soustrayez 16 à 1024 * 1024, cela semble fonctionner comme prévu. 2. Le comportement avec un jdk8 semble être différent de ce qui peut être observé ici.
deuxième
@second Oui, la limite magique est évidemment de savoir si la baie occupe 1 Mo de RAM ou non. Je suppose que si vous soustrayez juste 1, la mémoire est remplie pour l'efficacité d'exécution et / ou la surcharge de gestion pour le tableau compte jusqu'à 1 Mo ... C'est drôle que JDK8 se comporte différemment!
Georg

Réponses:

9

Ce que cela décrit, c'est le comportement prêt à l' emploi du garbage collector G1 qui par défaut est généralement de 1 Mo "régions" et est devenu une valeur par défaut JVM dans Java 9. L'exécution avec d'autres GC activés donne des nombres variables.

tout objet dont la taille est supérieure à la moitié d'une région est considéré comme "gigantesque" ... Pour les objets qui sont juste légèrement plus grands qu'un multiple de la taille de la région du tas, cet espace inutilisé peut entraîner la fragmentation du tas.

J'ai couru java -Xmx300M -XX:+PrintGCDetailset cela montre que le tas est épuisé par des régions énormes:

[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

Nous voulons que notre 1 Mo byte[]soit "moins de la moitié de la taille de la région G1", donc l'ajout -XX:G1HeapRegionSize=4Mdonne une application fonctionnelle:

[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

Présentation détaillée de G1: https://www.oracle.com/technical-resources/articles/java/g1gc.html

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

drekbour
la source
J'ai les mêmes problèmes avec le GC série et avec un long tableau qui prend 8 Mo (et ça allait avec la taille 1024-1024-2) et changer G1HeapRegionSize n'a rien fait dans mon cas
GotoFinal
Je ne suis pas clair à ce sujet. Pouvez-vous clarifier l'invocation java utilisée et la sortie du code ci-dessus avec un long []
drekbour
@GotoFinal, je n'observe aucun problème non expliqué par ce qui précède. J'ai testé le code long[1024*1024]qui donne une utilisation prévue de 1600M avec G1, variant de -XX:G1HeapRegionSize[1M utilisé: 1887, 2M utilisé: 2097, 4M utilisé: 3358, 8M utilisé: 3358, 16M utilisé: 3363, 32M utilisé: 1682]. Avec -XX:+UseConcMarkSweepGCutilisé: 1687. Avec -XX:+UseZGCutilisé: 2105. Avec -XX:+UseSerialGCutilisé: 1698
drekbour
gist.github.com/c0a4d0c7cfb335ea9401848a6470e816 il suffit de coder comme ça, sans changer les options GC, il s'imprimera used: 417 expected usage: 400mais si je le supprime, -2il passera à used: 470environ 50 Mo, et 50 * 2 longs est certainement beaucoup moins que 50 Mo
GotoFinal
1
Même chose. La différence est de ~ 50 Mo et vous avez 50 blocs "gigantesques". Voici le détail du 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->400Cela prouve que ces deux derniers longs forcent G1 à allouer une autre région de 1 Mo juste pour stocker 16 octets.
drekbour