Заполнение HashMap
миллионами объектов быстро приведет к таким проблемам, как неэффективное использование памяти, низкая производительность и проблемы со сборкой мусора. Узнайте, как использовать CronicleMap
вне кучи, который может содержать миллиарды объектов с минимальным воздействием кучи или вообще без него.
Встроенные реализации Map
, такие как HashMap
и ConcurrentHashMap
являются отличными инструментами, когда мы хотим работать с наборами данных малого и среднего размера. Однако по мере роста объема данных эти
Реализация Map
ухудшается и начинает демонстрировать ряд неприятных недостатков, как показано в этой первой статье серии статей о CronicleMap
открытым исходным кодом.
Распределение кучи
В приведенных ниже примерах мы будем использовать объекты Point
. Point
— это POJO с общедоступным конструктором по умолчанию, геттерами и сеттерами для свойств X и Y (int). Следующий фрагмент добавляет миллион объектов Point
в HashMap
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
final Map<Long, Point> m = LongStream.range( 0 , 1_000_000) .boxed() .collect( toMap( Function.identity(), FillMaps::pointFrom, (u,v) -> { throw new IllegalStateException(); }, HashMap:: new ) ); // Conveniency method that creates a Point from // a long by applying modulo prime number operations private static Point pointFrom( long seed) { final Point point = new Point(); point.setX(( int ) seed % 4517 ); point.setY(( int ) seed % 5011 ); return point; } |
Мы можем легко увидеть количество объектов, выделенных в куче, и объем памяти, занимаемой этими объектами:
01
02
03
04
05
06
07
08
09
10
11
|
Pers-MacBook-Pro:chronicle-test pemi$ jmap -histo 34366 | head num #instances #bytes class name (module) ------------------------------------------------------- 1 : 1002429 32077728 java.util.HashMap$Node (java.base @10 ) 2 : 1000128 24003072 java.lang.Long (java.base @10 ) 3 : 1000000 24000000 com.speedment.chronicle.test.map.Point 4 : 454 8434256 [Ljava.util.HashMap$Node; (java.base @10 ) 5 : 3427 870104 [B (java.base @10 ) 6 : 185 746312 [I (java.base @10 ) 7 : 839 102696 java.lang.Class (java.base @10 ) 8 : 1164 89088 [Ljava.lang.Object; (java.base @10 ) |
Для каждой записи Map
в куче должны быть созданы объект Long
, HashMap$Node
и Point
. Существует также ряд массивов с созданными объектами HashMap$Node
. В общей сложности эти объекты и массивы занимают 88,515,056 байт кучи памяти. Таким образом, каждая запись потребляет в среднем 88,5 байта.
NB. Дополнительные объекты 2429 HashMap$Node
происходят из других объектов HashMap
используемых внутри Java.
Распределение вне кучи
В отличие от этого, CronicleMap
использует очень мало динамической памяти, что можно наблюдать при выполнении следующего кода:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
final Map<Long, Point> m2 = LongStream.range( 0 , 1_000_000) .boxed() .collect( toMap( Function.identity(), FillMaps::pointFrom, (u,v) -> { throw new IllegalStateException(); }, () -> ChronicleMap .of(Long. class , Point. class ) .averageValueSize( 8 ) .valueMarshaller(PointSerializer.getInstance()) .entries(1_000_000) .create() ) ); |
01
02
03
04
05
06
07
08
09
10
11
|
Pers-MacBook-Pro:chronicle-test pemi$ jmap -histo 34413 | head num #instances #bytes class name (module) ------------------------------------------------------- 1 : 6537 1017768 [B (java.base @10 ) 2 : 448 563936 [I (java.base @10 ) 3 : 1899 227480 java.lang.Class (java.base @10 ) 4 : 6294 151056 java.lang.String (java.base @10 ) 5 : 2456 145992 [Ljava.lang.Object; (java.base @10 ) 6 : 3351 107232 java.util.concurrent.ConcurrentHashMap$Node (java.base @10 ) 7 : 2537 81184 java.util.HashMap$Node (java.base @10 ) 8 : 512 49360 [Ljava.util.HashMap$Node; (java.base @10 ) |
Как можно видеть, нет объектов кучи Java, выделенных для
Записи CronicleMap
и, следовательно, нет кучи памяти либо.
Вместо того, чтобы выделять кучу памяти, CronicleMap
выделяет свою память вне кучи. При условии, что мы запускаем нашу JVM с флагом -XX:NativeMemoryTracking=summary
, мы можем извлечь объем используемой памяти вне кучи, выполнив следующую команду:
1
2
|
Pers-MacBook-Pro:chronicle-test pemi$ jcmd 34413 VM.native_memory | grep Internal - Internal (reserved=30229KB, committed=30229KB) |
По-видимому, наш миллион объектов был размещен в памяти вне кучи, используя чуть более 30 МБ памяти вне кучи. Это означает, что каждая запись в
CronicleMap
используемый выше, требует в среднем 30 байтов.
Это намного эффективнее памяти, чем HashMap
который требует 88,5 байтов. Фактически мы сэкономили 66% оперативной памяти и почти 100% кучи памяти. Последнее важно, потому что Java Garbage Collector видит только объекты, находящиеся в куче.
Обратите внимание, что при создании мы должны решить, сколько записей может содержать CronicleMap
. Это отличается от
HashMap
который может динамически расти по мере добавления новых ассоциаций. Мы также должны предоставить сериализатор (т.е. PointSerializer.getInstance()
), который будет подробно обсуждаться позже в этой статье.
Вывоз мусора
Многие алгоритмы сборки мусора (GC) завершаются за время, пропорциональное квадрату объектов, которые существуют в куче. Так, например, если мы удвоим количество объектов в куче, мы можем ожидать, что сборка GC займет в четыре раза больше времени.
С другой стороны, если мы создадим в 64 раза больше объектов, мы можем ожидать мучительного увеличения ожидаемого времени GC в 1024 раза. Это эффективно мешает нам создавать действительно большие
Объекты HashMap
.
С ChronicleMap
мы могли бы просто создавать новые ассоциации, не заботясь о времени сбора мусора.
Serializer
Посредник между кучей и памятью вне кучи часто называют
сериализатор . ChronicleMap
поставляется с несколькими предварительно настроенными сериализаторами для большинства встроенных типов Java, таких как Integer
, Long
, String
и многих других.
В приведенном выше примере мы использовали специальный сериализатор, который использовался для преобразования Point
туда-сюда между кучей и памятью вне кучи. Класс сериализатора выглядит так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public final class PointSerializer implements SizedReader<Point>, SizedWriter<Point> { private static PointSerializer INSTANCE = new PointSerializer(); public static PointSerializer getInstance() { return INSTANCE; } private PointSerializer() {} @Override public long size( @NotNull Point toWrite) { return Integer.BYTES * 2 ; } @Override public void write(Bytes out, long size, @NotNull Point point) { out.writeInt(point.getX()); out.writeInt(point.getY()); } @NotNull @Override public Point read(Bytes in, long size, @Nullable Point using) { if (using == null ) { using = new Point(); } using.setX(in.readInt()); using.setY(in.readInt()); return using; } } |
Приведенный выше сериализатор реализован как одноэлементный файл без сохранения состояния, и фактическая сериализация в методах write()
и read()
довольно проста. Единственная сложность заключается в том, что нам нужно иметь нулевую проверку в
Метод read()
если переменная «using» не ссылается на экземплярный / повторно используемый объект.
Как установить это?
Когда мы хотим использовать ChronicleMap
в нашем проекте, мы просто добавляем следующую зависимость Maven в наш файл pom.xml, и у нас есть доступ к библиотеке.
1
2
3
4
5
|
< dependency > < groupId >net.openhft</ groupId > < artifactId >chronicle-map</ artifactId > < version >3.17.3</ version > </ dependency > |
Если вы используете другой инструмент для сборки, например, Gradle, вы можете узнать, как зависеть от ChronicleMap
, перейдя по этой ссылке .
Короткая история
Вот некоторые свойства ChronicleMap:
Хранит данные вне кучи
Почти всегда эффективнее память, чем HashMap
Реализует ConcurrentMap
Не влияет на время сбора мусора
Иногда нужен сериализатор
Имеет фиксированный максимальный размер записи
Может содержать миллиарды ассоциаций
Бесплатный и с открытым исходным кодом
Смотреть оригинальную статью здесь: Java: ChronicleMap Part 1, Go Off-Heap Мнения, высказанные участниками Java Code Geeks, являются их собственными. |