У меня есть приведенный ниже пример кода, который зацикливается на 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 ГБ ОЗУ.
Вопрос:
PS: используя java17
Я думаю, что JVM может интернировать некоторые строковые объекты «key1»,..., «key6», используемые в случае MyDataHolder_map, что приводит к меньшему объему памяти, чем ожидалось для подхода на основе переменных. также JVM может внутренне объединять некоторые объекты String, используемые в подходе на основе переменных, увеличивая использование памяти по сравнению с реализацией HashMap. Если у вас достаточно процессора, используйте ConcurrentHashMap
@Ali.Mojtahed — ключевые строки будут интернированы в обоих случаях, поскольку они являются строковыми литералами. Так что это оказывает незначительное влияние на использование памяти. И наоборот, строки, которые не являются константами времени компиляции, не интернируются JVM. Таким образом, строки значений не интернируются. Интернирование — отвлекающий маневр.
Почему программа занимает 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.)
Накладные расходы намного больше, чем вы думаете.
Каждый объект Java имеет 12-байтовый заголовок. Каждый массив Java имеет заголовок длиной 16 байт.
Выделенный размер узла кучи Java (для объекта или массива) округляется до числа, кратного 8 байтам.
Объект Java String
(в Java 9 и более поздних версиях) имеет следующие поля:
private final byte[] value;
private final byte coder;
private int hash;
private boolean hashIsZero;
В сумме это составит как минимум 14 байт (или 10 байт со сжатым OOPS) в зависимости от того, как поля выровнены в памяти.
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. Но общая мысль, конечно, правильная. Объекты потребляют гораздо больше памяти, чем первоначально предполагал ОП…
Вам стоит посмотреть ДЖОЛ. Каждая строка — это не просто 7 байтов — объект с заголовком для себя и базовыми элементами, такими как byte[]. В hashMap есть таблица записей и Map.Entry для каждой записи, поэтому это как минимум +7x50M объектов по сравнению с другой вашей реализацией.