Java高速、多線程虛擬內存

本文作者Alex已經從事Java開發15年了,最近幫助開發了COBOL和Magik語言的JVM 。當前,他正致力於Micro Focus的Java性能測試工具。在本文中,他闡述了在標準硬件中實現高速、多線程虛擬內存的可能性及方案。原文內容如下。 

你想在標準硬件上運行TB級甚至PB級內存的JVM嗎?你想與內存交互一樣讀寫文件,且無需關心文件的打開、關閉、讀、寫嗎? 

JVM的64位地址空間使這些成爲可能。 

首先,不要在觀念上將內存和磁盤進行區分,而是統一處理爲內存映射文件。在32位地址空間時,內存映射文件只是爲了高速訪問磁盤;因爲受限於虛擬機的有限地址空間,並不支持大規模的虛擬內存或大文件。如今JVM已經發展爲64位,而且可以在64位操作系統上運行。在一個進程的地址空間中,內存映射文件大小就可以達到TB甚至PB。 

進程無需關心內存是在RAM或是磁盤上。操作系統會負責處理,而且處理得非常高效。 

我們可以使用Java的MappedByteBuffer類訪問內存映射文件。該類的實例對象與普通的ByteBuffer一樣,但包含的內存是虛擬的——可能是在磁盤上,也可能是在RAM中。但無論哪種方式,都是由操作系統負責處理。因爲的ByteBuffer的大小上限是Intger.MAX_VALUE,我們需要若干個ByteBuffer來映射大量內存。在這個示例中,我映射了40GB。 

這是因爲我的Mac只有16GB內存,下面代碼證明了這一點! 

Java代碼 
  1. public MapperCore(String prefix, long size) throws IOException {  
  2.     coreFile = new File(prefix + getUniqueId() + ".mem");  
  3.     // This is a for testing - to avoid the disk filling up  
  4.     coreFile.deleteOnExit();  
  5.     // Now create the actual file  
  6.     coreFileAccessor = new RandomAccessFile(coreFile, "rw");  
  7.     FileChannel channelMapper = coreFileAccessor.getChannel();  
  8.     long nChunks = size / TWOGIG;  
  9.     if (nChunks > Integer.MAX_VALUE)  
  10.         throw new ArithmeticException("Requested File Size Too Large");  
  11.     length = size;  
  12.     long countDown = size;  
  13.     long from = 0;  
  14.     while (countDown > 0) {  
  15.         long len = Math.min(TWOGIG, countDown);  
  16.         ByteBuffer chunk = channelMapper.map(MapMode.READ_WRITE, from, len);  
  17.         chunks.add(chunk);  
  18.         from += len;  
  19.         countDown -= len;  
  20.     }  
  21. }  


上面的代碼在虛擬內存創建了40GB MappedByteBuffer對象列表。讀取和寫入時只需要注意處理兩個內存模塊的跨越訪問。完整代碼的可以在這裏找到。 

線程 

一個極其強大且簡單易用的方法就是線程。但是普通的Java IO簡直就是線程的噩夢。兩個線程無法在不引起衝突的情況下同時訪問相同的數據流或RandomAccessFile 。雖然可以使用非阻塞IO,但是這樣做會增加代碼的複雜性並對原有的代碼造成侵入。 

與其他的內存線程一樣,內存映射文件也是由操作系統來處理。可以根據讀寫需要,在同一時刻儘可能多的使用線程。我的測試代碼有128個線程,而且工作得很好(雖然機器發熱比較大)。唯一重要的技巧是複用MappedByteBuffer對象,避免自身位置狀態引發問題。 

現在可以執行下面的測試: 

Java代碼 
  1. @Test  
  2. public void readWriteCycleThreaded() throws IOException {  
  3. final MapperCore mapper = new MapperCore("/tmp/MemoryMap", BIG_SIZE);  
  4. final AtomicInteger fails = new AtomicInteger();  
  5. final AtomicInteger done = new AtomicInteger();  
  6. Runnable r = new Runnable() {  
  7.     public void run() {  
  8.         try {  
  9.             // Set to 0 for sequential test  
  10.             long off = (long) ((BIG_SIZE - 1024) * Math.random());  
  11.             System.out.println("Running new thread");  
  12.             byte[] bOut = new byte[1024];  
  13.             double counts = 10000000;  
  14.             for (long i = 0; i < counts; ++i) {  
  15.                 ByteBuffer buf = ByteBuffer.wrap(bOut);  
  16.                 long pos = (long) (((BIG_SIZE - 1024) * (i / counts)) + off)  
  17.                         % (BIG_SIZE - 1024);  
  18.                 // Align with 8 byte boundary  
  19.                 pos = pos / 8;  
  20.                 pos = pos * 8;  
  21.                 for (int j = 0; j < 128; ++j) {  
  22.                     buf.putLong(pos + j * 8);  
  23.                 }  
  24.                 mapper.put(pos, bOut);  
  25.                 byte[] bIn = mapper.get(pos, 1024);  
  26.                 buf = ByteBuffer.wrap(bIn);  
  27.                 for (int j = 0; j < 128; ++j) {  
  28.                     long val = buf.getLong();  
  29.                     if (val != pos + j * 8) {  
  30.                         throw new RuntimeException("Error at " + (pos + j * 8) + " was " + val);  
  31.                     }  
  32.                 }  
  33.             }  
  34.             System.out.println("Thread Complete");  
  35.         } catch (Throwable e) {  
  36.             e.printStackTrace();  
  37.             fails.incrementAndGet();  
  38.         } finally {  
  39.             done.incrementAndGet();  
  40.         }  
  41.     }  
  42. };  
  43. int nThreads = 128;  
  44. for (int i = 0; i < nThreads; ++i) {  
  45.     new Thread(r).start();  
  46.     }  
  47. while (done.intValue() != nThreads) {  
  48.     try {  
  49.         Thread.sleep(1000);  
  50.     } catch (InterruptedException e) {  
  51.         // ignore  
  52.     }  
  53.     }  
  54. if (fails.intValue() != 0) {  
  55.     throw new RuntimeException("It failed " + fails.intValue());  
  56.     }  
  57. }  


我曾嘗試進行其他形式的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
發佈了41 篇原創文章 · 獲贊 7 · 訪問量 35萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章