Ein Java-Byte-Array von 1 MB oder mehr belegt das Doppelte des Arbeitsspeichers

14

Das Ausführen des folgenden Codes unter Windows 10 / OpenJDK 11.0.4_x64 erzeugt als Ausgabe used: 197und expected usage: 200. Dies bedeutet, dass 200-Byte-Arrays mit einer Million Elementen ca. 200 MB RAM. Alles ist gut.

Wenn ich die Byte-Array-Zuordnung im Code von new byte[1000000]auf new byte[1048576](dh auf 1024 * 1024 Elemente) ändere , wird als Ausgabe used: 417und ausgegeben expected usage: 200. Was zum Teufel?

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

Wenn ich mit visualvm etwas tiefer schaue, sehe ich im ersten Fall alles wie erwartet:

Byte-Arrays belegen 200 MB

Im zweiten Fall sehe ich zusätzlich zu den Byte-Arrays die gleiche Anzahl von int-Arrays, die dieselbe Menge an RAM belegen wie die Byte-Arrays:

Int-Arrays belegen zusätzliche 200 MB

Diese int-Arrays zeigen übrigens nicht, dass auf sie verwiesen wird, aber ich kann sie nicht mit Müll sammeln ... (Die Byte-Arrays zeigen genau, wo sie referenziert werden.)

Irgendwelche Ideen, was hier passiert?

Georg
quelle
Versuchen Sie, Daten von ArrayList <Byte []> in Byte [Blöcke] [] und in Ihrer for-Schleife zu ändern: data [i] = neues Byte [1000000], um Abhängigkeiten von den Interna von ArrayList zu beseitigen
jalynn2
Könnte es etwas damit zu tun haben, dass die JVM intern int[]eine große emuliert, um eine byte[]bessere räumliche Lokalität zu erreichen?
Jacob G.
@ JacobG. es sieht definitiv etwas internes aus, aber es scheint keinen Hinweis in der Anleitung zu geben .
Kayaman
Nur zwei Beobachtungen: 1. Wenn Sie 16 von 1024 * 1024 subtrahieren, scheint dies wie erwartet zu funktionieren. 2. Das Verhalten mit einem jdk8 scheint anders zu sein als das, was hier beobachtet werden kann.
2.
@second Ja, die magische Grenze ist offensichtlich, ob das Array 1 MB RAM belegt oder nicht. Ich gehe davon aus, dass, wenn Sie nur 1 subtrahieren, der Speicher für die Laufzeiteffizienz aufgefüllt wird und / oder der Verwaltungsaufwand für das Array auf 1 MB zählt ... Komisch, dass sich JDK8 anders verhält!
Georg

Antworten:

9

Dies beschreibt das Out-of-the-Box-Verhalten des G1-Garbage Collectors, das normalerweise standardmäßig 1 MB "Regionen" verwendet und in Java 9 zu einem JVM-Standard wird. Wenn andere GCs aktiviert sind, ergeben sich unterschiedliche Zahlen.

Jedes Objekt, das mehr als eine halbe Regionsgröße hat, wird als "riesig" betrachtet. Bei Objekten, die nur geringfügig größer als ein Vielfaches der Größe der Heap-Region sind, kann dieser nicht verwendete Speicherplatz dazu führen, dass der Heap fragmentiert wird.

Ich bin gelaufen java -Xmx300M -XX:+PrintGCDetailsund es zeigt, dass der Haufen von riesigen Regionen erschöpft ist:

[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

Wir möchten, dass unser 1MiB byte[]"weniger als die Hälfte der Größe der G1-Region" beträgt, sodass das Hinzufügen -XX:G1HeapRegionSize=4Meine funktionale Anwendung bietet:

[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

Ausführliche Übersicht über G1: https://www.oracle.com/technical-resources/articles/java/g1gc.html

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

drekbour
quelle
Ich habe die gleichen Probleme mit seriellem GC und mit langem Array, das 8 MB benötigt (und mit Größe 1024-1024-2 in Ordnung war), und das Ändern von G1HeapRegionSize hat in meinem Fall nichts
bewirkt
Ich bin mir nicht sicher. Können Sie den verwendeten Java-Aufruf und die Ausgabe des obigen Codes mit einem langen []
drekbour
@GotoFinal, ich beobachte kein Problem, das oben nicht erklärt wurde. Ich habe den Code getestet, mit long[1024*1024]dem eine erwartete Verwendung von 1600M mit G1 angegeben wird, variierend um -XX:G1HeapRegionSize[1M verwendet: 1887, 2M verwendet: 2097, 4M verwendet: 3358, 8M verwendet: 3358, 16M verwendet: 3363, 32M verwendet: 1682]. Mit -XX:+UseConcMarkSweepGCgebraucht: 1687. Mit -XX:+UseZGCgebraucht: 2105. Mit -XX:+UseSerialGCgebraucht: 1698
drekbour
gist.github.com/c0a4d0c7cfb335ea9401848a6470e816 Code einfach so, ohne die GC-Optionen zu ändern, wird er gedruckt, used: 417 expected usage: 400aber wenn ich ihn entferne, -2ändert er sich auf used: 470ungefähr 50 MB, und 50 * 2 Longs sind definitiv viel weniger als 50 MB
GotoFinal
1
Gleiche Sache. Der Unterschied beträgt ~ 50 MB, und Sie haben 50 "humongous" Blöcke. Hier ist das GC-Detail: 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->400Es beweist, dass die letzten beiden Longs G1 zwingen, eine weitere 1-MB-Region zuzuweisen, nur um 16 Bytes in zu speichern.
drekbour