Статьи

Java: ChronicleMap Part 1, Go Off-Heap

Заполнение 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, являются их собственными.