冷静对待你遇到的所有Java内存异常
被人說爛的Java內存模型
Java內存模型的相關資料在網上實在是太多了,不管是過時的還是不過時的,網絡上充斥的學習資料,比如各類研究Java內存模型的博文,也隨著Java的發展,漸漸失去了其內容的準確性.
要在那么多網絡資料中找到對Java內存模型最新最全的說法,估計最好的方式只有翻閱Oracle的文檔了!(字體大小太不舒服了!)
我最近也不停的查閱和總結了不少網上的資料,不過由于類似資料實在太多,所以不打算重復的說明這個被人說爛的Java內存模型
從各種OOM異常出發來零距離的理解Java內存模型
對于大腦來說, 大腦更喜歡問題, 而不是從陳述開始.
本文會從平時工作中可能會遇到的OOM異常出發,來一步步的深入理解我們所熟知的Java內存模型,從而哪怕可以更加理解一點這些方面的編程思想和設計精髓, 也是一個不小的進步
java.lang.StackOverFlowError
這個Stack是什么鬼東西
Stack是個棧, 是一種數據結構, 會占用一塊內存空間
Java在哪些地方會使用Stack來存儲數據
最常見的就是虛擬機棧, 它是專門為java Method執行服務的一塊內存, 每個方法調用都會往這個棧中壓入一個棧幀(stackFrame), 由于方法可以互調,迭代,所以使用棧模型來服務Java Method是很適合的一種數據結構模型
別忘了還有一個本地方法棧, 它是專門為java的底層native方法執行服務的一塊內存. 然而由于native方法都是術語jdk內部的測試穩定的程序,所以作為應用java開發人員的我們,一般是不可能遇到這個層面拋出的這個異常,同時我也幾乎可以判斷這種方法是不會直接拋出java.lang.StackOverFlowError異常的,所以我們可以縮小我們的關注范圍,把拋出這個異常的原因全部指向于虛擬機棧即可
這種異常是如何發生的?
我們知道每調用一次Java Method,就會往虛擬機棧中壓入一個棧幀,在方法結束之前都不會出棧. 所以可以直接推理出在一個java線程運行過程中,如果同時調用的方法過多(比如遞歸的調用一個方法),就會出現這個異常
事實上,除了惡性遞歸或者虛擬機棧可用內存過小的情況下, 也很難觸發這種異常, 所以一般來說遇到這種異常幾乎是可以直接斷定程序中存在惡性遞歸導致的.
這類問題在實際開發中遇到的并不多, 反而是在做一些算法問題的時候, 由于自己的疏忽從而引發不可預知的惡性遞歸
一個簡單的Demo復現這種異常
public class Main {public static void main(String[] args) {Main.main(null);} }上述代碼就會報StackOverFlowError, 因為main方法會被不停的循環執行, 直到超出虛擬機棧能夠承受的大小
相關JVM參數
-Xss, 正常取值128K~256K, 如果仍然不夠可以進行加大, 這個選項對性能影響比較大,需要嚴格的測試哦
java.lang.OutOfMemoryError: Java heap space
這個異常表示, Java程序運行過程中遭遇了內存超限問題, 根本原因是Java的堆(Heap)內存超限
Java常用的內存空間對應計算機硬件是哪些組件?
什么是Java的堆內存(Heap)
這就涉及了Java的運行時內存模型了~
我就簡單來說下吧~
一個JVM進程運行后, 會有一個主線程去運行我們寫的Java程序, 那么每一個這種線程都擁有兩大塊內存空間
- 線程共享內存空間
- 堆(Heap, 所有java的對象實例和數組,jdk8后還存放了字符串常量池和類靜態變量)
- 方法區(存放類元數據,符號引用,靜態常量,jdk8后HotSpot將其從永久代移動到了Metaspace)
- 線程獨享內存空間
- 虛擬機棧(為Java方法提供的一塊內存空間,內部有棧幀組成)
- 本地方法棧(為Java的native方法)
- 程序計數器(PC寄存器,記錄執行行號)
所以Java的堆內存就是JVM中設定的一塊專門存儲所有java的對象實例和數組,jdk8后甚至包括字符串常量池和類靜態變量的內存區域
這種異常是如何發生的?
如果是1.7以前, Java堆溢出的問題根源是簡單的, 就是運行時存在的對象實例和數組太多了!
但是在1.8后, 由于還存放了字符串常量, 所以出現異常還有一種可能就是 interned Strings 過多導致的哦!
最小復現Demo
執行前最好先修改下JVM參數,防止等待時間過長
JVM參數:
-Xms20m
-Xmx20m
-XX:MetaspaceSize=10m
-XX:MaxMetaspaceSize=10m
-XX:-UseGCOverheadLimit
( 相關JVM參數 -Xms : 初始堆大小 -Xmx : 最大堆大小 )
JVM參數說明:
限制堆大小20M,方便快速報錯! 由于我用的是jdk8,所以限制了元空間的大小為10m,說實話在這個情況下沒啥用哈哈哈哈哈哈哈(就是覺得加上去舒服才加的,不信我說的你可以自己google)!最后一個參數-XX:-UseGCOverheadLimit這個有必要加一下. 因為我的demo程序屬于那種惡意的程序,所以一次GC幾乎沒辦法清理任何對象實例,因為他們都在被占用著! 所以必須使用這個參數來防止GC檢測出我的這種惡意程序,從而正常的提示堆溢出的錯誤而不是GC Overhead limit exceeded錯誤(這個錯誤會在后面細講) – 我自己試驗了下,不加最后的-XX:-UseGCOverheadLimit 也會正常提示堆溢出
代碼說明: 這串代碼會每次生成一個新的interned String, 也就是數字遞增對應的String表示, 所以最終爆掉內存, 證明了是interned Strings爆掉了內存, 相同的代碼在jkd1.7以前是不會報堆內存溢出的, 請注意
如何處理?
查看jvm快照,分析占用內存大的對象是哪些, 然后定位到代碼位置, 最后進行優化
我一般使用visualVM來查看這類問題
java.lang.OutOfMemoryError: GC Overhead limit exceeded
這個異常表示您的Java程序在運行的時候, 98%的時間都在執行GC回收, 但是每次回收只回收不到2%的空間!
換句話說,其實這個異常往往是拋出java.lang.OutOfMemoryError: Java heap space異常的前兆! 因為Java程序每次都GC回收只能回收一點點內存空間,而你的程序卻仍然在不停的產生新的對象實例, 這無疑導致了兩種可能結果:
這個問題還有一些細節需要我們去掌握,我們先從下面的例子來看吧
最小復現Demo
public static void main(String args[]) throws Exception {Map map = System.getProperties();Random r = new Random();while (true) {map.put(r.nextInt(), "value");}}代碼說明: 這段代碼不停的往map中加入新的key-value,導致map大小不斷變大! 當到達堆內存頂點的時候,GC發生, 但是清理完畢后,JVM發現清理前后的堆內存大小改變很小,不到2%; 這時候程序繼續運行,繼續往map中加數據!GC又發生了!又只清理不到2%! 如此不停的循環, 最后JVM得出了一個判斷! 你的Java程序在占用CPU進行運算的時間里,98%的時間都特么的在垃圾回收,而每次GC居然只能回收堆內存的2%空間, 這肯定是代碼存在問題,于是拋出了這個異常. 如果這個時候,你斷定不是自己的代碼問題, 使用JVM參數-XX:-UseGCOverheadLimit來關閉這種檢查! 然后你就會發現你的程序拋出了堆溢出異常! 為什么呢? 因為堆內存不斷的被占滿,最終導致最后一次加入新的int的時候, 堆內存空間直接不足了!
這個異常一般如何處理
和堆溢出的解決方式一致
相關JVM參數 -XX:-UseGCOverheadLimit
java.lang.OutOfMemoryError: Permgen space (jdk8已經不會出現此異常,請注意)
只存在于jdk1.8以前的java程序中! 這個異常表示,永久代大小不夠!
什么是Permgen
是HotSpot在jdk1.8以前存在的一個區域,用于實現方法區
什么時候會產生這個錯誤以及如何解決
由于是實現方法區的地方, 所以肯定是類元信息或者常量(jdk1.7后部分常量已經挪到堆中),靜態常量和JIT即時編譯器編譯后的代碼等數據太多導致大小不夠
乍一看也許你會頭暈! 不過沒關系, 根據我兩年的開發經驗, 我碰到過的唯一一次Permgen space問題是因為SpringIoC容器一口氣加載了過多的Bean導致的!
所以正常來說, 直接擴大這個區域的大小即可!
比如使用如下JVM參數擴大:
-XX:MaxNewSize=xxxm -XX:MaxPermSize=xxxm
最小復現Demo
運行要求: jdk版本 <= 1.6
import javassist.ClassPool;public class MicroGenerator {public static void main(String[] args) throws Exception {for (int i = 0; i < 100_000_000; i++) {generate("eu.plumbr.demo.Generated" + i);}}public static Class generate(String name) throws Exception {ClassPool pool = ClassPool.getDefault();return pool.makeClass(name).toClass();} }借助了javassist來不停的加載新的class,直至爆掉永久代區域
相關JVM參數
-XX:PermSize=xxxm , -XX:MaxPermSize=xxxm
java.lang.OutOfMemoryError: Metaspace (since jdk8 才有可能拋出的錯誤)
這個異常表示: Metaspace的空間不足導致OOM異常發生
什么是Metaspace
有些不太專注JVM知識的小伙伴可能對Metaspace是陌生的, 因為這玩意是jdk8開始才正式登場的一塊內存區域. 它專門用于替代原來的永久代, 且存在于本地內存中, 所以它的最大內存理論就是你電腦的最大內存. 和永久代不一樣的是, 它可以進行自我擴容, 直到達到規定的MaxMetaspaceSize或者到達本機的最大可用內存為止.
Metaspace接替了永久代的任務, 方法區的內容全部轉移到此處(除了字符串常量池被挪到了堆中)
不過相比于永久代, Metaspace進行GC的時候, 稍微改變了一點規則, Metaspace中類元數據是否需要回收是根據類加載器死活來來決定的, 這不同于永久代的, 只要類引用消失就會被回收. 這種規則會產生一些問題:
所以在jdk8后使用反射,動態代理等會生成class對象的方法, 一定要小心MetaSpace是否會對其進行回收, 如果不會, 則需要進行相應的優化處理
為什么要移除永久代
方法區大小難以設定,容易發生內存溢出。永久代會存放Class的相關信息,一般這些信息在編譯期間就能確定大小。但是如果是在一些需要動態生成大量Class的應用中,如:Spring的動態代理、大量的JSP頁面或動態生成JSP頁面等,由于方法區的大小在一開始就要分配好,因此就能難確定大小,容易出現內存溢出
GC復雜且效率低。方法區存儲了類的元數據信息和各種常量,它的內存回收目標理應當是對這些類型的卸載和常量的回收。但由于這些數據被類的實例引用,卸載條件變得復雜且嚴格,回收不當會導致堆中的類實例失去元數據信息和常量信息。因此,回收方法區內存不是一件簡單高效的事情。
促進HotSpot JVM與JRockit VM的融合。JRockit沒有方法區,移除永久代可以促進HotSpot JVM與JRockit VM的融合。
最小復現Demo
/**-XX:MetaspaceSize=8m-XX:MaxMetaspaceSize=8m*/ public class MetaSpaceOOMTest {public static void main(String[] args) {while (true) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(OOMObject.class);enhancer.setUseCache(false);enhancer.setCallback(new MethodInterceptor() {public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {return proxy.invokeSuper(obj, args);}});//無限創建動態代理,生成Class對象enhancer.create();}}static class OOMObject {} }如何解決這類異常
相關JVM參數
-XX:MetaspaceSize=8m
-XX:MaxMetaspaceSize=8m
java.lang.OutOfMemoryError: Unable to create new native thread
這個異常表示,JVM無法再創建新的線程了!JVM能夠創建的線程數是有限制的
復現demo
public class TestNativeOutOfMemoryError { public static void main(String[] args) { for (int i = 0;; i++) { System.out.println("i = " + i); new Thread(new HoldThread()).start(); } } } class HoldThread extends Thread { CountDownLatch cdl = new CountDownLatch(1); public HoldThread() { this.setDaemon(true); } public void run() { try { cdl.await(); } catch (InterruptedException e) { } } }解決方案
java.lang.OutOfMemoryError: request size bytes for reason
如果你看到了這個異常, 說明你的OS內存不夠用了, JVM想本地操作系統申請內存被拒絕, 導致JVM進程無法繼續運行! 發生這個問題的原因一般是你的Java程序需要的內存容量超過了操作系統可提供給JVM的最大內存容量, 連swap內存都沒了
java.lang.OutOfMemoryError: Requested array size exceeds VM
當你正準備創建一個超過虛擬機允許的大小的數組時,這條錯誤就會出現在你眼前!
尾
本文對java常見的OOM異常做了總結說明,同時對于涉及的Java內存模型進行了說明,希望可以在日后遇到類似問題的時候可以沉著冷靜,不慌不忙的來排查問題
參考:
Understanding Java Memory Model
你還在看《深入理解Java虛擬機》的運行時數據模型嗎?
假笨說-謹防JDK8重復類定義造成的內存泄漏
https://www.zhihu.com/question/39990490/answer/369690291
原文作者:ZAZALU’S BLOG
原文鏈接: https://zazalu.space/2019/09/17/java-memory-error-solution-Theoretically/
總結
以上是生活随笔為你收集整理的冷静对待你遇到的所有Java内存异常的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: QUIC实战(四) 设置应用开机自启动
- 下一篇: 深入理解java虚拟机(十三) Java