为内存密集型应用程序转义JVM堆
如果您曾經分配過大的Java堆,您就會知道在某個時候(通常從大約4 GiB開始),您將開始遇到垃圾回收暫停的問題。
我不會詳細介紹為什么在JVM中會出現暫停,但是總之,當JVM進行完整的收集并且您有很大的堆時,就會發生暫停。 隨著堆的增加,這些集合可能會變得更長。
解決此問題的最簡單方法是調整JVM垃圾回收參數,以匹配特定應用程序的內存分配和釋放行為。 這有點晦澀,需要仔細測量,但是可能有很大的堆,同時又避免了大多數舊式垃圾回收。 如果您想了解有關垃圾收集調優的更多信息,請查閱JVM GC調優指南 。 如果您總體上對GC真的很感興趣,那么這是一本很棒的書: The Garbage Collection Handbook 。
有一些JVM實現可以保證比Sun VM(例如Zing JVM)要少得多的暫停時間,但是通常會增加系統的其他成本,例如增加內存使用量和單線程性能。 易于配置和低gc保證仍然非常吸引人。 出于本文的目的,我將使用內存緩存或Java存儲示例,這主要是因為我在過去使用這些技術中的一部分時已經構建了一對。
我們假設我們有一個基本的緩存接口定義,如下所示:
import java.io.Externalizable;public interface Cache<K extends Externalizable, V extends Externalizable> {public void put(K key, V value);public V get(K key); }對于這個簡單的示例,我們要求鍵和值是可外部化的,而不是像此IRL那樣。
我們將展示如何使用此緩存的不同實現,以不同的方式將數據存儲在內存中。 實現此緩存的最簡單方法是使用Java集合:
import java.io.Externalizable; import java.util.HashMap; import java.util.Map;public class CollectionCache<K extends Externalizable, V extends Externalizable> implements Cache<K, V> {private final Map<K, V> backingMap = new HashMap<K, V>();public void put(K key, V value) {backingMap.put(key, value);}public V get(K key) {return backingMap.get(key);} }該實現是直接的。 但是,隨著地圖大小的增加,我們將分配大量對象(并取消分配),我們使用的是盒裝原語,它占用了更多的內存空間,因此原語和地圖需要不時調整大小。 當然,我們可以簡單地通過使用基于基元的映射來改進此實現。 它會使用較少的內存和對象,但仍會占用堆中的空間并可能對堆進行分區,如果由于其他原因我們執行完整的GC,則會導致更長的暫停時間。
讓我們看看不使用堆來存儲相似數據的其他方法:
- 使用一個單獨的過程來存儲數據 。 可能是通過套接字或Unix套接字連接的Redis或Memcached實例。 實施起來相當簡單。
- 使用內存映射文件將數據卸載到磁盤 。 操作系統是您的朋友,并且會做很多繁重的工作來預測接下來從文件中讀取的內容以及與文件的接口,就像是一大堆數據一樣。
- 使用本機代碼并通過JNI或JNA訪問它 。 通過JNI,您將獲得更好的性能,并通過JNA易于使用。 需要您編寫本機代碼。
- 使用 NIO包中直接分配的緩沖區 。
- 使用Sun特定的Unsafe類可以直接從Java代碼訪問內存。
我將重點介紹本文僅使用Java的解決方案,直接分配的緩沖區和Unsafe類。
直接分配的緩沖區
在Java NIO中開發高性能網絡應用程序時,直接分配緩沖區非常有用,并且廣泛使用。 通過在堆外直接分配數據,在許多情況下,您可以編寫軟件,使這些數據實際上永遠不會碰到堆。
創建新的直接分配緩沖區非常簡單:
int numBytes = 1000; ByteBuffer buffer = ByteBuffer.allocateDirect(numBytes);創建新緩沖區后,可以用幾種不同的方式來操作緩沖區。 如果您從未使用過Java NIO緩沖區,那么絕對值得一看,因為它們確實很棒。
除了填充,清空和標記緩沖區中不同點的方法外,您還可以選擇在緩沖區上使用不同的視圖而不是ByteBuffer –例如, buffer.asLongBuffer()為您提供了在ByteBuffer上的視圖,您可以在該視圖上buffer.asLongBuffer()操作元素。
那么如何在我們的Cache示例中使用它們? 有很多種方法,最直接的方法是將值記錄的序列化/外部化形式存儲在一個大數組中,以及指向該數組中記錄的偏移量和大小的鍵映射。
可能看起來像這樣(非常寬松的方法,缺少實現并假設記錄大小固定):
import java.io.Externalizable; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map;public class DirectAllocatedCache<K extends Externalizable, V extends Externalizable> implements Cache<K,V> {private final ByteBuffer backingMap;private final Map<K, Integer> keyToOffset;private final int recordSize;public DirectAllocatedCache(int recordSize, int maxRecords) {this.recordSize = recordSize;this.backingMap = ByteBuffer.allocateDirect(recordSize * maxRecords);this.keyToOffset = new HashMap<K, Integer>();}public void put(K key, V value) {if(backingMap.position() + recordSize < backingMap.capacity()) {keyToOffset.put(key, backingMap.position());store(value);} }public V get(K key) {int offset = keyToOffset.get(key);if(offset >= 0)return retrieve(offset);throw new KeyNotFoundException();}public V retrieve(int offset) {byte[] record = new byte[recordSize];int oldPosition = backingMap.position();backingMap.position(offset);backingMap.get(record);backingMap.position(oldPosition);//implementation left as an exercisereturn internalize(record);}public void store(V value) {byte[] record = externalize(value);backingMap.put(record);} }如您所見,此代碼有許多限制:固定的記錄大小,固定的支持映射大小,完成外部化的方式有限,難以刪除和重用空間等。盡管其中某些方式可以通過巧妙的方法來克服以字節數組表示記錄(也可以在直接分配的緩沖區中表示keyToOffset映射)或處理刪除操作(我們可以實現自己的SLAB分配器),其他諸如調整支持映射大小的操作很難克服。 一個有趣的改進是將記錄實現為記錄和字段的偏移量,從而減少了我們僅按需復制和復制的數據量。
請注意,JVM對直接分配的緩沖區使用的內存量施加了限制。 您可以使用-XX:MaxDirectMemorySize選項進行調整。 查看ByteBuffer javadocs
不安全
直接從Java管理內存的另一種方法是使用隱藏的Unsafe類。 從技術上講,我們不應該使用它,它是特定于實現的,因為它位于sun軟件包中,但是提供的可能性是無限的。
Unsafe給我們帶來的是直接從Java代碼分配,取消分配和管理內存的能力。 我們還可以獲取實際的指針,并將它們在本機代碼和Java代碼之間互換傳遞。
為了獲得一個不安全的實例,我們需要走一些彎路:
private Unsafe getUnsafeBackingMap() {try {Field f = Unsafe.class.getDeclaredField('theUnsafe');f.setAccessible(true);return (Unsafe) f.get(null);} catch (Exception e) { }return null; }一旦有了不安全因素,我們可以將其應用于之前的Cache示例:
import java.io.Externalizable; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map;import sun.misc.Unsafe;public class UnsafeCache<K extends Externalizable, V extends Externalizable> implements Cache<K, V> {private final int recordSize;private final Unsafe backingMap;private final Map<K, Integer> keyToOffset;private long address;private int capacity;private int currentOffset;public UnsafeCache(int recordSize, int maxRecords) {this.recordSize = recordSize;this.backingMap = getUnsafeBackingMap();this.capacity = recordSize * maxRecords;this.address = backingMap.allocateMemory(capacity);this.keyToOffset = new HashMap<K, Integer>();}public void put(K key, V value) {if(currentOffset + recordSize < capacity) {store(currentOffset, value);keyToOffset.put(key, currentOffset);currentOffset += recordSize;}}public V get(K key) {int offset = keyToOffset.get(key);if(offset >= 0)return retrieve(offset);throw new KeyNotFoundException();}public V retrieve(int offset) {byte[] record = new byte[recordSize];//Inefficientfor(int i=0; i<record.length; i++) {record[i] = backingMap.getByte(address + offset + i);}//implementation left as an exercisereturn internalize(record);}public void store(int offset, V value) {byte[] record = externalize(value);//Inefficientfor(int i=0; i<record.length; i++) {backingMap.putByte(address + offset + i, record[i]);}}private Unsafe getUnsafeBackingMap() {try {Field f = Unsafe.class.getDeclaredField('theUnsafe');f.setAccessible(true);return (Unsafe) f.get(null);} catch (Exception e) { }return null;} }有很多改進的空間,您需要手動執行許多操作,但是功能非常強大。 您還可以顯式釋放和重新分配以這種方式分配的內存,這使您可以以與C相同的方式編寫一些代碼。
查看javadocs中的Unsafe
結論
有許多種方法可以避免在Java中使用堆,并以此方式使用更多的內存。 您無需執行此操作,而且我個人看到運行20GiB-30GiB且已進行了適當調整的JVM,并且沒有長時間的垃圾收集暫停,但這非常有趣。
如果要查看一些項目如何將其用于我在此處編寫的基本(并且未經測試,幾乎寫在餐巾紙上)緩存代碼,請查看EHCache的BigMemory或Apache Cassandra,它們也將Unsafe用于此類方法。
參考:在Java Advent Calendar博客上,我們的JCG合作伙伴 Ruben Badaro 從JVM堆轉出了內存密集型應用程序 。
翻譯自: https://www.javacodegeeks.com/2012/12/escaping-the-jvm-heap-for-memory-intensive-applications.html
總結
以上是生活随笔為你收集整理的为内存密集型应用程序转义JVM堆的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 均衡器怎么调能达到最佳效果
- 下一篇: 使用SynchronousQueue实现