此篇是上一篇文章Java內(nèi)存溢出異常(上)的續(xù)篇,沒有看過的同學(xué)栈雳,可以先看一下上篇丘逸。本篇文章將介紹剩余的兩個溢出異常:方法區(qū)和運行時常量池溢出次屠。
方法區(qū)和運行時常量池溢出
這部分為什么會放在一起呢媒楼?在Java內(nèi)存區(qū)域與內(nèi)存溢出異常這篇文章中我們說過乐尊,運行時常量池實際上屬于方法區(qū)的一部分,所以就放在一起討論匣砖。
常量池溢出
在討論常量池的溢出之前科吭,先說明一下String.intern()方法昏滴,該方法會檢查字符串常量池中是否已經(jīng)包含了一個等于此String對象的字符串猴鲫,如果已經(jīng)包含了,則返回代表池中這個字符串的String對象谣殊;否則拂共,將此String對象包含的字符串添加到常量池中,并返回此String對象的引用姻几∫撕看過fastjson源代碼的同學(xué)應(yīng)該知道势告,該方法在fastjson這個json解析庫中大量的出現(xiàn),以提高json的解析速度抚恒。
在JDK 1.6之前的版本中咱台,常量池是分配在永久代的,可以通過-XX:PermSize和-XX:MaxPermSize參數(shù)來設(shè)置大小俭驮,從而間接限制其中常量池的容量回溺。
通過以上條件,我們可以輕易的復(fù)現(xiàn)這個異常混萝,代碼如下:
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
如果你的JDK環(huán)境是1.6版本之前的遗遵,你會得到如下運行結(jié)果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
......
如果你是JDK 1.7+,那么這段代碼會無限的運行下去逸嘀。因為在JDK 1.7之后對String.intern()方法進行了修改车要。
繼續(xù)看下面這段經(jīng)典的代碼:
public class RuntimeConstantPoolOOM {
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 1.6中運行,會得到兩個false崭倘,而在JDK 1.7中運行翼岁,會得到一個true和一個false。
出現(xiàn)差異的原因是因為司光,在JDK 1.6中登澜,intern()方法會把首次遇到的字符串實例復(fù)制到常量池中,返回的也是常量池中這個字符串實例的引用飘庄,而由StringBuilder創(chuàng)建的字符串實例在Java堆上脑蠕,所以必然不是同一個引用,將返回false跪削。而JDK 1.7中的intern()實訓(xùn)不會再復(fù)制實例谴仙,只是在常量池中記錄首次出現(xiàn)的實例引用,因此intern()返回的引用和由StringBuilder創(chuàng)建的那個字符串實例是同一個碾盐。對str2比較返回false是因為“java”這個字符串在執(zhí)行StringBuilder.toString()之前已經(jīng)出現(xiàn)過了晃跺,字符串中常量池中已經(jīng)有它的引用了,不符合“首次”出現(xiàn)的原則毫玖。
這就可以解釋掀虎,為什么在JDK 1.7+版本之后不能用String.intern()方法使常量池溢出的原因了,intern()不會像JDK 1.6之前的版本一樣無限復(fù)制實例到常量池中了付枫。
方法區(qū)溢出
方法區(qū)用于存放Class的相關(guān)信息烹玉,如類名、訪問修飾符阐滩、常量池二打、字段描述、方法描述等掂榔。這部分的測試可以通過生成大量的類去填滿方法區(qū)继效,直到溢出症杏,可以借助CGLib直接操作字節(jié)碼運行時生成大量的動態(tài)類,代碼如下:
public class JavaMethodAreaOOM {
public static void main(final String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
運行結(jié)果如下:
Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
...
在經(jīng)常動態(tài)生成大量Class的應(yīng)用中瑞信,比如說使用CGLib這類字節(jié)碼技術(shù)的時候厉颤,容易出現(xiàn)方法區(qū)溢出。所以說在使用這類技術(shù)編寫框架時凡简,要留意這方面導(dǎo)致的內(nèi)存溢出走芋。
本機直接內(nèi)存溢出
本機直接內(nèi)存的溢出主要與大量的直接操作內(nèi)存的API有關(guān),比如說NIO相關(guān)的API潘鲫,也可以通過rt.jar中的類使用Unsafe的功能來復(fù)現(xiàn)這個異常翁逞。DirectMemory容量可通過-XX:MaxDirectMemorySize指定,主要代碼如下:
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);
}
}
}
運行結(jié)果如下:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
...
由DirectMemory導(dǎo)致的內(nèi)存溢出溉仑,不會在Heap Dump文件中不會看見明顯的異常挖函,如果發(fā)現(xiàn)OOM之后Dump文件很小,而程序中又直接或間接使用了NIO浊竟,那就可以考慮檢查一下是不是這方面的原因怨喘。