你想在標準硬件上運行TB級甚至PB級內存的JVM嗎?你想與內存交互一樣讀寫文件,且無需關心文件的打開、關閉、讀、寫嗎?
JVM的64位地址空間使這些成爲可能。
首先,不要在觀念上將內存和磁盤進行區分,而是統一處理爲內存映射文件。在32位地址空間時,內存映射文件只是爲了高速訪問磁盤;因爲受限於虛擬機的有限地址空間,並不支持大規模的虛擬內存或大文件。如今JVM已經發展爲64位,而且可以在64位操作系統上運行。在一個進程的地址空間中,內存映射文件大小就可以達到TB甚至PB。
進程無需關心內存是在RAM或是磁盤上。操作系統會負責處理,而且處理得非常高效。
我們可以使用Java的MappedByteBuffer類訪問內存映射文件。該類的實例對象與普通的ByteBuffer一樣,但包含的內存是虛擬的——可能是在磁盤上,也可能是在RAM中。但無論哪種方式,都是由操作系統負責處理。因爲的ByteBuffer的大小上限是Intger.MAX_VALUE,我們需要若干個ByteBuffer來映射大量內存。在這個示例中,我映射了40GB。
這是因爲我的Mac只有16GB內存,下面代碼證明了這一點!
- public MapperCore(String prefix, long size) throws IOException {
- coreFile = new File(prefix + getUniqueId() + ".mem");
- // This is a for testing - to avoid the disk filling up
- coreFile.deleteOnExit();
- // Now create the actual file
- coreFileAccessor = new RandomAccessFile(coreFile, "rw");
- FileChannel channelMapper = coreFileAccessor.getChannel();
- long nChunks = size / TWOGIG;
- if (nChunks > Integer.MAX_VALUE)
- throw new ArithmeticException("Requested File Size Too Large");
- length = size;
- long countDown = size;
- long from = 0;
- while (countDown > 0) {
- long len = Math.min(TWOGIG, countDown);
- ByteBuffer chunk = channelMapper.map(MapMode.READ_WRITE, from, len);
- chunks.add(chunk);
- from += len;
- countDown -= len;
- }
- }
上面的代碼在虛擬內存創建了40GB MappedByteBuffer對象列表。讀取和寫入時只需要注意處理兩個內存模塊的跨越訪問。完整代碼的可以在這裏找到。
線程
一個極其強大且簡單易用的方法就是線程。但是普通的Java IO簡直就是線程的噩夢。兩個線程無法在不引起衝突的情況下同時訪問相同的數據流或RandomAccessFile 。雖然可以使用非阻塞IO,但是這樣做會增加代碼的複雜性並對原有的代碼造成侵入。
與其他的內存線程一樣,內存映射文件也是由操作系統來處理。可以根據讀寫需要,在同一時刻儘可能多的使用線程。我的測試代碼有128個線程,而且工作得很好(雖然機器發熱比較大)。唯一重要的技巧是複用MappedByteBuffer對象,避免自身位置狀態引發問題。
現在可以執行下面的測試:
- @Test
- public void readWriteCycleThreaded() throws IOException {
- final MapperCore mapper = new MapperCore("/tmp/MemoryMap", BIG_SIZE);
- final AtomicInteger fails = new AtomicInteger();
- final AtomicInteger done = new AtomicInteger();
- Runnable r = new Runnable() {
- public void run() {
- try {
- // Set to 0 for sequential test
- long off = (long) ((BIG_SIZE - 1024) * Math.random());
- System.out.println("Running new thread");
- byte[] bOut = new byte[1024];
- double counts = 10000000;
- for (long i = 0; i < counts; ++i) {
- ByteBuffer buf = ByteBuffer.wrap(bOut);
- long pos = (long) (((BIG_SIZE - 1024) * (i / counts)) + off)
- % (BIG_SIZE - 1024);
- // Align with 8 byte boundary
- pos = pos / 8;
- pos = pos * 8;
- for (int j = 0; j < 128; ++j) {
- buf.putLong(pos + j * 8);
- }
- mapper.put(pos, bOut);
- byte[] bIn = mapper.get(pos, 1024);
- buf = ByteBuffer.wrap(bIn);
- for (int j = 0; j < 128; ++j) {
- long val = buf.getLong();
- if (val != pos + j * 8) {
- throw new RuntimeException("Error at " + (pos + j * 8) + " was " + val);
- }
- }
- }
- System.out.println("Thread Complete");
- } catch (Throwable e) {
- e.printStackTrace();
- fails.incrementAndGet();
- } finally {
- done.incrementAndGet();
- }
- }
- };
- int nThreads = 128;
- for (int i = 0; i < nThreads; ++i) {
- new Thread(r).start();
- }
- while (done.intValue() != nThreads) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- // ignore
- }
- }
- if (fails.intValue() != 0) {
- throw new RuntimeException("It failed " + fails.intValue());
- }
- }
我曾嘗試進行其他形式的IO,但是隻要像上面那樣運行128個線程,性能都不如上面的方法。我在四核、超線程I7 Retina MacBook Pro上嘗試過。代碼運行時會啓動128個線程,超出CPU的最大負載(800%),直到操作系統檢測到該進程的內存不足。在這個時候,系統開始對內存映射文件的讀寫進行分頁。爲實現這一目標,內核會佔用一定的CPU,Java進程的性能會下降到650~750%。Java無需關心讀取、寫入、同步或類似的東西——操作系統會負責處理。
結果會有所不同
如果讀取和寫入點不是連續而是隨機的,性能下降有所區別(帶有交換時會達到750%,否則會達到250%)。我相信這種方式可能更適合處理少量的大數據對象,而不適用於大量的小數據對象。對於後者,可能的處理辦法是預先將大量小數據對象加載到緩存中,再將其映射到虛擬內存。
應用程序
到目前爲止,我使用的技術都是虛擬內存系統。在示例中,一旦與虛擬內存交互完成,就會刪除底層文件。但是,這種方法可以很容易地進行數據持久化。
例如,視頻編輯是一個非常具有挑戰性的工程問題。一般來說,有兩個有效的方法:無損耗存儲整個視頻,並編輯存儲的信息;或根據需要重新生成視頻。因爲RAM的制約,後一種方法越來越普遍。然而,視頻是線性的——這是一種理想的數據類型,可用來存儲非常大的映射虛擬內存。由於在視頻算法上取得的進步,可以將它作爲原始字節數組訪問。操作系統會根據需要將磁盤到虛擬內存的緩衝區進行分頁處理。
另一個同樣有效的應用場景是替代文檔服務中過度設計的RAM緩存解決方案。想想看,我們有一個幾TB的中等規模的文檔庫。它可能包含圖片、短片和PDF文件。有一種常見的快速訪問磁盤的方法,使用文件的RAM緩存弱引用或軟引用。但是,這會對JVM垃圾收集器產生重大影響,並且增加操作難度。如果將整個文檔映射到虛擬內存,可以更加簡單地完成同樣的工作。操作系統會根據需要將數據讀入內存。更重要的是,操作系統將盡量保持RAM中最近被訪問的內存頁。這意味着內存映射文件就像RAM緩存一樣,不會對Java或JVM垃圾收集器產生任何影響。
最後,內存映射文件在科學計算和建模等應用中非常有效。在用來處理代表真實世界系統的計算模型時,經常需要大量的數據才能正常工作。在我的音頻處理系統Sonic Field中,通過混合和處理單一聲波,可以模擬真實世界中的音頻效果。例如,創建原始音頻副本是爲模擬從硬表面反射的聲波,並將反射回來的聲波與原聲波混合。這種方法需要大量的存儲空間,這時就可以把音頻信號放在虛擬內存中(也是這項工作的最初動機)。
原文鏈接: jaxenter 翻譯: ImportNew.com - MarkGZ
譯文鏈接: http://www.importnew.com/9270.html