Одним дождливым вечером я размышлял о памяти менеджмент в Java и как эффективно использовать Java коллекции. Я сделал простой эксперимент, сколько записей я могу вставить map с 16 Гб оперативной памяти?
Целью этого эксперимента является исследование внутренних расходов памяти на управление коллекциями. Поэтому я решил использовать маленькие ключи и малые значения. Все тесты проводились на 64-битных Linux Kubuntu 12.04. JVM 64bit Oracle Java 1.7.0_09-b05 с HotSpot 23.5-b02. Включены сжатые указатели (-XX: + UseCompressedOops) по умолчанию на этой JVM.
Первый тест с java.util.TreeMap. Вставляет число в map, работает пока память не заканчивается. JVM параметры для этого теста-Xmx15G
import java.util.*;
Map m = new TreeMap();
for(long counter=0;;counter++){
m.put(counter,"");
if(counter%1000000==0) System.out.println(""+counter);
}
Этот пример закончился 172 миллионами. Ближе к концу процесс замедлился благодаря агрессивной деятельности сборщика мусора. На втором заезде я заменил TreeMap на HashMap, он закончился 182 миллионами.
По умолчанию Java коллекции не являются супер эффективными. Так давайте попробуем более оптимизированы по памяти: Я выбрал LongHashMap из MapDB, который использует примитивные длинные ключи и оптимизирован чтоб иметь небольшой объем памяти. JVM настройки снова -Xmx15G
import org.mapdb.*
LongMap m = new LongHashMap();
for(long counter=0;;counter++){
m.put(counter,"");
if(counter%1000000==0) System.out.println(""+counter);
}
На этот раз счетчик остановился на 276 миллионов записей. Опять же ближе к концу процесс замедлился благодаря агрессивной деятельности сборщика мусора.
Похоже, что это предел для динамических коллекций, сбор мусора приносит дополнительные расходы.
Настало время, чтобы выкатить настоящее оружие:-). Мы всегда можем уйти от динамической памяти, где сборщик мусора не увидит наши данные. Позвольте мне представить вам MapDB, он предоставляет TreeMap и HashMap при поддержке базы данных. Поддерживает различные режимы хранения, включая вариант который не в динамической памяти.
Так что давайте запустим предыдущий пример, но теперь Map без динамической памяти. Во-первых, это несколько строк, чтобы настроить и открыть базу данных, прямой доступ в память с выключенными транзакциями. Следующая строка создает новый Map в БД.
import org.mapdb.*
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.make();
Map m = db.getTreeMap(«test»);
for(long counter=0;;counter++){
m.put(counter,"");
if(counter%1000000==0) System.out.println(""+counter);
}
Это Мap не находящийся в динамической памяти, так что мы нужны разные настройки JVM:-XX:MaxDirectMemorySize=15G -Xmx128M. Память закончилась на 980 миллионах.
Но MapDB можно сделать лучше. Проблема в предыдущем примере это фрагментация, узел дерева (b-tree) изменяет свой размер на каждой вставке. Решение заключается в кашировании узлов дерева, прежде чем они вставлены. Это уменьшает фрагментацию при записи до минимума. поменяем конфигурацию DB:
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.asyncFlushDelay(100)
.make();
Map m = db.getTreeMap(«test»);
Память закончилась на 1,738 миллионов записей, через 31 минуту.
MapDB можно сделать еще лучше — увеличив размер узла в дереве от 32 до 120 записей и включить прозрачное сжатие:
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.asyncFlushDelay(100)
.compressionEnable()
.make();
Map m = db.createTreeMap(«test»,120, false, null, null, null);
Этот пример заканчивает память на 3,315 миллионах записей. Это медленнее, благодаря сжатию, но он по-прежнему завершается в течение нескольких часов. Я мог бы, вероятно, сделать некоторые оптимизации (специальные сериализаторы) и увеличить количество записей, где-то около к 4 миллиардам.
Может быть, бы спросите, как все эти записи могут поместиться там. Ответ delta-key компрессия. Также вставлением упорядоченных ключей в B-Tree является лучшим сценарием и MapDB немного оптимизирована для него. Наихудший сценарий вставляет ключи в случайном порядке.
delta-key компрессия по умолчанию на всех примерах. В этом примере я активировал Zlib компрессию.
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.asyncFlushDelay(100)
.make();
Map m = db.getTreeMap(«test»);
Random r = new Random();
for(long counter=0;;counter++){
m.put(r.nextLong(),"");
if(counter%1000000==0) System.out.println(""+counter);
}
Но даже при случайном порядке MapDB сможет хранить 651 млн записей, почти в 4 раза больше, чем на основе обычных динамических коллекций.
У этого маленького упражнения не так уж много целей. Это лишь один из способов оптимизировать MapDB. Наиболее удивительным является то, что скорость вставки была отличной и MapDB может конкурировать коллекциям в памяти.
Автор: goodguyfromil