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 массивы занимают дополнительные 200 МБ

Кстати, эти int-массивы не показывают, что на них ссылаются, но я не могу собрать их ... (Байт-массивы прекрасно показывают, где на них ссылаются.)

Есть идеи, что здесь происходит?

Georg
источник
Попробуйте изменить данные из ArrayList <byte []> на byte [blocks] [] и в цикле for: data [i] = new byte [1000000], чтобы устранить зависимости от внутренних компонентов ArrayList
jalynn2
Может ли это иметь какое-то отношение к внутренней виртуальной машине Java, использующей int[]для эмуляции большой объект byte[]для лучшей пространственной локализации?
Джейкоб Дж.
@JacobG. это определенно выглядит чем-то внутренним, но в руководстве нет никаких указаний .
Каяман
Всего два наблюдения: 1. Если вы вычтете 16 из 1024 * 1024, то, кажется, работает как ожидалось. 2. Поведение с jdk8, кажется, отличается от того, что можно наблюдать здесь.
Второе
@ секунды Да, магический предел, очевидно, состоит в том, занимает ли массив 1 МБ ОЗУ или нет. Я предполагаю, что если вычесть только 1, то память будет заполнена для эффективности времени выполнения и / или издержки на управление для массива будут равны 1 МБ ... Забавно, что JDK8 ведет себя по-другому!
Георг

Ответы:

9

Это описывает готовое поведение сборщика мусора G1, который по умолчанию имеет значение «регионы» 1 МБ и становится Java по умолчанию в Java 9. Запуск с другими включенными GC дает различные числа.

любой объект, размер которого превышает половину области, считается «огромным» ... Для объектов, которые немного превышают размер области кучи, это неиспользуемое пространство может привести к фрагментации кучи.

Я побежал, 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

drekbour
источник
У меня те же проблемы с последовательным GC и с длинным массивом, который занимает 8 МБ (и был в порядке с размером 1024-1024-2), и изменение G1HeapRegionSize ничего не сделало в моем случае
GotoFinal
Мне неясно по этому поводу. Можете ли вы уточнить используемый вызов Java и вывод приведенного выше кода с помощью long []
drekbour
@GotoFinal, я не вижу проблем, не объясненных выше. Я протестировал код, long[1024*1024]который дает ожидаемое использование 1600M с G1, изменяясь на -XX:G1HeapRegionSize[1M используется: 1887, 2M используется: 2097, 4M используется: 3358, 8M используется: 3358, 16M используется: 3363, 32M используется: 1682]. С -XX:+UseConcMarkSweepGCиспользуется: 1687. С -XX:+UseZGCиспользуется: 2105. С -XX:+UseSerialGCиспользуется: 1698
drekbour
gist.github.com/c0a4d0c7cfb335ea9401848a6470e816 просто код, подобный этому, без изменения каких-либо опций GC, он будет печатать, used: 417 expected usage: 400но если я уберу -2его , он изменится used: 470примерно до 50 МБ, а 50 * 2 long определенно намного меньше, чем 50 МБ
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