Высокое использование памяти с большим количеством объектов в Java

У меня есть приведенный ниже пример кода, который зацикливается на 50 миллионов раз и создает объекты MyDataHolder . MyDataHolder имеет два варианта: один на основе хэш-карты, а другой — на основе переменных. см. (MyDataHolder_map и MyDataHolder_variable)

public class MemTest {
    public static void main(String[] args) {
        List<MyDataHolder_map> list = new LinkedList<>();
        for (int i = 0; i < 50_000_000; i++) {
            if (i % 100_000==0) System.out.println(i);
            MyDataHolder_map d = new MyDataHolder_map();
            d.add("key1", "value1"+i);
            d.add("key2", "value2"+i);
            d.add("key3", "value3"+i);
            d.add("key4", "value4"+i);
            d.add("key5", "value5"+i);
            d.add("key6", "value6"+i);
            list.add(d);
        }
    }
}
// use Map to store data
class MyDataHolder_map {
    private Map<String, String> data = new HashMap<>(6);

    public void add(String key, String value) {
        data.put(key, value);
    }
}

// use variables
class MyDataHolder_variable {
    private String key1;
    private String key2;
    private String key3;
    private String key4;
    private String key5;
    private String key6;

    public void add(String key, String value) {
        switch (key) {
            case "key1" -> key1 = value;
            case "key2" -> key2 = value;
            case "key3" -> key3 = value;
            case "key4" -> key4 = value;
            case "key5" -> key5 = value;
            case "key6" -> key6 = value;
        }
    }
}

Я запускаю код с 32 ГБ оперативной памяти -Xmx32g -verbose:gc и замечаю, что переменная MyDataHolder выводит следующее в конце программы:

With Variable based MyDataHolder
[32.495s][info][gc] GC(34) Pause Young (Normal) (G1 Evacuation Pause) 24032M->24072M(31312M) 1259.276ms
[35.537s][info][gc] GC(27) Pause Remark 24128M->24128M(32768M) 2.067ms
[59.101s][info][gc] GC(27) Pause Cleanup 24128M->24128M(32768M) 1.132ms
[59.234s][info][gc] GC(27) Concurrent Mark Cycle 39850.818ms

Обратите внимание, что потребовалось 24128M, т.е. 24 ГБ ОЗУ. Однако с помощью Hashmap на основе MyDataHolder происходит сбой, и он печатается.

[81.370s][info][gc] GC(73) Pause Young (Normal) (G1 Preventive Collection) 32581M->32583M(32768M) 42.816ms
[81.458s][info][gc] GC(74) To-space exhausted
[81.458s][info][gc] GC(74) Pause Young (Concurrent Start) (G1 Preventive Collection) 32615M->32647M(32768M) 82.420ms
[81.458s][info][gc] GC(75) Concurrent Mark Cycle
[81.519s][info][gc] GC(76) To-space exhausted
[81.519s][info][gc] GC(76) Pause Young (Normal) (G1 Preventive Collection) 32663M->32679M(32768M) 52.343ms

Обратите внимание, что он использовал все 32 ГБ ОЗУ.

Вопрос:

  1. Почему программа занимает 24 ГБ с переменной версией, она должна быть намного меньше, поскольку общее количество уникальных строк в виртуальной машине составляет 6 ключей на итератор, т.е. 6 * 50M = 300M, а каждая строка имеет размер 7 байт, поэтому общее количество составляет 300M * 7Byte = 1,9 ГБ. Я понимаю, что существуют накладные расходы на ссылки и объекты, но они не могут превышать 20 ГБ. что происходит?
  2. Почему MyDataHolder на основе Hashmap требуется больше места (32 ГБ) по сравнению с хранилищем на основе переменных. Имеет ли HashMap очень большие накладные расходы на память?

PS: используя java17

Вам стоит посмотреть ДЖОЛ. Каждая строка — это не просто 7 байтов — объект с заголовком для себя и базовыми элементами, такими как byte[]. В hashMap есть таблица записей и Map.Entry для каждой записи, поэтому это как минимум +7x50M объектов по сравнению с другой вашей реализацией.

DuncG 10.07.2024 16:15

Я думаю, что JVM может интернировать некоторые строковые объекты «key1»,..., «key6», используемые в случае MyDataHolder_map, что приводит к меньшему объему памяти, чем ожидалось для подхода на основе переменных. также JVM может внутренне объединять некоторые объекты String, используемые в подходе на основе переменных, увеличивая использование памяти по сравнению с реализацией HashMap. Если у вас достаточно процессора, используйте ConcurrentHashMap

Ali.Mojtahed 10.07.2024 20:52

@Ali.Mojtahed — ключевые строки будут интернированы в обоих случаях, поскольку они являются строковыми литералами. Так что это оказывает незначительное влияние на использование памяти. И наоборот, строки, которые не являются константами времени компиляции, не интернируются JVM. Таким образом, строки значений не интернируются. Интернирование — отвлекающий маневр.

Stephen C 11.07.2024 13:09
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
3
67
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Почему программа занимает 24 ГБ с переменной версией, она должна быть намного меньше, поскольку общее количество уникальных строк в виртуальной машине составляет 6 ключей на итератор, т.е. 6 * 50M = 300M, а каждая строка имеет размер 7 байт, поэтому общее количество составляет 300M * 7Byte = 1,9 ГБ. Я понимаю, что существуют накладные расходы на ссылки и объекты, но они не могут превышать 20 ГБ. что происходит?

(The following is making some assumptions about your JVM version, heap size and compressed OOPS options. For different assumptions, the object sizes may be different. But the overall point remains the same.)

Накладные расходы намного больше, чем вы думаете.

  1. Каждый объект Java имеет 12-байтовый заголовок. Каждый массив Java имеет заголовок длиной 16 байт.

  2. Выделенный размер узла кучи Java (для объекта или массива) округляется до числа, кратного 8 байтам.

  3. Объект Java String (в Java 9 и более поздних версиях) имеет следующие поля:

    private final byte[] value;
    private final byte coder;
    private int hash;
    private boolean hashIsZero;
    

В сумме это составит как минимум 14 байт (или 10 байт со сжатым OOPS) в зависимости от того, как поля выровнены в памяти.

  1. Поле value является ссылкой на массив.

Если сложить все это, строка из 7 символов (содержащая символы ASCII) будет занимать round_up(12 + 14) + round_up(16 + 7) = 32 + 24 = 56 байт.

Почему на основе Hashmap MyDataHolder требуется больше места (32 ГБ) по сравнению с хранилищем на основе переменных. Имеет ли HashMap очень большие накладные расходы на память?

В принципе да. Я не буду выполнять вычисления, потому что они довольно сложны, но в более простом случае узел хэш-цепочки, содержащий запись карты, имеет накладные расходы в размере 40 байт + 8 байт для каждого слота в хеш-массиве. (И это не включает сами объекты ключей и значений.)

Выравнивание по умолчанию составляет 8 байт, а не 16 байт. Кроме того, CompressedOOPs включен по умолчанию, поэтому поля экземпляра String имеют размер 10 байт, что делает (мелкий) размер String 24 байтами, а не 32. Но общая мысль, конечно, правильная. Объекты потребляют гораздо больше памяти, чем первоначально предполагал ОП…

Holger 11.07.2024 12:47

Другие вопросы по теме