ある雨の夜、私はJavaのメモリ管理とJavaコレクションを効果的に使用する方法について考えました。 16 GBのRAMでマップを挿入できるレコードはいくつですか?
この実験の目的は、コレクション管理の内部メモリコストを調査することです。 そこで、小さなキーと小さな値を使用することにしました。 すべてのテストは64ビットLinux Kubuntu 12.04で実行されました。 JVM 64ビットOracle Java 1.7.0_09-b05とHotSpot 23.5-b02。 このJVMでは、圧縮ポインター(-XX:+ UseCompressedOops)がデフォルトで有効になっています。
最初にjava.util.TreeMapを使用してテストします。 マップに数値を挿入し、メモリがなくなるまで機能します。 このテストのJVMパラメーター-Xmx15G
import java.util。*;
マップm =新しいTreeMap();
for(long counter = 0 ;; counter ++){
m.put(カウンター、 "");
if(counter%1000000 == 0)System.out.println( "" + counter);
}
この例は1億7200万で終わりました。 最後に向かって、ガベージコレクターの積極的なアクティビティのためにプロセスの速度が低下しました。 2回目の実行では、TreeMapをHashMapに置き換えましたが、1億8200万で終了しました。
デフォルトでは、Javaコレクションは非常に効率的ではありません。 メモリをさらに最適化してみましょう。MapDBからLongHashMapを選択しました。これは、プリミティブな長いキーを使用し、少量のメモリを持つように最適化されています。 再びJVM設定-Xmx15G
org.mapdbをインポートします*
LongMap m = new LongHashMap();
for(long counter = 0 ;; counter ++){
m.put(カウンター、 "");
if(counter%1000000 == 0)System.out.println( "" + counter);
}
今回は、カウンターは2億7,600万レコードで停止しました。 再び最後に向かって、ガベージコレクターの攻撃的なアクティビティのためにプロセスの速度が低下しました。
これは動的コレクションの制限のようです。ガベージコレクションは追加のコストをもたらします。
本物の武器を展開する時が来ました:-)。 ガベージコレクターがデータを表示しない動的メモリからいつでも逃げることができます。 MapDBを紹介しましょう。これはTreeMapとHashMapにデータベースサポートを提供します。 動的メモリにないオプションを含む、さまざまなストレージモードをサポートしています。
それでは、前の例を実行してみましょうが、Mapには動的メモリがありません。 まず、データベースを構成して開くためのいくつかの行があり、トランザクションをオフにしてメモリに直接アクセスします。 次の行は、データベースに新しいマップを作成します。
org.mapdbをインポートします*
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.make();
Map m = db.getTreeMap( "test");
for(long counter = 0 ;; counter ++){
m.put(カウンター、 "");
if(counter%1000000 == 0)System.out.println( "" + counter);
}
このマップは動的メモリにないため、異なるJVM設定が必要です:-XX:MaxDirectMemorySize = 15G -Xmx128M。 メモリーは9億8000万で使い果たしました。
ただし、MapDBの方が優れています。 前の例の問題は断片化であり、挿入ごとにbツリーノードのサイズが変更されます。 解決策は、ツリーノードを挿入する前にラッシュすることです。 これにより、記録の断片化が最小限に抑えられます。 DB構成を変更します。
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.asyncFlushDelay(100)
.make();
Map m = db.getTreeMap( "test");
31分後、メモリは1,738百万レコードになりました。
MapDBは、ツリー内のノードのサイズを32から120エントリに増やし、透過的な圧縮を有効にすることで、さらに改善できます。
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.asyncFlushDelay(100)
.compressionEnable()
.make();
Map m = db.createTreeMap( "test"、120、false、null、null、null);
この例では、3.315百万レコードのメモリを完成させます。 これは圧縮により遅くなりますが、数時間以内に完了します。 おそらく、いくつかの最適化(特別なシリアライザー)を行い、レコード数を増やすことができます(約40億)。
たぶん、これらすべてのメモがそこにどのように収まるかを尋ねることができます。 答えは、デルタキー圧縮です。 また、順序付けされたキーをBツリーに挿入することはより良いシナリオであり、MapDBはそれに対して少し最適化されています。 最悪のシナリオでは、キーがランダムに挿入されます。
すべてのサンプルでデフォルトでデルタキー圧縮。 この例では、Zlib圧縮を有効にしました。
DB db = DBMaker
.newDirectMemoryDB()
.transactionDisable()
.asyncFlushDelay(100)
.make();
Map m = db.getTreeMap( "test");
ランダムr =新しいランダム();
for(long counter = 0 ;; counter ++){
m.put(r.nextLong()、 "");
if(counter%1000000 == 0)System.out.println( "" + counter);
}
しかし、ランダムな順序であっても、MapDBは6億5100万件のレコードを保存できます。これは、通常の動的コレクションに基づくもののほぼ4倍です。
この小さな演習には多くの目標はありません。 これは、MapDBを最適化する1つの方法にすぎません。 最も驚くべきことに、挿入速度は素晴らしく、MapDBはメモリ内のコレクションと競合できます。
github.com/jankotek/jdbm3