聊聊Java中的內存溢出問題
內存溢出是指應用系統中存在無法回收的內存或使用的內存過多,最終使得程序運行要用到的內存大于虛擬機能提供的最大內存。這篇文章整理自《深入理解java虛擬機》。之前面阿里遇到過。
一、內存溢出原因
內存溢出就是內存不夠,引起內存溢出的原因有很多種,常見的有以下幾種:
1、內存中加載的數據量過于龐大,如一次從數據庫取出過多數據;
2、集合類中有對對象的引用,使用完后未清空,使得JVM不能回收;
3、代碼中存在死循環或循環產生過多重復的對象實體;
4、使用的第三方軟件中的BUG;
5、啟動參數內存值設定的過小;
當然實際情況中內存溢出的原因就太多了。下面我們就對這些原因分類一下:
以上的圖是基于java7來敘述的,從上面這張圖我們能夠得到如下信息:java虛擬機把內存分為5個模塊。
(1)程序計數器:程序計數器是線程私有的,主要的作用是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。既然每個線程都有一個,那么這些線程的計數器是互不影響的。也不會拋出任何異常。
(2)虛擬機棧和本地方法棧:虛擬機棧描述的是java方法執行的內存模型,每個方法在執行的時候都會創建一個棧幀用于存儲局部變量表、操作數棧、動態連接、方法出口等信息。本地方法棧與虛擬機棧的區別是,虛擬機棧為虛擬機執行java方法服務,而本地方法棧則為虛擬機提供native方法服務。
在單線程的操作中,無論是由于棧幀太大,還是虛擬機棧空間太小,當棧空間無法分配時,虛擬機拋出的都是StackOverflowError異常,而不會得到OutOfMemoryError異常。而在多線程環境下,則會拋出OutOfMemoryError異常。
(3)java堆和方法區:java堆區主要存放對象實例和數組等,方法區保存類信息、常量、靜態變量等等。運行時常量池也是方法區的一部分。這兩塊區域是線程共享的區域,只會拋出OutOfMemoryError。
不知道各位在B站看見過那個面試經典場景沒,在回答java的內存運行數據區結構時,以上的功能作用是一方面,如果回答時把內存溢出問題添加上是一個極大的加分項。
二、內存溢出實例
1、堆溢出
既然堆是存放實例對象的,那我們就無線創建實例對象。這樣堆區遲早會滿。
- public class HeapOOM {
- static class User {}
- public static void main(String[] args) {
- List<User> list = new ArrayList<User>();
- while (true) {
- list.add(new User());
- }
- }
- }
- /*Exception in thread "main" java.lang.OutOfMemoryError:
- GC overhead limit exceeded
- at com.fdd.test.HeapOOM.main(HeapOOM.java:11)*/
因為我提前設置了堆區內存,所以無限創建就會拋出異常。
2、虛擬機棧和本地方法棧溢出
Java虛擬機規范中描述了兩種異常:
如果線程請求的棧深度大于虛擬機鎖允許的最大深度,將拋出StackOverflowError異常。
如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。
第一種我們只需要使用方法遞歸調用即可模擬:
- public class StackOutOfMemoryError {
- public static void main(String[] args) {
- test();
- }
- private static void go() {
- System.out.println("StackOverflowError異常");
- test();
- }
- }
- /*Exception in thread "main" java.lang.StackOverflowError
- at sun.nio.cs.ext.DoubleByte$Encoder.encodeLoop(DoubleByte.java:617)
- at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
- at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
- at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
- at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
- at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
- at java.io.PrintStream.write(PrintStream.java:526)
- at java.io.PrintStream.print(PrintStream.java:597)
- at java.io.PrintStream.println(PrintStream.java:736)
- at com.fdd.test.StackOutOfMemoryError.go(StackOutOfMemoryError.java:11)
- at com.fdd.test.StackOutOfMemoryError.go(StackOutOfMemoryError.java:13)*/
第二種也可以遞歸調用模擬,,但是使用的是類直接調用。
- public class JavaVMStackSOF {
- private int stackLength = 1;
- public void stackLeak() {
- stackLength++;
- stackLeak();
- }
- public static void main(String[] args) {
- JavaVMStackSOF oom = new JavaVMStackSOF();
- oom.stackLeak();
- }
- }
- /*Exception in thread "main" java.lang.StackOverflowError
- at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:18)
- at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
- at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
- at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
- at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
- at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
- at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
- at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
- at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19)
- ... */
3、方法區和運行時常量池溢出
- public class JavaMethodAreaOOM {
- public static void main(String[] args) {
- while (true) {
- Enhancer enhancer = new Enhancer();
- enhancer.setSuperclass(User.class);
- enhancer.setUseCache(false);
- enhancer.setCallback(new MethodInterceptor() {
- public Object intercept(Object obj, Method method,
- Object[] args, MethodProxy proxy) throws Throwable {
- return proxy.invokeSuper(obj, args);
- }
- });
- enhancer.create();
- }
- }
- static class User {}
- }
- /*Exception in thread "main"
- Exception: java.lang.OutOfMemoryError thrown
- from the UncaughtExceptionHandler in thread "main"
- */
4、本機直接內存溢出
DirectMemory容量可通過-XX: MaxDirectMemorySize指定,如果不指定,則默認與Java堆最大值 (-Xmx指定)一樣。
- 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);
- }
- }
- }
上面介紹了幾個實例,那遇到這種問題如何排查呢?
三、內存溢出排查
排查其實最主要的就是檢查代碼,而且內存溢出往往都是代碼的問題。當然一下幾點都是需要注意的:
(1)內存中加載的數據量過于龐大,如一次從數據庫取出過多數據;
(2)集合類中有對對象的引用,使用完后未清空,使得JVM不能回收;
(3)代碼中存在死循環或循環產生過多重復的對象實體;
(4)使用的第三方軟件中的BUG;
(5)啟動參數內存值設定的過小;
最后就是解決了。
第一步,修改JVM啟動參數,直接增加內存。
第二步,檢查錯誤日志
第三步,對代碼進行走查和分析,找出可能發生內存溢出的位置。
一般情況下代碼出錯的概率會比較大一些,當然了不同的場景不同錯誤總是復雜多樣的。
本文轉載自微信公眾號「 愚公要移山」,可以通過以下二維碼關注。轉載本文請聯系 愚公要移山公眾號。