小程序 转义_为内存密集型应用程序转义JVM堆
小程序 轉(zhuǎn)義
如果您曾經(jīng)分配過大型Java堆,您就會知道在某個時候(通常從大約4 GiB開始),您將開始遇到垃圾回收暫停的問題。
我不會詳細說明為什么在JVM中會出現(xiàn)暫停,但是總之,當JVM進行完整的收集并且您有很大的堆時,就會發(fā)生暫停。 隨著堆的增加,這些集合可能會變長。
解決此問題的最簡單方法是調(diào)整JVM垃圾回收參數(shù),以匹配特定應(yīng)用程序的內(nèi)存分配和釋放行為。 這有點晦澀難懂,需要仔細測量,但是可能有很大的堆,同時又避免了大多數(shù)舊式垃圾回收。 如果您想了解有關(guān)垃圾收集調(diào)優(yōu)的更多信息,請查閱JVM GC調(diào)優(yōu)指南 。 如果您總體上對GC真的很感興趣,那么這是一本很棒的書: The Garbage Collection Handbook 。
有一些JVM實現(xiàn)可以保證比Sun VM(例如Zing JVM)少得多的暫停時間,但通常會以其他系統(tǒng)成本為代價,例如增加內(nèi)存使用量和單線程性能。 易于配置和低gc保證仍然非常吸引人。 出于本文的目的,我將使用Java中的內(nèi)存中緩存或存儲的示例,主要是因為我在過去使用這些技術(shù)中的一些時已經(jīng)構(gòu)建了一對。
我們假設(shè)我們有一個基本的緩存接口定義,如下所示:
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這樣的。
我們將展示如何使用此緩存的不同實現(xiàn),以不同的方式將數(shù)據(jù)存儲在內(nèi)存中。 實現(xiàn)此緩存的最簡單方法是使用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);} }該實現(xiàn)是直接的。 但是,隨著地圖大小的增加,我們將分配大量對象(并取消分配),我們使用的是盒裝原語,它占用了更多的內(nèi)存空間,因此原語和地圖需要不時調(diào)整大小。 當然,我們可以簡單地通過使用基于基元的映射來改進此實現(xiàn)。 它會使用較少的內(nèi)存和對象,但仍會占用堆中的空間并可能對堆進行分區(qū),如果由于其他原因執(zhí)行完整的GC,則會導(dǎo)致更長的暫停時間。
讓我們看看不使用堆來存儲相似數(shù)據(jù)的其他方法:
- 使用單獨的過程來存儲數(shù)據(jù) 。 可能是通過套接字或Unix套接字連接的Redis或Memcached實例。 實施起來相當簡單。
- 使用內(nèi)存映射文件將數(shù)據(jù)卸載到磁盤 。 操作系統(tǒng)是您的朋友,它將做很多繁重的工作來預(yù)測接下來從文件中讀取的內(nèi)容以及與文件的接口,就像一大堆數(shù)據(jù)一樣。
- 使用本機代碼并通過JNI或JNA訪問它 。 通過JNI,您將獲得更好的性能,并通過JNA易于使用。 需要您編寫本機代碼。
- 使用 NIO包中直接分配的緩沖區(qū) 。
- 使用特定于Sun的Unsafe類直接從Java代碼訪問內(nèi)存。
我將重點介紹本文僅使用Java的解決方案,直接分配的緩沖區(qū)和Unsafe類。
直接分配的緩沖區(qū)
在Java NIO中開發(fā)高性能網(wǎng)絡(luò)應(yīng)用程序時,直接分配緩沖區(qū)非常有用,并且廣泛使用。 通過直接在堆外部分配數(shù)據(jù),在許多情況下,您可以編寫軟件,使這些數(shù)據(jù)實際上永遠不會碰到堆。
創(chuàng)建一個新的直接分配的緩沖區(qū)非常簡單:
int numBytes = 1000; ByteBuffer buffer = ByteBuffer.allocateDirect(numBytes);創(chuàng)建新緩沖區(qū)后,可以用幾種不同的方式來操作緩沖區(qū)。 如果您從未使用過Java NIO緩沖區(qū),那么絕對值得一看,因為它們確實很棒。
除了填充,清空和標記緩沖區(qū)中不同點的方法外,您還可以選擇在緩沖區(qū)上使用不同的視圖而不是ByteBuffer –例如, buffer.asLongBuffer()為您提供了在ByteBuffer上的視圖,您可以在該視圖上buffer.asLongBuffer()操作元素。
那么如何在我們的Cache示例中使用它們呢? 有很多種方法,最直接的方法是將值記錄的序列化/外部化形式存儲在一個大數(shù)組中,以及指向該數(shù)組中記錄的偏移量和大小的鍵映射。
它可能看起來像這樣(非常寬松的方法,缺少實現(xiàn)并假設(shè)記錄大小固定):
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);} }如您所見,此代碼有許多限制:固定的記錄大小,固定的支持映射大小,完成外部化的方式有限,難以刪除和重用空間等。盡管其中某些方式可以通過巧妙的方法來克服以字節(jié)數(shù)組表示記錄(也可以在直接分配的緩沖區(qū)中表示keyToOffset映射)或處理刪除操作(我們可以實現(xiàn)自己的SLAB分配器),其他諸如調(diào)整支持映射大小的操作很難克服。 一個有趣的改進是將記錄實現(xiàn)為記錄和字段的偏移量,從而減少了我們僅按需復(fù)制和復(fù)制的數(shù)據(jù)量。
請注意,JVM對直接分配的緩沖區(qū)使用的內(nèi)存量施加了限制。 您可以使用-XX:MaxDirectMemorySize選項進行調(diào)整。 查看ByteBuffer javadocs
不安全
直接從Java管理內(nèi)存的另一種方法是使用隱藏的Unsafe類。 從技術(shù)上講,我們不應(yīng)該使用它,它是特定于實現(xiàn)的,因為它位于sun軟件包中,但是提供的可能性是無限的。
Unsafe給我們帶來的是直接從Java代碼分配,取消分配和管理內(nèi)存的能力。 我們還可以獲取實際的指針,并將它們在本機代碼和Java代碼之間互換傳遞。
為了獲得一個不安全的實例,我們需要走一些彎路:
private Unsafe getUnsafeBackingMap() {try {Field f = Unsafe.class.getDeclaredField('theUnsafe');f.setAccessible(true);return (Unsafe) f.get(null);} catch (Exception e) { }return null; }一旦有了不安全因素,我們可以將其應(yīng)用于我們先前的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;} }有很多改進的空間,您需要手動執(zhí)行許多操作,但是功能非常強大。 您還可以顯式釋放和重新分配以這種方式分配的內(nèi)存,這使您可以以與C相同的方式編寫一些代碼。
查看javadocs中的Unsafe
結(jié)論
有很多方法可以避免在Java中使用堆,并以此方式使用更多的內(nèi)存。 您無需執(zhí)行此操作,而且我個人看到運行20GiB-30GiB且已進行適當調(diào)整的JVM,并且沒有長時間的垃圾回收暫停,但這非常有趣。
如果您想了解一些項目如何將其用于我在此處編寫的基本(并且未經(jīng)測試,幾乎寫在餐巾紙上)緩存代碼,請查看EHCache的BigMemory或Apache Cassandra,它們也將Unsafe用于此類方法。
參考:在Java Advent Calendar博客上,我們的JCG合作伙伴 Ruben Badaro 從JVM堆轉(zhuǎn)出了內(nèi)存密集型應(yīng)用程序 。
翻譯自: https://www.javacodegeeks.com/2012/12/escaping-the-jvm-heap-for-memory-intensive-applications.html
小程序 轉(zhuǎn)義
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的小程序 转义_为内存密集型应用程序转义JVM堆的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JUnit与TestNG:您应该选择哪种
- 下一篇: 海尔三翼鸟是什么意思(三翼鸟和海尔是啥品