Java堆溢出
Java堆用于存儲對象實(shí)例,只要不斷地創(chuàng)建對象,當(dāng)對象數(shù)量到達(dá)最大堆的容量限制后就會(huì)產(chǎn)生內(nèi)存溢出異常。最常見的內(nèi)存溢出就是存在大的容器,而沒法回收,比如:Map,List等。
- 內(nèi)存溢出:內(nèi)存空間不足導(dǎo)致,新對象無法分配到足夠的內(nèi)存;
- 內(nèi)存泄漏:應(yīng)該釋放的對象沒有被釋放,多見于自己使用容器保存元素的情況下。
出現(xiàn)下面信息就可以斷定出現(xiàn)了堆內(nèi)存溢出。
java.lang.OutOfMemoryError: Java heap space
保證GC Roots到對象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對象
示例
設(shè)置JVM內(nèi)存參數(shù):
-verbose:gc -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\dump
/**
* java 堆內(nèi)存溢出
* <p>
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\dump
*
* @author yuhao.wang3
* @since 2019/11/30 17:09
*/
public class HeapOutOfMemoryErrorTest {
public static void main(String[] args) throws InterruptedException {
// 模擬大容器
List<Object> list = Lists.newArrayList();
for (long i = 1; i > 0; i++) {
list.add(new Object());
if (i % 100_000 == 0) {
System.out.println(Thread.currentThread().getName() + "::" + i);
}
}
}
}
運(yùn)行結(jié)果
[GC (Allocation Failure) 5596K->1589K(19968K), 0.0422027 secs]
main::100000
main::200000
[GC (Allocation Failure) 7221K->5476K(19968K), 0.0144103 secs]
main::300000
[GC (Allocation Failure) 9190K->9195K(19968K), 0.0098252 secs]
main::400000
main::500000
[Full GC (Ergonomics) 17992K->13471K(19968K), 0.3431052 secs]
main::600000
main::700000
main::800000
[Full GC (Ergonomics) 17127K->16788K(19968K), 0.1581969 secs]
[Full GC (Allocation Failure) 16788K->16758K(19968K), 0.1994445 secs]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to D:\dump\java_pid7432.hprof ...
Heap dump file created [28774262 bytes in 0.221 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.xiaolyuh.HeapOutOfMemoryErrorTest.main(HeapOutOfMemoryErrorTest.java:23)
Disconnected from the target VM, address: '127.0.0.1:61622', transport: 'socket'
分析工具
JDK自帶的jvisualvm.exe工具可以分析.hprof和.dump文件。
首先需要找出最大的對象,判斷最大對象的存在是否合理,如何合理就需要調(diào)整JVM內(nèi)存大小。如果不合理,那么這個(gè)對象的存在,就是最有可能是引起內(nèi)存溢出的根源。通過GC Roots的引用鏈信息,就可以比較準(zhǔn)確地定位出泄露代碼的位置。
- 查詢最大對象

- 找出具體的對象

解決方案
- 優(yōu)化代碼,去除大對象;
- 調(diào)整JVM內(nèi)存大?。?Xmx與-Xms);
超出GC開銷限制
當(dāng)出現(xiàn)java.lang.OutOfMemoryError: GC overhead limit exceeded異常信息時(shí),表示超出了GC開銷限制。當(dāng)超過98%的時(shí)間用來做GC,但是卻回收了不到2%的堆內(nèi)存時(shí)會(huì)拋出此異常。
異常棧
[Full GC (Ergonomics) 19225K->19225K(19968K), 0.1044070 secs]
[Full GC (Ergonomics) 19227K->19227K(19968K), 0.0684710 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to D:\dump\java_pid17556.hprof ...
Heap dump file created [34925385 bytes in 0.132 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) 19257K->933K(19968K), 0.0403569 secs]
at com.xiaolyuh.HeapOutOfMemoryErrorTest.main(HeapOutOfMemoryErrorTest.java:25)
ERROR: JDWP Unable to get JNI 1.2 environment, jvm->GetEnv() return code = -2
JDWP exit error AGENT_ERROR_NO_JNI_ENV(183): [util.c:840]
解決方案
- 通過
-XX:-UseGCOverheadLimit參數(shù)來禁用這個(gè)檢查,但是并不能從根本上來解決內(nèi)存溢出的問題,最后還是會(huì)報(bào)出java.lang.OutOfMemoryError: Java heap space異常; - 調(diào)整JVM內(nèi)存大小(-Xmx與-Xms);
虛擬機(jī)棧和本地方法棧溢出
- 如果線程請求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常。
- 如果虛擬機(jī)在擴(kuò)展棧時(shí)無法申請到足夠的內(nèi)存空間,則拋出OutOfMemoryError異常。
這里把異常分成兩種情況,看似更加嚴(yán)謹(jǐn),但卻存在著一些互相重疊的地方:當(dāng)棧空間無法繼續(xù)分配時(shí),到底是內(nèi)存太小,還是已使用的??臻g太大,其本質(zhì)上只是對同一件事情的兩種描述而已。
StackOverflowError
出現(xiàn)StackOverflowError異常的主要原因有兩點(diǎn):
- 單個(gè)線程請求的棧深度大于虛擬機(jī)所允許的最大深度
- 創(chuàng)建的線程過多
單個(gè)線程請求的棧深度過大
單個(gè)線程請求的棧深度大于虛擬機(jī)所允許的最大深度,主要表現(xiàn)有以下幾點(diǎn):
- 存在遞歸調(diào)用
- 存在循環(huán)依賴調(diào)用
- 方法調(diào)用鏈路很深,比如使用裝飾器模式的時(shí)候,對已經(jīng)裝飾后的對象再進(jìn)行裝飾
異常信息java.lang.StackOverflowError。
裝飾器示例:
Collections.unmodifiableList(
Collections.unmodifiableList(
Collections.unmodifiableList(
Collections.unmodifiableList(
Collections.unmodifiableList(
...)))))));
遞歸示例:
/**
* java 虛擬機(jī)棧和本地方法棧內(nèi)存溢出測試
* <p>
* VM Args: -Xss128k
*
* @author yuhao.wang3
* @since 2019/11/30 17:09
*/
public class StackOverflowErrorErrorTest {
private int stackLength = 0;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
StackOverflowErrorErrorTest sof = new StackOverflowErrorErrorTest();
try {
sof.stackLeak();
} catch (Exception e) {
System.out.println(sof.stackLength);
e.printStackTrace();
}
}
}
運(yùn)行結(jié)果:
stackLength::1372
java.lang.StackOverflowError
at com.xiaolyuh.StackOverflowErrorErrorTest.stackLeak(StackOverflowErrorErrorTest.java:16)
at com.xiaolyuh.StackOverflowErrorErrorTest.stackLeak(StackOverflowErrorErrorTest.java:16)
at com.xiaolyuh.StackOverflowErrorErrorTest.stackLeak(StackOverflowErrorErrorTest.java:16)
...
當(dāng)增大??臻g的時(shí)候我們就會(huì)發(fā)現(xiàn),遞歸深度會(huì)增加,修改棧空間-Xss1m,然后運(yùn)行程序,運(yùn)行結(jié)果如下:
stackLength::20641
java.lang.StackOverflowError
at com.xiaolyuh.StackOverflowErrorErrorTest.stackLeak(StackOverflowErrorErrorTest.java:16)
at com.xiaolyuh.StackOverflowErrorErrorTest.stackLeak(StackOverflowErrorErrorTest.java:16)
...
修改遞歸方法的參數(shù)列表后遞歸深度急劇減少:
public void stackLeak(String ags1, String ags2, String ags3) {
stackLength++;
stackLeak(ags1, ags2, ags3);
}
運(yùn)行結(jié)果如下:
stackLength::13154
java.lang.StackOverflowError
at com.xiaolyuh.StackOverflowErrorErrorTest.stackLeak(StackOverflowErrorErrorTest.java:16)
at com.xiaolyuh.StackOverflowErrorErrorTest.stackLeak(StackOverflowErrorErrorTest.java:16)
...
由此可見影響遞歸的深度因素有:
- 單個(gè)線程的棧空間大?。?Xss)
- 局部變量表的大小
單個(gè)線程請求的棧深度超過內(nèi)存限制導(dǎo)致的棧內(nèi)存溢出,一般是由于非正確的編碼導(dǎo)致的。從上面的示例我們可以看出,當(dāng)??臻g在
-Xss128k的時(shí)候,調(diào)用層級都在1000以上,一般情況下方法的調(diào)用是達(dá)不到這個(gè)深度的。如果方法調(diào)用的深度確實(shí)有這么大,那么我們可以通過-Xss配置來增大??臻g大小。
創(chuàng)建的線程過多
不斷地建立線程也可能導(dǎo)致棧內(nèi)存溢出,因?yàn)槲覀儥C(jī)器的總內(nèi)存是有限制的,所以虛擬機(jī)棧和本地方法棧對應(yīng)的內(nèi)存也是有最大限制的。如果單個(gè)線程的??臻g越大,那么整個(gè)應(yīng)用允許創(chuàng)建的線程數(shù)就越少。異常信息java.lang.OutOfMemoryError: unable to create new native thread。
虛擬機(jī)棧和本地方法棧內(nèi)存 ≈ 操作系統(tǒng)內(nèi)存限制 - 最大堆容量(Xmx) - 最大方法區(qū)容量(MaxPermSize)
過多創(chuàng)建線程示例:
/**
* java 虛擬機(jī)棧和本地方法棧內(nèi)存溢出測試
* <p>
* 創(chuàng)建線程過多導(dǎo)致內(nèi)存溢出異常
* <p>
* VM Args: -verbose:gc -Xss20M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\dump
*
* @author yuhao.wang3
* @since 2019/11/30 17:09
*/
public class StackOutOfMemoryErrorTest {
private static int threadCount;
public static void main(String[] args) throws Throwable {
try {
while (true) {
threadCount++;
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000 * 60 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
} catch (Throwable e) {
e.printStackTrace();
throw e;
} finally {
System.out.println("threadCount=" + threadCount);
}
}
}
Java的線程是映射到操作系統(tǒng)的內(nèi)核線程上,因此上述代碼執(zhí)行時(shí)有較大的風(fēng)險(xiǎn),可能會(huì)導(dǎo)致操作系統(tǒng)假死。
運(yùn)行結(jié)果:
java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at StackOutOfMemoryErrorTest.main(StackOutOfMemoryErrorTest.java:17)
threadCount=4131
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at StackOutOfMemoryErrorTest.main(StackOutOfMemoryErrorTest.java:17)
需要重新上述異常,最好是在32位機(jī)器上,因?yàn)槲以?4位機(jī)器沒有重現(xiàn)。
在有限的內(nèi)存空間里面,當(dāng)我們需要?jiǎng)?chuàng)建更多的線程的時(shí)候,我們可以減少單個(gè)線程的??臻g大小。
元數(shù)據(jù)區(qū)域的內(nèi)存溢出
元數(shù)據(jù)區(qū)域或方法區(qū)是用于存放Class的相關(guān)信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。我們可以通過在運(yùn)行時(shí)產(chǎn)生大量的類去填滿方法區(qū),直到溢出,如:代理的使用(CGlib)、大量JSP或動(dòng)態(tài)產(chǎn)生JSP文件的應(yīng)用(JSP第一次運(yùn)行時(shí)需要編譯為Java類)、基于OSGi的應(yīng)用(即使是同一個(gè)類文件,被不同的加載器加載也會(huì)視為不同的類)等。
/**
* java 元數(shù)據(jù)區(qū)域/方法區(qū)的內(nèi)存溢出
* <p>
* VM Args JDK 1.6: set JAVA_OPTS=-verbose:gc -XX:PermSize=10m -XX:MaxPermSize=10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\dump
* <p>
* VM Args JDK 1.8: set JAVA_OPTS=-verbose:gc -Xmx20m -XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\dump
*
* @author yuhao.wang3
* @since 2019/11/30 17:09
*/
public class MethodAreaOutOfMemoryErrorTest {
static class MethodAreaOOM {
}
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MethodAreaOOM.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] params, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, params);
}
});
enhancer.create();
}
}
}
運(yùn)行結(jié)果:
[GC (Last ditch collection) 1283K->1283K(16384K), 0.0002585 secs]
[Full GC (Last ditch collection) 1283K->1226K(19968K), 0.0075856 secs]
java.lang.OutOfMemoryError: Metaspace
Dumping heap to D:\dump\java_pid18364.hprof ...
Heap dump file created [2479477 bytes in 0.015 secs]
[GC (Metadata GC Threshold) 1450K->1354K(19968K), 0.0003906 secs]
[Full GC (Metadata GC Threshold) 1354K->976K(19968K), 0.0073752 secs]
[GC (Last ditch collection) 976K->976K(19968K), 0.0002921 secs]
[Full GC (Last ditch collection) 976K->973K(19968K), 0.0045243 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at org.springframework.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:52)
at org.springframework.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:116)
at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
at org.springframework.cglib.core.KeyFactory$Generator.create(KeyFactory.java:221)
at org.springframework.cglib.core.KeyFactory.create(KeyFactory.java:174)
at org.springframework.cglib.core.KeyFactory.create(KeyFactory.java:153)
at org.springframework.cglib.proxy.Enhancer.<clinit>(Enhancer.java:73)
at com.xiaolyuh.MethodAreaOutOfMemoryErrorTest.main(MethodAreaOutOfMemoryErrorTest.java:26)
運(yùn)行時(shí)常量池的內(nèi)存溢出
String.intern()是一個(gè)Native方法,它的作用是:如果字符串常量池中已經(jīng)包含一個(gè)等于此String對象的字符串,則返回代表池中這個(gè)字符串的String對象;否則,將此String對象包含的字符串添加到常量池中,并且返回此String對象的引用。
在JDK 1.6的時(shí)候,運(yùn)行時(shí)常量池是在方法區(qū)中,所以直接限制了方法區(qū)中大小就可以模擬出運(yùn)行池常量池的內(nèi)存溢出。
/**
* java 方法區(qū)和運(yùn)行時(shí)常量池溢出
* <p>
* VM Args JDK 1.6: set JAVA_OPTS=-verbose:gc -XX:PermSize10 -XX:MaxPermSize10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\dump
*
* @author yuhao.wang3
* @since 2019/11/30 17:09
*/
public class RuntimeConstantOutOfMemoryErrorTest {
public static void main(String[] args) {
// 使用List保存著常量池的引用,避免Full GC 回收常量池行為
List<String> list = new ArrayList<>();
for (int i = 0; ; i++) {
list.add(String.valueOf(i).intern());
}
}
}
運(yùn)行結(jié)果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at RuntimeConstantOutOfMemoryErrorTest.main(RuntimeConstantOutOfMemoryErrorTest.java:18)
直接內(nèi)存溢出
DirectMemory容量可通過-XX:MaxDirectMemorySize指定,如果不指定,則默認(rèn)與Java堆最大值(-Xmx指定)一樣。
/**
* java 直接內(nèi)存溢出
* <p>
* VM Args JDK 1.6: set JAVA_OPTS=-verbose:gc -Xms20m -XX:MaxDirectMemorySize=10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\dump
*
* @author yuhao.wang3
* @since 2019/11/30 17:09
*/
public class DirectMemoryOutOfMemoryErrorTest {
public static void main(String[] args) throws IllegalAccessException {
int _1M = 1024 * 1024;
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1M);
}
}
}
運(yùn)行結(jié)果:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.xiaolyuh.DirectMemoryOutOfMemoryErrorTest.main(DirectMemoryOutOfMemoryErrorTest.java:23)
由DirectMemory導(dǎo)致的內(nèi)存溢出,一個(gè)明顯的特征是在Heap Dump文件中不會(huì)看見明顯的異常,如果讀者發(fā)現(xiàn)OOM之后Dump文件很小,而程序中又直接或間接使用了NIO,那就可以考慮檢查一下是不是這方面的原因。
解決方案
通過-XX:MaxDirectMemorySize指定直接內(nèi)存大小。
源碼
https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases
spring-boot-student-jvm工程
參考
《深入理解JAVA虛擬機(jī)》