Байтовий масив Java 1 Мб або більше займає вдвічі більше оперативної пам'яті


14

Запуск наведеного нижче коду в Windows 10 / OpenJDK 11.0.4_x64 видає як вихід used: 197і expected usage: 200. Це означає, що 200 байтових масивів з мільйона елементів займають приблизно. 200 Мб оперативної пам’яті. Все добре.

Коли я змінюю розподіл байтового масиву в коді з new byte[1000000]на new byte[1048576](тобто на елементи 1024 * 1024), він видає як вихід used: 417і expected usage: 200. Якого біса?

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

Поглянувши трохи глибше на visualvm, я бачу в першому випадку все, як очікувалося:

байтові масиви займають 200 Мб

У другому випадку, окрім байтових масивів, я бачу таку ж кількість масивів int, що займають стільки ж оперативної пам’яті, що й байтові масиви:

int масиви займають додаткові 200mb

До речі, ці масиви int не показують, що на них посилаються, але я не можу збирати їх сміття ... (Байтові масиви добре показують, куди вони посилаються.)

Якісь ідеї, що тут відбувається?


Спробуйте змінити дані з ArrayList <байт []> на байт [блоки] [], а у вашому для циклу: data [i] = новий байт [1000000], щоб усунути залежності від внутрішніх даних ArrayList
jalynn2

Чи може це мати щось спільне з JVM внутрішньо, використовуючи int[]емуляцію великого byte[]для кращого просторового простору?
Яків Г.

@JacobG. це, безумовно, виглядає щось внутрішнє, але, схоже, в посібнику немає жодних вказівок .
Каяман

Лише два спостереження: 1. Якщо відняти 16 від 1024 * 1024, здається, працює так, як очікувалося. 2. Здається, поведінка з jdk8 відрізняється від того, що можна спостерігати тут.
другий

@second Так, магічна межа, очевидно, полягає в тому, чи має масив 1 Мб оперативної пам’яті чи ні. Я припускаю, що якщо ви підсумуєте лише 1, то пам’ять забита для ефективності виконання та / або накладні витрати для масиву зараховуються до 1 Мб ... Смішно, що JDK8 поводиться інакше!
Георг

Відповіді:


9

Це описує нестандартну поведінку сміттєзбірника G1, який зазвичай за замовчуванням розміщується на "регіонах" 1 Мб і став за замовчуванням JVM в Java 9. Запуск з іншими включеними ГК дає різну кількість.

будь-який об’єкт, розмір якого більше половини регіону, вважається "гумоногенним" ... Для об'єктів, які трохи більше кратного розміру області купи, цей невикористаний простір може призвести до фрагментації купи.

Я побіг, java -Xmx300M -XX:+PrintGCDetailsі це показує, що купа виснажує гумонні регіони:

[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

Ми хочемо, щоб наш 1MiB byte[]був "менше половини розміру регіону G1", тому додавання -XX:G1HeapRegionSize=4Mдає функціональне застосування:

[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

Поглиблений огляд G1: https://www.oracle.com/technical-resources/articles/java/g1gc.html

Подробиця G1: https://docs.oracle.com/en/java/javase/13/gctuning/garbage-first-garbage-collector-tuning.html#GUID-2428DA90-B93D-48E6-B336-A849ADF1C552


У мене такі ж проблеми із серійним GC та довгим масивом, який займає 8 МБ (і це було добре з розміром 1024-1024-2) та зміною G1HeapRegionSize у моєму випадку нічого не зробило
GotoFinal

Мені незрозуміло з цього приводу. Чи можете ви уточнити використане виклик Java та вивести вищезазначений код довгим []
drekbour

@GotoFinal, я не спостерігаю жодної проблеми, не поясненої вище. Я протестував код, за допомогою long[1024*1024]якого передбачено очікуване використання 1600M З G1, що змінюється -XX:G1HeapRegionSize[1M використано: 1887, 2М використано: 2097, 4М використано: 3358, 8М використано: 3358, 16М використано: 3363, 32М використано: 1682]. З -XX:+UseConcMarkSweepGCвикористаною: 1687. З -XX:+UseZGCвикористаною: 2105. З -XX:+UseSerialGCвикористаною: 1698
дрекбур

gist.github.com/c0a4d0c7cfb335ea9401848a6470e816 просто такий код, не змінюючи жодних параметрів GC, він буде надрукований, used: 417 expected usage: 400але якщо я видаляю, що -2він зміниться, used: 470так що приблизно 50MB більше не буде, і 50 * 2 довгих, безумовно, набагато менше 50MB
GotoFinal

1
Однакові речі. Різниця становить ~ 50 Мб, і у вас є 50 "гумористичних" блоків. Ось детальна інформація про 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->400Це доводить, що останні два довгі сили змушують G1 виділити ще одну область 1 МБ просто для зберігання 16 байт.
drekbour
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.