用16G內存在Java Map中處理30億對象

轉自http://www.coderli.com/translate-java-collections-bigdata-mapdb

在一個下雨的夜晚,我在思考Java中內存管理的問題,以及Java集合對內存使用的效率情況。我做了一個簡單的實驗,測試在16G內存條件下,Java的Map可以插入多少對象。

這個試驗的目的是爲了得出集合的內部上限。所以,我決定使用很小的key和value。所有的測試,都是在64w位linux環境下進行的,操作系統是ubuntu12.04。JVM版本爲Oracle Java 1.7.0_09-bo5 (HotSpot 23.5-b02)。在這個JVM中,壓縮指針(compressed pointers(-XX:+UseCompressedOops))的選項是默認打開的。

首先是簡單的針對java.util.TreeMap的測試。不停向其中插入數字,直到其拋出內存溢出異常。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);
}

這個用例插入了1 7200 0000條數據。在接近結束的時候,由於高負荷的GC插入效率開始降低。第二次,我用HashMap代替TreeMap,這次插入了182 000 000條數據。

Java默認的集合並不是最高效利用內存的。所以,這回我們嘗試最後化內存的測試。我選擇了MapDB中的LongHashMap,其使用原始的long key並且對封裝的內存佔用進行了優化。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 000 000。同樣,在插入接近結束的時候,速度開始減慢。看起來這是基於堆的結合的限制所在。垃圾回收帶來了瓶頸 。

現在是時候祭出殺手鐗了:-)。我們可以採用非基於堆的方式存儲,這樣GC就不會發現我們的數據。我來介紹一下MapDB,它提供了基於數據庫引擎的併發同步的TreeMap和HashMap。它提供了多樣化的存儲方式,其中一個就是非堆內存的方式。(聲明:我是MapDB的作者)。

現在,讓我們再跑一下之前的用例,這次採用的是非堆的Map。首先是配置並打開數據庫,它打開的直接基於內存存儲並且關閉事物的模式。接下來的代碼是在這個db中創建一個新的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);
}

這是非堆的Map,所以我們需要不同的JVM配置: -XX:MaxDirectMemorySize=15G -Xmx128M。這次測試在達到980 000 000條記錄的時候出現內存溢出。

但是,MapDB還可以優化。之前樣例的問題在於記錄的破碎分散,b-tree的節點每次插入都要調整它的容量。變通的方案是,將b-tree的節點在其插入前短暫的緩存起來。這使得記錄的分散降到最低。所以,我們來改變一下DB的配置:

DB db = DBMaker
     .newDirectMemoryDB()
     .transactionDisable()
     .asyncFlushDelay(100)
     .make();
 
Map m = db.getTreeMap("test");
這次記錄數達到了 1 738 000 000。速度也是達到了驚人的31分鐘完成了17億數據的插入。

MapDB還能繼續優化。我們把b-tree的節點容量從32提升到120並且打開透明(OneCoder理解爲對用戶不可見的)壓縮:

DB db = DBMaker
            .newDirectMemoryDB()
            .transactionDisable()
            .asyncFlushDelay(100)
            .compressionEnable()
            .make();
 
   Map m = db.createTreeMap("test",120, false, null, null, null);
這個用例在大約3 315 000 000條記錄時出現內存溢出。由於壓縮,他的速度 有所降低,不過還是在幾個小時內完成。我還可以進行一些優化(自定義序列化等等) ,使得數據量達到大約40億。

也許你好奇所有這些記錄是怎麼存儲的。答案就是,delta-key壓縮。(OneCoder注:不知如何翻譯)。當然,向B-Tree插入已經排好序的遞增key是最佳的使用場景,並且MapDB也對此進行了一些小小的 優化。最差的情形就是key是隨機的.

後續更新:很多朋友對壓縮有一些困惑。在這些用例中,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也可以存儲652 000 000條記錄,大概4倍於基於堆的集合。

這個簡單的試驗沒有太多的目的。這僅僅是我對MapDB的一種優化。也許,更多的驚喜在於插入效率確實不錯並且MapDB可以抗衡基於內存的集合。

原文地址:http://kotek.net/blog/3G_map


發佈了119 篇原創文章 · 獲贊 21 · 訪問量 101萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章