一 堆內(nèi)存溢出
堆內(nèi)存溢出太常見,大部分人都應(yīng)該能想得到這一點(diǎn)漾狼,堆內(nèi)存用來存儲(chǔ)對(duì)象實(shí)例重慢,我們只要不停的創(chuàng)建對(duì)象,并且保證GC Roots和對(duì)象之間有可達(dá)路徑避免垃圾回收逊躁,那么在對(duì)象數(shù)量超過最大堆的大小限制后很快就能出現(xiàn)這個(gè)異常
寫一段代碼測(cè)試一下似踱,設(shè)置堆內(nèi)存大小2M
public class HeapOOM {
public static void main(String[] args) {
List<HeapOOM> list = new ArrayList<>();
while (true) {
list.add(new HeapOOM());
}
}
}
運(yùn)行代碼,很快能看見OOM異常出現(xiàn)志衣,這里的提示是Java heap space堆內(nèi)存溢出
一般的排查方式可以通過設(shè)置-XX: +HeapDumpOnOutOfMemoryError在發(fā)生異常時(shí)dump出當(dāng)前的內(nèi)存轉(zhuǎn)儲(chǔ)快照來分析屯援,分析可以使用Eclipse Memory Analyzer(MAT)來分析,獨(dú)立文件可以在官網(wǎng)下載
另外如果使用的是IDEA的話念脯,可以使用商業(yè)版JProfiler或者開源版本的JVM-Profiler狞洋,此外IDEA2018版本之后內(nèi)置了分析工具,包括Flame Graph(火焰圖)和Call Tree(調(diào)用樹)功能
二 方法區(qū)(運(yùn)行時(shí)常量池)和元空間溢出
方法區(qū)和堆一樣绿店,是線程共享的區(qū)域吉懊,包含Class文件信息、運(yùn)行時(shí)常量池假勿、常量池借嗽,運(yùn)行時(shí)常量池和常量池的主要區(qū)別是具備動(dòng)態(tài)性,也就是不一定非要是在Class文件中的常量池中的內(nèi)容才能進(jìn)入運(yùn)行時(shí)常量池转培,運(yùn)行期間也可以可以將新的常量放入池中恶导,比如String的intern()方法
我們寫一段代碼驗(yàn)證一下String.intern(),同時(shí)我們?cè)O(shè)置-XX:MetaspaceSize=50m -XX:MaxMetaspaceSize=50m 元空間大小浸须。由于我使用的是1.8版本的JDK惨寿,而1.8版本之前方法區(qū)存在于永久代(PermGen),1.8之后取消了永久代的概念删窒,轉(zhuǎn)為元空間(Metaspace)裂垦,如果是之前版本可以設(shè)置PermSize MaxPermSize永久代的大小
private static String str = "test";
public static void main(String[] args) {
List<String> list = new ArrayList<>();
while (true){
String str2 = str + str;
str = str2;
list.add(str.intern());
}
}
運(yùn)行代碼,會(huì)發(fā)現(xiàn)代碼報(bào)錯(cuò)
再次修改配置肌索,去除元空間限制蕉拢,修改堆內(nèi)存大小-Xms20m -Xmx20m,可以看見堆內(nèi)存報(bào)錯(cuò)诚亚。
這是為什么呢晕换?
intern()本身是一個(gè)native方法,它的作用是:如果字符串常量池中已經(jīng)包含一個(gè)等 于此String對(duì)象的字符串亡电,則返回代表池中這個(gè)字符串的String對(duì)象;否則届巩,將此String對(duì)象包含的字符串添加到常量池中,并且返回String對(duì)象的引用
而在1.7版本之后份乒,字符串常量池已經(jīng)轉(zhuǎn)移到堆區(qū)恕汇,所以會(huì)報(bào)出堆內(nèi)存溢出的錯(cuò)誤腕唧,如果1.7之前版本的話會(huì)看見PermGen space的報(bào)錯(cuò)
三 直接內(nèi)存溢出
直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)域的一部分,并且不受堆內(nèi)存的限制瘾英,但是受到機(jī)器內(nèi)存大小的限制枣接。常見的比如在NIO中可以使用native函數(shù)直接分配堆外內(nèi)存就容易導(dǎo)致OOM的問題
直接內(nèi)存大小可以通過-XX:MaxDirectMemorySize指定,如果不指定缺谴,則默認(rèn)與Java 堆最大值-Xmx一樣
由直接內(nèi)存導(dǎo)致的內(nèi)存溢出但惶,一個(gè)明顯的特征是在Dump文件中不會(huì)看見明顯的異常,如果發(fā)現(xiàn)OOM之后Dump文件很小湿蛔,而程序中又直接或間接使用了NIO膀曾,那就可以考慮檢查一下是不是這方面的原因
四 棧內(nèi)存溢出
棧是線程私有,它的生命周期和線程相同阳啥。每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表添谊、操作數(shù)棧、動(dòng)態(tài)鏈接察迟、方法出口等信息斩狱,方法調(diào)用的過程就是棧幀入棧和出棧的過程
在java虛擬機(jī)規(guī)范中,對(duì)虛擬機(jī)棧定義了兩種異常:
1.如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度扎瓶,將拋出StackOverflowError異常
2.如果虛擬機(jī)椝唬可以動(dòng)態(tài)擴(kuò)展,并且擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存概荷,拋出OutOfMemoryError異常
先寫一段代碼測(cè)試一下秕岛,設(shè)置-Xss160k,-Xss代表每個(gè)線程的棧內(nèi)存大小
public class StackOOM {
private int length = 1;
public void stackTest() {
System.out.println("stack lenght=" + length);
length++;
stackTest();
}
public static void main(String[] args) {
StackOOM test = new StackOOM();
test.stackTest();
}
}
測(cè)試發(fā)現(xiàn)误证,單線程下無論怎么設(shè)置參數(shù)都是StackOverflow異常
嘗試把代碼修改為多線程瓣蛀,調(diào)整-Xss2m,因?yàn)闉槊總€(gè)線程分配的內(nèi)存越大雷厂,棧空間可容納的線程數(shù)量越少叠殷,越容易產(chǎn)生內(nèi)存溢出改鲫。反之,如果內(nèi)存不夠的情況林束,可以調(diào)小該參數(shù)來達(dá)到支撐更多線程的目的
public class StackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
new Thread(() -> dontStop()).start();
}
}
public static void main(String[] args) throws Throwable {
StackOOM stackOOM = new StackOOM();
stackOOM.stackLeakByThread();
}
}