<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在Java虛擬機器器規範的描述中,除了程式計數器外,虛擬機器器記憶體的其他幾個執行時區域都有發生OutOfMemoryError (下文稱OOM)異常的可能。本篇主要結合著【深入理解Java虛擬機器器】一書當中整理了本篇部落格,感興趣的跟著小編一塊來學習呀!
本篇文章和上一篇寫到的 Java記憶體區域劃分 息息相關,如果您對Java記憶體區域劃分不是很瞭解,建議瞭解一下,不然這篇文章讀起來會很痛苦。。。
本節內容的目的有兩個:
通過程式碼驗證Java虛擬機器器規範中描述的各個執行時區域儲存的內容
;能根據異常的資訊快速判斷是哪個區域的記憶體溢位
,知道怎樣的程式碼可能會導致這些區域的記憶體溢位,以及出現這些異常後該如何處理。下面程式碼的開頭都註釋了執行時所需要設定的虛擬機器器啟動引數
(註釋中“VM Args”後面跟著的引數),這些引數對實驗的結果有直接影響,請讀者偵錯程式碼的時候不要忽略掉。(本篇文章所有案例都採用了JDK1.8版本進行測試)
如果讀者使用控制檯命令來執行程式,那直接跟在Java命令之後書寫就可以。如果讀者使用Eclipse IDE,可以在Debug/Run頁籤中的設定。
Java堆用於儲存物件範例,我們只要不斷地建立物件,並且保證GC Roots到物件之間有可達路徑
來避免垃圾回收機制清除這些物件,就會在物件數量到達最大堆的容量限制後產生記憶體溢位異常。
將Java堆設定大小為20MB,不可延伸(將堆的最小值-Xms引數與最大值-Xmx引數設定為一樣即可避免堆自動擴充套件),通過引數-XX:+HeapDumpOnOutOfMemoryError 可以讓虛擬機器器在出現記憶體溢位異常時Dump出當前的記憶體堆轉儲快照以便事後進行分析(記憶體堆轉儲快照 指的是溢位後,記憶體當中的物件佔用情況)。
我用的是ider:
設定啟動引數:
Xms:最小堆記憶體 Xmx:最大可延伸記憶體
XX:+HeapDumpOnOutOfMemoryError:可以讓虛擬機器器在出現記憶體溢位異常時Dump出當前的記憶體堆轉儲快照以便事後進行分析
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
import java.util.ArrayList; import java.util.List; public class HeapOOM { static class OOMObject{ } public static void main(String[] args) { List<OOMObject> list = new ArrayList<>(); while (true){ list.add(new OOMObject()); } }
執行結果:
因為設定了-XX:+HeapDumpOnOutOfMemoryError引數,所以生成了 這個報告。可以檢視物件佔用記憶體。
Java堆記憶體的OOM異常是實際應用中最常見的記憶體溢位異常情況。出現Java堆記憶體溢位時,異常堆疊資訊"java.lang.OutOfMemoryError”會跟著進一步提示“Java heapspace"。
要解決這個區域的異常,一般的手段是首先通過記憶體映像分析工具
(如EclipseMemory Analyzer、Dier的jprofiler)對dump出來的堆轉儲快照進行分析
,重點是確認記憶體中的物件是否是必要的
,也就是要先分清楚到底是出現了記憶體漏失(Memory Leak)還是記憶體溢位(Memory Overflow)。
如果是記憶體漏失,可進一步通過工具檢視洩漏物件到GC Roots的參照鏈
。於是就能找到洩漏物件是通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收它們的。掌握了洩漏物件的型別資訊,以及GC Roots參照鏈的資訊,就可以比較準確地定位出洩漏程式碼的位置。
如果不存在洩漏,換句話說就是記憶體中的物件確實都還必須存活著,那就應當檢查虛擬機器器的堆引數
(-Xmx與-Xms),與機器實體記憶體對比看是否還可以調大,從程式碼上檢查是否存在某些物件生命週期過長、持有狀態時間過長
的情況,嘗試減少程式執行期的記憶體消耗。
後面我會專門寫一篇關於記憶體分析工具的部落格,XX:+HeapDumpOnOutOfMemoryError這個只是有記憶體佔用情況,工具可以幫我們看到物件的參照鏈情況。
由於在HotSpot虛擬機器器中並不區分虛擬機器器棧和本地方法棧,因此對於HotSpot來說,-Xoss引數(設定本地方法棧大小)雖然存在,但實際上是無效的,棧容量只由-Xss引數設定。關於虛擬機器器棧和本地方法棧,在Java虛擬機器器規範中描述了兩種異常。
注意:HotSpot虛擬機器器的棧容量是不可以動態擴充套件的。
擴充套件時無法申請到足夠的記憶體時會丟擲OutOfMemoryError異常
public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } }
實驗結果表明:在單個執行緒下,無論是由於棧幀太大,還是虛擬機器器棧容量太小,當記憶體無法分配的時候,虛擬機器器丟擲的都是StackOverflowError異常。換成遠古時代的Classic虛擬機器器,這款虛擬機器器可以支援動態擴充套件 棧記憶體的容量
,這時候就會報StackOverflowError異常了。
也就是當我設定-Xss128k和不設定都是報同樣的錯誤
,並沒有出現記憶體溢位異常,原因就是 HotSpot虛擬機器器的棧容量是不可以動態擴充套件的
,但是值得注意的是我的電腦是16G執行記憶體的,當我設定-Xss128k的時候輸出的長度是將近1000,當我不限制-Xss128k大小的時候輸出的長度是20000左右,也就意味著每個執行緒的棧幀大小預設最大是2MB
。
如果測試時不限於單執行緒,通過不斷地建立執行緒的方式倒是可以產生記憶體溢位異常,在這種情況下,給每個執行緒的棧分配的記憶體越大,反而越容易產生記憶體溢位異常。
原因其實不難理解,作業系統分配給每個程序的記憶體是有限制的,譬如32位元Windows的單個程序 最大記憶體限制為2GB。HotSpot虛擬機器器提供了引數可以控制Java堆和方法區這兩部分的記憶體的最大值。那麼虛擬機器器棧和本地方法棧記憶體如下:
虛擬機器器棧和本地方法棧記憶體=2GB-最大堆容量-最大方法區容量-程式計數器容量
因此為每個執行緒分配到的棧記憶體越大,可以建立的執行緒數量自 然就越少,建立執行緒時就越容易把剩下的記憶體耗盡。
通過上面瞭解到,出現StackOverflowError異常時有錯誤堆疊可以閱讀,相對來說,比較容易找到問題的所在(一般出現死迴圈可能會導致)。
如果是建立過多執行緒導致的記憶體溢位,而不是棧溢位,在不能減少執行緒數或者更換64位元虛擬機器器的情況下,就只能通過減少最大堆和減少棧容量來換取更多的執行緒
。如果沒有這方面的經驗,這種通過“減少記憶體”的手段來解決記憶體溢位的方式會比較難以想到。
public class JavaVMStackOOM { private void dontStop(){ while (true){ } } public void stackLeakByThread(){ while (true){ Thread thread = new Thread(new Runnable() { @Override public void run() { dontStop(); } }); thread.start(); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } }
注意
重點提示一下,如果讀者要嘗試執行上面這段程式碼,記得要先儲存當前的工作,由於在 Windows平臺的虛擬機器器中,Java的執行緒是對映到作業系統的核心執行緒上,無限制地建立執行緒會對操 作系統帶來很大壓力,上述程式碼執行時有很高的風險,可能會由於建立執行緒數量過多而導致作業系統 假死
(電腦可能直接宕機)。
在32位元作業系統下的執行結果:
原因:32位元有程序大小記憶體限制。
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
注意:如果要測試上面記憶體溢位程式碼,記住先儲存當前的工作,避免電腦卡死帶來的麻煩。
由於執行時常數池是方法區的一部分
,所以這兩個區域的溢位測試可以放到一起進行。前面曾經 提到HotSpot從JDK 7開始逐步“去永久代”的計劃,並在JDK 8中完全使用元空間來代替永久代
,在此我們就以測試程式碼來觀察一下,使用“永久代”還是“元空間”來實現方法區,對程式有什麼 實際的影響。
String::intern()是一個本地方法
,它的作用是如果字串常數池中已經包含一個等於此String物件的 字串,則返回代表池中這個字串的String物件的參照
;否則,會將此String物件包含的字串新增 到常數池中,並且返回此String物件的參照
。
import java.util.ArrayList; import java.util.List; public class RuntimeConstantPoolOOM { public static void main(String[] args) { // 使用List保持著常數池參照,避免Full GC回收常數池行為 List<String> list = new ArrayList<>(); // 10MB的PerSize在integer範圍內足夠產生00M int i = 0; while (true){ list.add(String.valueOf(i++).intern()); } } }
使用JDK 7或更高版本的JDK來執行這段程式並不會得到相同的結果,無論是在JDK 7中繼續使 用-XX:MaxPermSize
引數或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize
引數把方法區容量同 樣限制在6MB,都不會出現溢位異常,迴圈將一直進行下去,永不停歇。出現這種變 化,是因為自JDK 7起,原本存放在永久代的字串常數池被移至Java堆之中,所以在JDK 7及以上版 本,限制方法區的容量對該測試用例來說是毫無意義的
。
在JDK1.7中(包括1.7以上)常數池儲存的不再是物件,而是物件參照,真正的物件是儲存在堆中的。把RuntimeConstantPoolOOM.java執行時的VM引數改為如下(設定堆大小)所示:
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
執行結果:
檢視生成的堆記憶體快照:
方法區用於存放Class的相關資訊,如類名、存取修飾符、常數池、欄位描述、方法描述等。對於這個區域的測試,基本的思路是執行時產生大量的類去填滿方法區,直到溢位。雖然直接使用Java SE API也可以動態產生類(如反射時的GeneratedConstructorAccessor 和動態代理等),但在本次實驗中操作起來比較麻煩。藉助CGLib直接操作位元組碼執行時,生成了大量的動態類。
值得特別注意的是,我們在這個例子中模擬的場景並非純粹是一個實驗,這樣的應用經常會出現在實際應用中:當前的很多主流框架,如Spring和Hibernate對類進行增強時,都會使用到CGLib這類位元組碼技術,增強的類越多,就需要越大的方法區來保證動態生成的Class可以載入人記憶體
。
測試範例:
import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class JavaMethodAreaOOM { public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } } static class OOMObject { } }
設定元空間最大空間,和初始化空間引數:
類資訊是都存在方法區的,方法區在jdk1.8將永久區改為了元空間。自此以後,常數池在元空間都是儲存的參照。實際物件是在堆中。
-XX:MaxMetaspaceSize=10m -XX:MetaspaceSize=10m
執行結果:
方法區溢位也是一種常見的記憶體溢位異常,一個類如果要被垃圾收集器回收,要達成的條件是比 較苛刻的。在經常執行時生成大量動態類的應用場景裡,就應該特別關注這些類的回收狀況。這類場 景除了之前提到的程式使用了CGLib位元組碼增強和動態語言外,常見的還有:大量JSP或動態產生JSP 檔案的應用(JSP第一次執行時需要編譯為Java類)、基於OSGi的應用(即使是同一個類檔案,被不同 的載入器載入也會視為不同的類)等。
直接記憶體(Direct Memory)並不是虛擬機器器執行時資料區的一部分,也不是《Java虛擬機器器規範》中 定義的記憶體區域。但是這部分記憶體也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現。
直接記憶體:可以使用Native函數庫直接分配堆外記憶體,然後通過一個儲存在Java堆裡面的DirectByteBuffer物件作為這塊記憶體的參照`進行操作。這樣能在一些場景中顯著提高效能,因為避免了在Java堆和Native堆中來回複製資料(但是有一點注意,雖然不佔用堆記憶體,但是他佔用了伺服器記憶體)。
直接記憶體(Direct Memory)的容量大小可通過-XX:MaxDirectMemorySize引數來指定,如果不 去指定,則預設與Java堆最大值(由-Xmx指定)一致。
程式碼範例:
越過了DirectByteBuffer類直接通 過反射獲取Unsafe範例進行記憶體分配
(Unsafe類的getUnsafe()
方法指定只有引導類載入器才會返回實 例,體現了設計者希望只有虛擬機器器標準類庫裡面的類才能使用Unsafe的功能,在JDK 10時才將Unsafe 的部分功能通過VarHandle開放給外部使用
),因為雖然使用DirectByteBuffer分配記憶體也會丟擲記憶體溢 出異常,但它丟擲異常時並沒有真正向作業系統申請分配記憶體,而是通過計算得知記憶體無法分配就會 在程式碼裡手動丟擲溢位異常,真正申請分配記憶體的方法是Unsafe::allocateMemory()
。
import sun.misc.Unsafe; import java.lang.reflect.Field; public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } }
執行引數:
-Xmx20M -XX:MaxDirectMemorySize=10M -XX:+HeapDumpOnOutOfMemoryError
執行結果:
我設定了-XX:+HeapDumpOnOutOfMemoryError發現執行完成之後並沒有發現有記憶體快照。
由直接記憶體導致的記憶體溢位,一個明顯的特徵是在Heap Dump檔案中不會看見有什麼明顯的異常 情況,如果讀者發現記憶體溢位之後產生的Dump檔案很小,而程式中又直接或間接使用了 DirectMemory(典型的間接使用就是NIO),那就可以考慮重點檢查一下直接記憶體方面的原因了。
堆:
-Xms3550m
:設定JVM初始記憶體為3550M。表示初始化JAVA堆的大小及該程序剛建立出來的時候,他的專屬JAVA堆的大小,一旦物件容量超過了JAVA堆的初始容量,JAVA堆將會自動擴容到-Xmx大小。-Xmx3550m
:設定JVM最大可用記憶體為3550M。表示java堆可以擴充套件到的最大值,在很多情況下,通常將-Xms和-Xmx設定成一樣的,因為當堆不夠用而發生擴容時,會發生記憶體抖動影響程式執行時的穩定性。棧:
-Xss128k
:規定了每個執行緒虛擬機器器棧及堆疊的大小,一般情況下,256k是足夠的,此設定將會影響此程序中並行執行緒數的大小(和堆是不一樣的,不支援動態擴充套件)。方法區:
-XX:PermSize
設定永久代初始大小。-XX:MaxPermSize
設定永久代最大可分配空間。(JDK7目前已經很少用了,這兩個引數在JDK8及以後已經沒有了,所以不必掌握,瞭解一下)-XX:MaxMetaspaceSize
=10m:設定元空間最大值,預設是-1,即不限制,或者說只受限於本地記憶體 大小。-XX:MetaspaceSize
=10m:指定元空間的初始空間大小,以位元組為單位,達到該值就會觸發垃圾收集 進行型別解除安裝,同時收集器會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放 了很少的空間,那麼在不超過-XX:MaxMetaspaceSize(如果設定了的話)的情況下,適當提高該值。-XX:MinMetaspaceFreeRatio
:作用是在垃圾收集之後控制最小的元空間剩餘容量的百分比,可 減少因為元空間不足導致的垃圾收集的頻率。類似的還有-XX:Max-MetaspaceFreeRatio,用於控制最 大的元空間剩餘容量的百分比。記憶體:
-XX:+HeapDumpOnOutOfMemoryError
可以讓虛擬機器器在出現記憶體溢位異常時Dump出當前的記憶體堆轉儲快照以便事後進行分析(記憶體堆轉儲快照 指的是溢位後,記憶體當中的物件佔用情況)GC:
-XX:-PrintGCDetails
:每次GC時列印詳細資訊。public static void main(String[] args) { String str1 = new StringBuilder("計算機").append("軟體").toString(); System.out.println(str1.intern() == str1); String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern() == str2); }
這段程式碼在JDK 6中執行,會得到兩個false,而在JDK 7中執行,會得到一個true和一個false。在jdk1.8執行也是,true、false。
產 生差異的原因是,在JDK 6中,intern()方法會把首次遇到的字串範例複製到永久代的字串常數池 中儲存,
返回的也是永久代裡面這個字串範例的參照
,而由StringBuilder建立的字串物件範例在 Java堆上
,所以必然不可能是同一個參照,結果將返回false。
而JDK 7(以及部分其他虛擬機器器,例如JRockit)的intern()方法實現就不需要再拷貝字串的範例 到永久代了,既然字串常數池已經移到Java堆中,那隻需要在常數池裡記錄一下首次出現的範例引 用即可,因此intern()返回的參照和由StringBuilder建立的那個字串範例就是同一個。
而對str2比較返 回false,這是因為“java
”這個字串在執行String-Builder.toString()之前就已經出現過了
,字串常數 池中已經有它的參照,不符合intern()方法要求“首次遇到”的原則,“計算機軟體”這個字串則是首次 出現的,因此結果返回true。(這塊說實話不好理解,說白了就是java是個特殊的字串,他在常數池裡面就一直存在)
總結:在1.8之後通過intern()新增到常數池,只有字串在常數池不存在的時候才會返回字串的參照。
到此為止,我們明白了虛擬機器器裡面的記憶體是如何劃分的,哪部分割區域、什麼樣的程式碼和操作可能 導致記憶體溢位異常。雖然Java有垃圾收集機制,但記憶體溢位異常離我們並不遙遠,本章只是講解了各 個區域出現記憶體溢位異常的原因,下一章將詳細講解Java垃圾收集機制為了避免出現記憶體溢位異常都 做了哪些努力。
到此這篇關於Java實戰之OutOfMemoryError異常的文章就介紹到這了,更多相關java OutOfMemoryError異常內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45