Tablica bajtów Java o wielkości 1 MB lub więcej zajmuje dwa razy więcej pamięci RAM

14

Uruchomienie poniższego kodu w systemie Windows 10 / OpenJDK 11.0.4_x64 powoduje wygenerowanie danych wyjściowych used: 197i expected usage: 200. Oznacza to, że 200 bajtów tablic z milionem elementów zajmuje około. 200 MB pamięci RAM. Wszystko w porządku.

Kiedy zmienię przydział tablicy bajtów w kodzie z new byte[1000000]na new byte[1048576](to znaczy na 1024 * 1024 elementów), to generuje jako dane wyjściowe used: 417i expected usage: 200. Co za cholera?

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

Patrząc nieco głębiej w visualvm, w pierwszym przypadku widzę wszystko zgodnie z oczekiwaniami:

tablice bajtów zajmują 200 MB

W drugim przypadku oprócz tablic bajtowych widzę tę samą liczbę tablic int zajmujących taką samą ilość pamięci RAM, jak tablice bajtów:

Macierze zajmują dodatkowe 200 MB

Nawiasem mówiąc, te tablice int nie pokazują, że są do nich odniesienia, ale nie mogę ich wyrzucać do śmieci ... (Tablice bajtów pokazują dobrze, gdzie są odniesienia).

Jakieś pomysły, co się tutaj dzieje?

Georg
źródło
Spróbuj zmienić dane z ArrayList <bajt []> na bajt [bloki] [], aw swojej pętli for: dane [i] = nowy bajt [1000000], aby wyeliminować zależności od wewnętrznych elementów ArrayList
jalynn2
Czy może mieć to coś wspólnego z wewnętrzną maszyną JVM za pomocą int[]polecenia emulacji dużej byte[]dla lepszej lokalizacji przestrzennej?
Jacob G.
@JacobG. zdecydowanie wygląda na coś wewnętrznego, ale wydaje się, że nie ma żadnych wskazówek w przewodniku .
Kayaman
Tylko dwie obserwacje: 1. Jeśli odejmiesz 16 od 1024 * 1024, wydaje się, że działa zgodnie z oczekiwaniami. 2. Zachowanie z jdk8 wydaje się inne niż to, co można tutaj zaobserwować.
drugi
@ sekunda Tak, magicznym ograniczeniem jest oczywiście to, czy tablica zajmuje 1 MB pamięci RAM, czy nie. Zakładam, że jeśli odejmiesz tylko 1, pamięć zostanie uzupełniona w celu zwiększenia wydajności środowiska wykonawczego i / lub narzutów związanych z zarządzaniem dla tablicy liczy się do 1 MB ... Zabawne, że JDK8 zachowuje się inaczej!
Georg

Odpowiedzi:

9

Opisuje to natychmiastowe zachowanie modułu śmieciowego G1, który zwykle domyślnie przyjmuje 1 MB „regionów” i stał się domyślnym JVM w Javie 9. Uruchamianie z włączonymi innymi GC daje różne liczby.

każdy obiekt, który jest większy niż połowa wielkości regionu, jest uważany za „humongous” ... W przypadku obiektów, które są tylko nieco większe niż wielokrotność rozmiaru regionu stosu, to niewykorzystane miejsce może spowodować fragmentację stosu.

Pobiegłem java -Xmx300M -XX:+PrintGCDetailsi pokazuje, że kupa jest wyczerpana przez ogromne regiony:

[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

Chcemy, aby nasz 1MiB byte[]był „mniejszy niż połowa wielkości regionu G1”, więc dodanie -XX:G1HeapRegionSize=4Mdaje funkcjonalną aplikację:

[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

Szczegółowy przegląd G1: https://www.oracle.com/technical-resources/articles/java/g1gc.html

Szczegóły kruszenia G1: https://docs.oracle.com/en/java/javase/13/gctuning/garbage-first-garbage-collector-tuning.html#GUID-2428DA90-B93D-48E6-B336-A849ADF1C552

drekbour
źródło
Mam te same problemy z szeregowym GC iz długą tablicą, która zajmuje 8 MB (i było w porządku z rozmiarem 1024-1024-2), a zmiana G1HeapRegionSize nic nie zrobiła w moim przypadku
GotoFinal
Nie jestem tego pewien. Czy możesz wyjaśnić używane wywołanie Java i wynik powyższego kodu za pomocą długiego []
drekbour
@GotoFinal, nie widzę żadnego problemu, który nie został wyjaśniony powyżej. Przetestowałem kod, za pomocą long[1024*1024]którego podaje się oczekiwane użycie 1600 M Z G1, zmieniając się o -XX:G1HeapRegionSize[Użyto 1M: 1887, użyto 2M: 2097, użyto 4M: 3358, użyto 8M: 3358, użyto 16M: 3363, użyto 32M: 1682]. Z -XX:+UseConcMarkSweepGCstosować: 1687. Z -XX:+UseZGCstosować: 2105. Z -XX:+UseSerialGCstosować: 1698
drekbour
gist.github.com/c0a4d0c7cfb335ea9401848a6470e816 po prostu taki kod, bez zmiany jakichkolwiek opcji GC, wydrukuje, used: 417 expected usage: 400ale jeśli go usunę,-2 zmieni się na used: 470około 50 MB, a 50 * 2 długości to zdecydowanie mniej niż 50 MB
GotoFinal
1
Ta sama rzecz. Różnica wynosi ~ 50 MB i masz 50 „ogromnych” bloków. Oto szczegół 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->400Dowodzi, że te dwie ostatnie długości zmuszają G1 do przydzielenia kolejnego regionu 1 MB tylko do przechowywania 16 bajtów.
drekbour