Java實(shí)戰(zhàn)之OutOfMemoryError異常問題及解決方法
在Java虛擬機(jī)規(guī)范的描述中,除了程序計(jì)數(shù)器外,虛擬機(jī)內(nèi)存的其他幾個(gè)運(yùn)行時(shí)區(qū)域都有發(fā)生OutOfMemoryError (下文稱OOM)異常的可能。本篇主要結(jié)合著【深入理解Java虛擬機(jī)】一書當(dāng)中整理了本篇博客,感興趣的跟著小編一塊來學(xué)習(xí)呀!
本篇文章和上一篇寫到的 Java內(nèi)存區(qū)域劃分 息息相關(guān),如果您對(duì)Java內(nèi)存區(qū)域劃分不是很了解,建議了解一下,不然這篇文章讀起來會(huì)很痛苦。。。
一、簡(jiǎn)言
本節(jié)內(nèi)容的目的有兩個(gè):
- 第一,
通過代碼驗(yàn)證Java虛擬機(jī)規(guī)范中描述的各個(gè)運(yùn)行時(shí)區(qū)域儲(chǔ)存的內(nèi)容
; - 第二,希望讀者在工作中遇到實(shí)際的內(nèi)存溢出異常時(shí),
能根據(jù)異常的信息快速判斷是哪個(gè)區(qū)域的內(nèi)存溢出
,知道怎樣的代碼可能會(huì)導(dǎo)致這些區(qū)域的內(nèi)存溢出,以及出現(xiàn)這些異常后該如何處理。
下面代碼的開頭都注釋了執(zhí)行時(shí)所需要設(shè)置的虛擬機(jī)啟動(dòng)參數(shù)
(注釋中“VM Args”后面跟著的參數(shù)),這些參數(shù)對(duì)實(shí)驗(yàn)的結(jié)果有直接影響,請(qǐng)讀者調(diào)試代碼的時(shí)候不要忽略掉。(本篇文章所有案例都采用了JDK1.8版本進(jìn)行測(cè)試)
如果讀者使用控制臺(tái)命令來執(zhí)行程序,那直接跟在Java命令之后書寫就可以。如果讀者使用Eclipse IDE,可以在Debug/Run頁簽中的設(shè)置。
二、代碼實(shí)戰(zhàn)
1、Java堆溢出
Java堆用于儲(chǔ)存對(duì)象實(shí)例,我們只要不斷地創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑
來避免垃圾回收機(jī)制清除這些對(duì)象,就會(huì)在對(duì)象數(shù)量到達(dá)最大堆的容量限制后產(chǎn)生內(nèi)存溢出異常。
將Java堆設(shè)置大小為20MB,不可擴(kuò)展(將堆的最小值-Xms參數(shù)與最大值-Xmx參數(shù)設(shè)置為一樣即可避免堆自動(dòng)擴(kuò)展),通過參數(shù)-XX:+HeapDumpOnOutOfMemoryError 可以讓虛擬機(jī)在出現(xiàn)內(nèi)存溢出異常時(shí)Dump出當(dāng)前的內(nèi)存堆轉(zhuǎn)儲(chǔ)快照以便事后進(jìn)行分析(內(nèi)存堆轉(zhuǎn)儲(chǔ)快照 指的是溢出后,內(nèi)存當(dāng)中的對(duì)象占用情況)。
我用的是ider:
設(shè)置啟動(dòng)參數(shù):
Xms:最小堆內(nèi)存 Xmx:最大可擴(kuò)展內(nèi)存
XX:+HeapDumpOnOutOfMemoryError:可以讓虛擬機(jī)在出現(xiàn)內(nèi)存溢出異常時(shí)Dump出當(dāng)前的內(nèi)存堆轉(zhuǎn)儲(chǔ)快照以便事后進(jìn)行分析
-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()); } }
運(yùn)行結(jié)果:
因?yàn)樵O(shè)置了-XX:+HeapDumpOnOutOfMemoryError參數(shù),所以生成了 這個(gè)報(bào)告??梢圆榭磳?duì)象占用內(nèi)存。
Java堆內(nèi)存的OOM異常是實(shí)際應(yīng)用中最常見的內(nèi)存溢出異常情況。出現(xiàn)Java堆內(nèi)存溢出時(shí),異常堆棧信息"java.lang.OutOfMemoryError”會(huì)跟著進(jìn)一步提示“Java heapspace"。
要解決這個(gè)區(qū)域的異常,一般的手段是首先通過內(nèi)存映像分析工具
(如EclipseMemory Analyzer、Dier的jprofiler)對(duì)dump出來的堆轉(zhuǎn)儲(chǔ)快照進(jìn)行分析
,重點(diǎn)是確認(rèn)內(nèi)存中的對(duì)象是否是必要的
,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)。
如果是內(nèi)存泄漏,可進(jìn)一步通過工具查看泄漏對(duì)象到GC Roots的引用鏈
。于是就能找到泄漏對(duì)象是通過怎樣的路徑與GC Roots相關(guān)聯(lián)并導(dǎo)致垃圾收集器無法自動(dòng)回收它們的。掌握了泄漏對(duì)象的類型信息,以及GC Roots引用鏈的信息,就可以比較準(zhǔn)確地定位出泄漏代碼的位置。
如果不存在泄漏,換句話說就是內(nèi)存中的對(duì)象確實(shí)都還必須存活著,那就應(yīng)當(dāng)檢查虛擬機(jī)的堆參數(shù)
(-Xmx與-Xms),與機(jī)器物理內(nèi)存對(duì)比看是否還可以調(diào)大,從代碼上檢查是否存在某些對(duì)象生命周期過長、持有狀態(tài)時(shí)間過長
的情況,嘗試減少程序運(yùn)行期的內(nèi)存消耗。
后面我會(huì)專門寫一篇關(guān)于內(nèi)存分析工具的博客,XX:+HeapDumpOnOutOfMemoryError這個(gè)只是有內(nèi)存占用情況,工具可以幫我們看到對(duì)象的引用鏈情況。
2、虛擬機(jī)棧和本地方法棧溢出
由于在HotSpot虛擬機(jī)中并不區(qū)分虛擬機(jī)棧和本地方法棧,因此對(duì)于HotSpot來說,-Xoss參數(shù)(設(shè)置本地方法棧大小)雖然存在,但實(shí)際上是無效的,棧容量只由-Xss參數(shù)設(shè)定。關(guān)于虛擬機(jī)棧和本地方法棧,在Java虛擬機(jī)規(guī)范中描述了兩種異常。
注意:HotSpot虛擬機(jī)的棧容量是不可以動(dòng)態(tài)擴(kuò)展的。
- 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常
- 允許??臻g動(dòng)態(tài)擴(kuò)展時(shí),當(dāng)
擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存時(shí)會(huì)拋出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; } } }
- 當(dāng)聲明是基本類型的變量的時(shí),其變量名及值(變量名及值是兩個(gè)概念)是放在JAVA虛擬機(jī)棧中
- 當(dāng)聲明的是引用變量時(shí),所聲明的變量(該變量實(shí)際上是在方法中存儲(chǔ)的是內(nèi)存地址值)是放在JAVA虛擬機(jī)的棧中,該變量所指向的對(duì)象是放在堆類存中的。
實(shí)驗(yàn)結(jié)果表明:在單個(gè)線程下,無論是由于棧幀太大,還是虛擬機(jī)棧容量太小,當(dāng)內(nèi)存無法分配的時(shí)候,虛擬機(jī)拋出的都是StackOverflowError異常。換成遠(yuǎn)古時(shí)代的Classic虛擬機(jī),這款虛擬機(jī)可以支持動(dòng)態(tài)擴(kuò)展 棧內(nèi)存的容量
,這時(shí)候就會(huì)報(bào)StackOverflowError異常了。
也就是當(dāng)我設(shè)置-Xss128k和不設(shè)置都是報(bào)同樣的錯(cuò)誤
,并沒有出現(xiàn)內(nèi)存溢出異常,原因就是 HotSpot虛擬機(jī)的棧容量是不可以動(dòng)態(tài)擴(kuò)展的
,但是值得注意的是我的電腦是16G運(yùn)行內(nèi)存的,當(dāng)我設(shè)置-Xss128k的時(shí)候輸出的長度是將近1000,當(dāng)我不限制-Xss128k大小的時(shí)候輸出的長度是20000左右,也就意味著每個(gè)線程的棧幀大小默認(rèn)最大是2MB
。
如果測(cè)試時(shí)不限于單線程,通過不斷地建立線程的方式倒是可以產(chǎn)生內(nèi)存溢出異常,在這種情況下,給每個(gè)線程的棧分配的內(nèi)存越大,反而越容易產(chǎn)生內(nèi)存溢出異常。
原因其實(shí)不難理解,操作系統(tǒng)分配給每個(gè)進(jìn)程的內(nèi)存是有限制的,譬如32位Windows的單個(gè)進(jìn)程 最大內(nèi)存限制為2GB。HotSpot虛擬機(jī)提供了參數(shù)可以控制Java堆和方法區(qū)這兩部分的內(nèi)存的最大值。那么虛擬機(jī)棧和本地方法棧內(nèi)存如下:
虛擬機(jī)棧和本地方法棧內(nèi)存=2GB-最大堆容量-最大方法區(qū)容量-程序計(jì)數(shù)器容量
因此為每個(gè)線程分配到的棧內(nèi)存越大,可以建立的線程數(shù)量自 然就越少,建立線程時(shí)就越容易把剩下的內(nèi)存耗盡。
通過上面了解到,出現(xiàn)StackOverflowError異常時(shí)有錯(cuò)誤堆??梢蚤喿x,相對(duì)來說,比較容易找到問題的所在(一般出現(xiàn)死循環(huán)可能會(huì)導(dǎo)致)。
如果是建立過多線程導(dǎo)致的內(nèi)存溢出,而不是棧溢出,在不能減少線程數(shù)或者更換64位虛擬機(jī)的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程
。如果沒有這方面的經(jīng)驗(yàn),這種通過“減少內(nèi)存”的手段來解決內(nèi)存溢出的方式會(huì)比較難以想到。
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(); } }
注意
重點(diǎn)提示一下,如果讀者要嘗試運(yùn)行上面這段代碼,記得要先保存當(dāng)前的工作,由于在 Windows平臺(tái)的虛擬機(jī)中,Java的線程是映射到操作系統(tǒng)的內(nèi)核線程上,無限制地創(chuàng)建線程會(huì)對(duì)操 作系統(tǒng)帶來很大壓力,上述代碼執(zhí)行時(shí)有很高的風(fēng)險(xiǎn),可能會(huì)由于創(chuàng)建線程數(shù)量過多而導(dǎo)致操作系統(tǒng) 假死
(電腦可能直接死機(jī))。
在32位操作系統(tǒng)下的運(yùn)行結(jié)果:
原因:32位有進(jìn)程大小內(nèi)存限制。
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
注意:如果要測(cè)試上面內(nèi)存溢出代碼,記住先保存當(dāng)前的工作,避免電腦卡死帶來的麻煩。
3、運(yùn)行時(shí)常量池溢出
由于運(yùn)行時(shí)常量池是方法區(qū)的一部分
,所以這兩個(gè)區(qū)域的溢出測(cè)試可以放到一起進(jìn)行。前面曾經(jīng) 提到HotSpot從JDK 7開始逐步“去永久代”的計(jì)劃,并在JDK 8中完全使用元空間來代替永久代
,在此我們就以測(cè)試代碼來觀察一下,使用“永久代”還是“元空間”來實(shí)現(xiàn)方法區(qū),對(duì)程序有什么 實(shí)際的影響。
String::intern()是一個(gè)本地方法
,它的作用是如果字符串常量池中已經(jīng)包含一個(gè)等于此String對(duì)象的 字符串,則返回代表池中這個(gè)字符串的String對(duì)象的引用
;否則,會(huì)將此String對(duì)象包含的字符串添加 到常量池中,并且返回此String對(duì)象的引用
。
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范圍內(nèi)足夠產(chǎn)生00M int i = 0; while (true){ list.add(String.valueOf(i++).intern()); } } }
- JDK7及以前(了解):-XX:PermSize設(shè)置永久代初始大小。-XX:MaxPermSize設(shè)置永久代最大可分配空間。(JDK7目前已經(jīng)很少用了,這兩個(gè)參數(shù)在JDK8及以后已經(jīng)沒有了,所以不必掌握,了解一下)
- JDK8及以后:可以使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize設(shè)置元空間初始大小以及最大可分配大小。
使用JDK 7或更高版本的JDK來運(yùn)行這段程序并不會(huì)得到相同的結(jié)果,無論是在JDK 7中繼續(xù)使 用-XX:MaxPermSize
參數(shù)或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize
參數(shù)把方法區(qū)容量同 樣限制在6MB,都不會(huì)出現(xiàn)溢出異常,循環(huán)將一直進(jìn)行下去,永不停歇。出現(xiàn)這種變 化,是因?yàn)樽訨DK 7起,原本存放在永久代的字符串常量池被移至Java堆之中,所以在JDK 7及以上版 本,限制方法區(qū)的容量對(duì)該測(cè)試用例來說是毫無意義的
。
在JDK1.7中(包括1.7以上)常量池存儲(chǔ)的不再是對(duì)象,而是對(duì)象引用,真正的對(duì)象是存儲(chǔ)在堆中的。把RuntimeConstantPoolOOM.java運(yùn)行時(shí)的VM參數(shù)改為如下(設(shè)置堆大小)所示:
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
運(yùn)行結(jié)果:
查看生成的堆內(nèi)存快照:
4、方法區(qū)溢出
方法區(qū)用于存放Class的相關(guān)信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。對(duì)于這個(gè)區(qū)域的測(cè)試,基本的思路是運(yùn)行時(shí)產(chǎn)生大量的類去填滿方法區(qū),直到溢出。雖然直接使用Java SE API也可以動(dòng)態(tài)產(chǎn)生類(如反射時(shí)的GeneratedConstructorAccessor 和動(dòng)態(tài)代理等),但在本次實(shí)驗(yàn)中操作起來比較麻煩。借助CGLib直接操作字節(jié)碼運(yùn)行時(shí),生成了大量的動(dòng)態(tài)類。
值得特別注意的是,我們?cè)谶@個(gè)例子中模擬的場(chǎng)景并非純粹是一個(gè)實(shí)驗(yàn),這樣的應(yīng)用經(jīng)常會(huì)出現(xiàn)在實(shí)際應(yīng)用中:當(dāng)前的很多主流框架,如Spring和Hibernate對(duì)類進(jìn)行增強(qiáng)時(shí),都會(huì)使用到CGLib這類字節(jié)碼技術(shù),增強(qiáng)的類越多,就需要越大的方法區(qū)來保證動(dòng)態(tài)生成的Class可以加載人內(nèi)存
。
測(cè)試示例:
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 { } }
設(shè)置元空間最大空間,和初始化空間參數(shù):
類信息是都存在方法區(qū)的,方法區(qū)在jdk1.8將永久區(qū)改為了元空間。自此以后,常量池在元空間都是存儲(chǔ)的引用。實(shí)際對(duì)象是在堆中。
-XX:MaxMetaspaceSize=10m -XX:MetaspaceSize=10m
運(yùn)行結(jié)果:
方法區(qū)溢出也是一種常見的內(nèi)存溢出異常,一個(gè)類如果要被垃圾收集器回收,要達(dá)成的條件是比 較苛刻的。在經(jīng)常運(yùn)行時(shí)生成大量動(dòng)態(tài)類的應(yīng)用場(chǎng)景里,就應(yīng)該特別關(guān)注這些類的回收狀況。這類場(chǎng) 景除了之前提到的程序使用了CGLib字節(jié)碼增強(qiáng)和動(dòng)態(tài)語言外,常見的還有:大量JSP或動(dòng)態(tài)產(chǎn)生JSP 文件的應(yīng)用(JSP第一次運(yùn)行時(shí)需要編譯為Java類)、基于OSGi的應(yīng)用(即使是同一個(gè)類文件,被不同 的加載器加載也會(huì)視為不同的類)等。
5、本機(jī)直接內(nèi)存溢出
直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機(jī)規(guī)范》中 定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。
直接內(nèi)存:可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在Java堆里面的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用`進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)(但是有一點(diǎn)注意,雖然不占用堆內(nèi)存,但是他占用了服務(wù)器內(nèi)存)。
直接內(nèi)存(Direct Memory)的容量大小可通過-XX:MaxDirectMemorySize參數(shù)來指定,如果不 去指定,則默認(rèn)與Java堆最大值(由-Xmx指定)一致。
代碼示例:
越過了DirectByteBuffer類直接通 過反射獲取Unsafe實(shí)例進(jìn)行內(nèi)存分配
(Unsafe類的getUnsafe()
方法指定只有引導(dǎo)類加載器才會(huì)返回實(shí) 例,體現(xiàn)了設(shè)計(jì)者希望只有虛擬機(jī)標(biāo)準(zhǔn)類庫里面的類才能使用Unsafe的功能,在JDK 10時(shí)才將Unsafe 的部分功能通過VarHandle開放給外部使用
),因?yàn)殡m然使用DirectByteBuffer分配內(nèi)存也會(huì)拋出內(nèi)存溢 出異常,但它拋出異常時(shí)并沒有真正向操作系統(tǒng)申請(qǐng)分配內(nèi)存,而是通過計(jì)算得知內(nèi)存無法分配就會(huì) 在代碼里手動(dòng)拋出溢出異常,真正申請(qǐng)分配內(nèi)存的方法是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); } } }
運(yùn)行參數(shù):
-Xmx20M -XX:MaxDirectMemorySize=10M -XX:+HeapDumpOnOutOfMemoryError
運(yùn)行結(jié)果:
我設(shè)置了-XX:+HeapDumpOnOutOfMemoryError發(fā)現(xiàn)運(yùn)行完成之后并沒有發(fā)現(xiàn)有內(nèi)存快照。
由直接內(nèi)存導(dǎo)致的內(nèi)存溢出,一個(gè)明顯的特征是在Heap Dump文件中不會(huì)看見有什么明顯的異常 情況,如果讀者發(fā)現(xiàn)內(nèi)存溢出之后產(chǎn)生的Dump文件很小,而程序中又直接或間接使用了 DirectMemory(典型的間接使用就是NIO),那就可以考慮重點(diǎn)檢查一下直接內(nèi)存方面的原因了。
三、JVM常用的啟動(dòng)參數(shù)
堆:
-Xms3550m
:設(shè)置JVM初始內(nèi)存為3550M。表示初始化JAVA堆的大小及該進(jìn)程剛創(chuàng)建出來的時(shí)候,他的專屬JAVA堆的大小,一旦對(duì)象容量超過了JAVA堆的初始容量,JAVA堆將會(huì)自動(dòng)擴(kuò)容到-Xmx大小。-Xmx3550m
:設(shè)置JVM最大可用內(nèi)存為3550M。表示java堆可以擴(kuò)展到的最大值,在很多情況下,通常將-Xms和-Xmx設(shè)置成一樣的,因?yàn)楫?dāng)堆不夠用而發(fā)生擴(kuò)容時(shí),會(huì)發(fā)生內(nèi)存抖動(dòng)影響程序運(yùn)行時(shí)的穩(wěn)定性。
棧:
-Xss128k
:規(guī)定了每個(gè)線程虛擬機(jī)棧及堆棧的大小,一般情況下,256k是足夠的,此配置將會(huì)影響此進(jìn)程中并發(fā)線程數(shù)的大?。ê投咽遣灰粯拥?,不支持動(dòng)態(tài)擴(kuò)展)。
方法區(qū):
- JDK7及以前(了解):
-XX:PermSize
設(shè)置永久代初始大小。 -XX:MaxPermSize
設(shè)置永久代最大可分配空間。(JDK7目前已經(jīng)很少用了,這兩個(gè)參數(shù)在JDK8及以后已經(jīng)沒有了,所以不必掌握,了解一下)-XX:MaxMetaspaceSize
=10m:設(shè)置元空間最大值,默認(rèn)是-1,即不限制,或者說只受限于本地內(nèi)存 大小。-XX:MetaspaceSize
=10m:指定元空間的初始空間大小,以字節(jié)為單位,達(dá)到該值就會(huì)觸發(fā)垃圾收集 進(jìn)行類型卸載,同時(shí)收集器會(huì)對(duì)該值進(jìn)行調(diào)整:如果釋放了大量的空間,就適當(dāng)降低該值;如果釋放 了很少的空間,那么在不超過-XX:MaxMetaspaceSize(如果設(shè)置了的話)的情況下,適當(dāng)提高該值。-XX:MinMetaspaceFreeRatio
:作用是在垃圾收集之后控制最小的元空間剩余容量的百分比,可 減少因?yàn)樵臻g不足導(dǎo)致的垃圾收集的頻率。類似的還有-XX:Max-MetaspaceFreeRatio,用于控制最 大的元空間剩余容量的百分比。
內(nèi)存:
-XX:+HeapDumpOnOutOfMemoryError
可以讓虛擬機(jī)在出現(xiàn)內(nèi)存溢出異常時(shí)Dump出當(dāng)前的內(nèi)存堆轉(zhuǎn)儲(chǔ)快照以便事后進(jìn)行分析(內(nèi)存堆轉(zhuǎn)儲(chǔ)快照 指的是溢出后,內(nèi)存當(dāng)中的對(duì)象占用情況)
GC:
-XX:-PrintGCDetails
:每次GC時(shí)打印詳細(xì)信息。
四、面試題
public static void main(String[] args) { String str1 = new StringBuilder("計(jì)算機(jī)").append("軟件").toString(); System.out.println(str1.intern() == str1); String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern() == str2); }
這段代碼在JDK 6中運(yùn)行,會(huì)得到兩個(gè)false,而在JDK 7中運(yùn)行,會(huì)得到一個(gè)true和一個(gè)false。在jdk1.8運(yùn)行也是,true、false。
產(chǎn) 生差異的原因是,在JDK 6中,intern()方法會(huì)把首次遇到的字符串實(shí)例復(fù)制到永久代的字符串常量池 中存儲(chǔ),
返回的也是永久代里面這個(gè)字符串實(shí)例的引用
,而由StringBuilder創(chuàng)建的字符串對(duì)象實(shí)例在 Java堆上
,所以必然不可能是同一個(gè)引用,結(jié)果將返回false。
而JDK 7(以及部分其他虛擬機(jī),例如JRockit)的intern()方法實(shí)現(xiàn)就不需要再拷貝字符串的實(shí)例 到永久代了,既然字符串常量池已經(jīng)移到Java堆中,那只需要在常量池里記錄一下首次出現(xiàn)的實(shí)例引 用即可,因此intern()返回的引用和由StringBuilder創(chuàng)建的那個(gè)字符串實(shí)例就是同一個(gè)。
而對(duì)str2比較返 回false,這是因?yàn)?ldquo;java
”這個(gè)字符串在執(zhí)行String-Builder.toString()之前就已經(jīng)出現(xiàn)過了
,字符串常量 池中已經(jīng)有它的引用,不符合intern()方法要求“首次遇到”的原則,“計(jì)算機(jī)軟件”這個(gè)字符串則是首次 出現(xiàn)的,因此結(jié)果返回true。(這塊說實(shí)話不好理解,說白了就是java是個(gè)特殊的字符串,他在常量池里面就一直存在)
總結(jié):在1.8之后通過intern()添加到常量池,只有字符串在常量池不存在的時(shí)候才會(huì)返回字符串的引用。
五、總結(jié)
到此為止,我們明白了虛擬機(jī)里面的內(nèi)存是如何劃分的,哪部分區(qū)域、什么樣的代碼和操作可能 導(dǎo)致內(nèi)存溢出異常。雖然Java有垃圾收集機(jī)制,但內(nèi)存溢出異常離我們并不遙遠(yuǎn),本章只是講解了各 個(gè)區(qū)域出現(xiàn)內(nèi)存溢出異常的原因,下一章將詳細(xì)講解Java垃圾收集機(jī)制為了避免出現(xiàn)內(nèi)存溢出異常都 做了哪些努力。
到此這篇關(guān)于Java實(shí)戰(zhàn)之OutOfMemoryError異常的文章就介紹到這了,更多相關(guān)java OutOfMemoryError異常內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Spring的ApplicationEvent實(shí)現(xiàn)本地事件驅(qū)動(dòng)的實(shí)現(xiàn)方法
本文介紹了如何使用Spring的ApplicationEvent實(shí)現(xiàn)本地事件驅(qū)動(dòng),通過自定義事件和監(jiān)聽器,實(shí)現(xiàn)模塊之間的松耦合,提升代碼的可維護(hù)性和擴(kuò)展性。同時(shí)還介紹了異步事件和事件傳遞的相關(guān)知識(shí)2023-04-04Java面試題 從源碼角度分析HashSet實(shí)現(xiàn)原理
這篇文章主要介紹了Java面試題 從源碼角度分析HashSet實(shí)現(xiàn)原理?,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-07-07Java通過SSH連接路由器輸入命令并讀取響應(yīng)的操作方法
最近需要讀取和修改華為路由器的配置,使用Java語言開發(fā),通過SSH連接,輸入命令并讀取響應(yīng),接下來通過本文給大家介紹下Java通過SSH連接路由器,輸入命令并讀取響應(yīng),需要的朋友可以參考下2024-01-01Spring使用IOC與DI實(shí)現(xiàn)完全注解開發(fā)
IOC也是Spring的核心之一了,之前學(xué)的時(shí)候是采用xml配置文件的方式去實(shí)現(xiàn)的,后來其中也多少穿插了幾個(gè)注解,但是沒有說完全采用注解實(shí)現(xiàn)。那么這篇文章就和大家分享一下,全部采用注解來實(shí)現(xiàn)IOC + DI2022-09-09Event?Sourcing事件溯源模式優(yōu)化業(yè)務(wù)系統(tǒng)
這篇文章主要為大家介紹了Event?Sourcing事件溯源模式優(yōu)化業(yè)務(wù)系統(tǒng)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07spring是如何實(shí)現(xiàn)聲明式事務(wù)的
這篇文章主要介紹了spring是如何實(shí)現(xiàn)聲明式事務(wù)的,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Spring Boot 項(xiàng)目設(shè)置網(wǎng)站圖標(biāo)的方法
這篇文章主要介紹了Spring Boot 項(xiàng)目設(shè)置網(wǎng)站圖標(biāo)的方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02Jenkins+Maven+SVN自動(dòng)化部署java項(xiàng)目
這篇文章主要介紹了Jenkins+Maven+SVN自動(dòng)化部署java項(xiàng)目,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01