我們在開發過程中會遇到這樣的場景:就是一個服務的各項 JVM 的配置都比較合理的情況下,它的 GC 情況還是不容樂觀。分析之後發現有 2 個物件特別巨大,佔了總存活堆記憶體的 90%以上。其中第 1 大物件是本地快取, GC 之後物件一直存活。然後不久應用就會丟擲OutOfMemoryError,那怎樣避免這種情況呢?
這次給大家推薦一個比較好用的技術:堆外快取。哈哈哈,顧名思義就是在Java堆之外的快取,也就是把一些大的難以被GC回收的物件放到堆之外。PS堆外記憶體不受,堆內記憶體大小的限制,只受伺服器實體記憶體的大小限制。這三者之間的關係是這樣的:實體記憶體=堆外記憶體+堆內記憶體。
技術大佬的GITHUB地址:https://github.com/snazy/ohc 。
要使用他的技術就先要引用對應的jar包,Maven座標如下:
<dependency> <groupId>org.caffinitas.ohc</groupId> <artifactId>ohc-core</artifactId> <version>0.7.4</version> </dependency>
//大神給的使用方式如下: //Quickstart: OHCache ohCache = OHCacheBuilder.newBuilder() .keySerializer(yourKeySerializer) .valueSerializer(yourValueSerializer) .build();
上面是Quickstart 看起來使用如此的絲滑(簡單),但是上面的程式碼是填空題,我們看到複製貼上後程式碼不能使用後開始。。。。。。此處省略一萬字。其實大神寫的程式碼怎麼不能用的呢,不要懷疑大神一定是自己的方法不對,我們的口號是?
如果提供的程式碼複製貼上不能直接用的工程師不能稱之為大神工程師。
但是github上的大神寫的東西怎麼不能直接用呢?一定是你的思路不對,老司機都知道,大神寫程式碼一定有寫單元測試的,要不然不會有那麼多人用的(所以要想成為大神單元測試一定要寫好),所以把程式碼拉下來,複製單元測試的東西應該能直接使用 。下面是CTRL+C來的程式碼。
public static void main(String[] args) { OHCache ohCache = OHCacheBuilder.<String, String>newBuilder() .keySerializer(new StringSerializer()) .valueSerializer(new StringSerializer()) .build(); ohCache.put("name","xiaozhang"); System.out.println(ohCache.get("name")); // 結果 xiaozhang } static class StringSerializer implements CacheSerializer<String>{ @Override public void serialize(String value, ByteBuffer buf) { // 得到字串物件UTF-8編碼的位元組陣列 byte[] bytes = value.getBytes(Charsets.UTF_8); // 用前16位記錄陣列長度 buf.put((byte) ((bytes.length >>> 8) & 0xFF)); buf.put((byte) ((bytes.length) & 0xFF)); buf.put(bytes); } @Override public String deserialize(ByteBuffer buf) { // 判斷位元組陣列的長度 int length = (((buf.get() & 0xff) << 8) + ((buf.get() & 0xff))); byte[] bytes = new byte[length]; // 讀取位元組陣列 buf.get(bytes); // 返回字串物件 return new String(bytes, Charsets.UTF_8); } @Override public int serializedSize(String value) { byte[] bytes = value.getBytes(Charsets.UTF_8); // 設定字串長度限制,2^16 = 65536 if (bytes.length > 65536) throw new RuntimeException("encoded string too long: " + bytes.length + " bytes"); // 設定字串長度限制,2^16 = 65536 return bytes.length + 2; } }
上面的程式碼我們看到這傢伙類似Java中的Map 。也就是一個key和value 結構的物件。感覺So easy ,沒什麼大的用途。簡單?那是你想簡單了,後面大招來了。
很簡單的程式碼演示如下:
public class MapCasheTest { static HashMap<String,String> map = new HashMap<>(); public static void main(String[] args) throws Exception { oomTest(); } private static void oomTest() throws Exception{ // 休眠幾秒,便於觀察堆記憶體使用情況 TimeUnit.SECONDS.sleep(30); int result = 0 ; while (true){ String string = new String(new byte[1024*1024]) ; map.put(result+"",string) ; result++; } } }
執行一小會就報這個錯了,也是文章中剛開始說的那個錯誤。
然後我們去監控系統的堆疊使用情況如下圖(只用一小會就把堆記憶體快用滿了,然後自然系統就報錯了)
下面我們使用同樣的邏輯寫如下程式碼,很神奇的事情出現了,大跌眼鏡的事情出現了,先亮出程式碼如下:
public static void main(String[] args) throws Exception{ TimeUnit.SECONDS.sleep(30); OHCache ohCache = OHCacheBuilder.<String, String>newBuilder() .keySerializer(new Test.StringSerializer()) .valueSerializer(new Test.StringSerializer()) .build(); int result = 0 ; while (true){ String string = new String(new byte[2048*2048]) ; ohCache.put(result+"",string) ; result++; } }
程式一直很穩定的執行,沒有報錯,堆疊執行一上一下,至少程式沒報錯:
為什麼會出現這種情況呢?因為文章開始就講了這個用的是堆外記憶體,也就是用的自己電腦的記憶體,自己電腦的記憶體目前普通的電腦也有2個G那麼大,所以程式一直執行穩定。下面看看我們CPU執行的情況,剛開始有點高,當我把程式關閉CPU使用立馬下來了。
哈哈哈,如果是自己測試的時候記得要把自己的工作相關的東西都先儲存了,免得你的電腦記憶體太小,有可能會造成電腦關機。
那麼我們在什麼情況會使用這個大神的工具呢?就是自己程式有大的物件,並且GC一直無法把這個大物件回收,就可以使用上面的方法,能保證程式穩定執行。具體OH大神是怎麼實現這種方式的呢?本文不做研究,他用的技術太深了。
最近ChatGPT比較火,我也問了些問題,回得很好,給個贊。你如果有啥想問的我也可以幫你問問它。
歡迎關注微信公眾號:程式設計師xiaozhang 。會更新更多精彩內容。