本課時我們主要自己模擬一個 JVM 內存溢出的場景。在模擬 JVM 內存溢出之前我們先來看下這樣的幾個問題。
* 老年代溢出為什么那么可怕?
* 元空間也有溢出?怎么優化?
* 如何配置棧大小?避免棧溢出?
* 進程突然死掉,沒有留下任何信息時如何進行排查?
年輕代由于有老年代的擔保,一般在內存占滿的時候,并沒什么問題。但老年代滿了就比較嚴重了,它沒有其他的空間用來做擔保,只能 OOM 了,也就是發生 Out Of Memery Error。JVM 會在這種情況下直接停止工作,是非常嚴重的后果。
OOM 一般是內存泄漏引起的,表現在 GC 日志里,一般情況下就是 GC 的時間變長了,而且每次回收的效果都非常一般。GC 后,堆內存的實際占用呈上升趨勢。接下來,我們將模擬三種溢出場景,同時使用我們了解的工具進行觀測。
在開始之前,請你下載并安裝一個叫作 VisualVM 的工具,我們使用這個圖形化的工具看一下溢出過程。
雖然 VisualVM 工具非常好用,但一般生產環境都沒有這樣的條件,所以大概率使用不了。新版本 JDK 把這個工具單獨抽離了出去,需要自行下載。
這里需要注意下載安裝完成之后請在插件選項中勾選 Visual GC 下載,它將可視化內存布局。
#### 堆溢出模擬
首先,我們模擬堆溢出的情況,在模擬之前我們需要準備一份測試代碼。這份代碼開放了一個 HTTP 接口,當你觸發它之后,將每秒鐘生成 1MB 的數據。由于它和 GC Roots 的強關聯性,每次都不能被回收。
程序通過 JMX,將在每一秒創建數據之后,輸出一些內存區域的占用情況。然后通過訪問 http://localhost:8888 觸發后,它將一直運行,直到堆溢出。
```
import?com.sun.net.httpserver.HttpContext;
import?com.sun.net.httpserver.HttpExchange;
import?com.sun.net.httpserver.HttpServer;
import?java.io.OutputStream;
import?java.lang.management.ManagementFactory;
import?java.lang.management.MemoryPoolMXBean;
import?java.net.InetSocketAddress;
import?java.util.ArrayList;
import?java.util.List;
public?class?OOMTest?{
???public?static?final?int?_1MB?=?1024?*?1024;
???static?List<byte[]>?byteList?=?new?ArrayList<>();
???private?static?void?oom(HttpExchange?exchange)?{
???????try?{
???????????String?response?=?"oom?begin!";
???????????exchange.sendResponseHeaders(200,?response.getBytes().length);
???????????OutputStream?os?=?exchange.getResponseBody();
???????????os.write(response.getBytes());
???????????os.close();
???????}?catch?(Exception?ex)?{
???????}
???????for?(int?i?=?0;?;?i++)?{
???????????byte[]?bytes?=?new?byte[_1MB];
???????????byteList.add(bytes);
???????????System.out.println(i?+?"MB");
???????????memPrint();
???????????try?{
???????????????Thread.sleep(1000);
???????????}?catch?(Exception?e)?{
???????????}
???????}
???}
???static?void?memPrint()?{
???????for?(MemoryPoolMXBean?memoryPoolMXBean?:?ManagementFactory.getMemoryPoolMXBeans())?{
???????????System.out.println(memoryPoolMXBean.getName()?+
???????????????????"??committed:"?+?memoryPoolMXBean.getUsage().getCommitted()?+
???????????????????"??used:"?+?memoryPoolMXBean.getUsage().getUsed());
???????}
???}
???private?static?void?srv()?throws?Exception?{
???????HttpServer?server?=?HttpServer.create(new?InetSocketAddress(8888),?0);
???????HttpContext?context?=?server.createContext("/");
???????context.setHandler(OOMTest::oom);
???????server.start();
???}
???public?static?void?main(String[]?args)?throws?Exception{
???????srv();
???}
}
```
我們使用 CMS 收集器進行垃圾回收,可以看到如下的信息。
命令:
```
java -Xmx20m? -Xmn4m? ?-XX:+UseConcMarkSweepGC? -verbose:gc -Xlog:gc,
gc+ref=debug,gc+heap=debug,
gc+age=trace:file=/tmp/logs/gc_%p.log:tags,
uptime,
time,
level -Xlog:safepoint:file=/tmp/logs/safepoint_%p.log:tags,
uptime,
time,
level -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/logs -XX:ErrorFile=/tmp/logs/hs_error_pid%p.log -XX:-OmitStackTraceInFastThrow? OOMTest
```
輸出:
```
[0.025s][info][gc] Using Concurrent Mark Sweep
0MB
CodeHeap 'non-nmethods' ?committed:2555904 ?used:1120512
Metaspace ?committed:4980736 ?used:854432
CodeHeap 'profiled nmethods' ?committed:2555904 ?used:265728
Compressed Class Space ?committed:524288 ?used:96184
Par Eden Space ?committed:3407872 ?used:2490984
Par Survivor Space ?committed:393216 ?used:0
CodeHeap 'non-profiled nmethods' ?committed:2555904 ?used:78592
CMS Old Gen ?committed:16777216 ?used:0
...省略
[16.377s][info][gc] GC(9) Concurrent Mark 1.592ms
[16.377s][info][gc] GC(9) Concurrent Preclean
[16.378s][info][gc] GC(9) Concurrent Preclean 0.721ms
[16.378s][info][gc] GC(9) Concurrent Abortable Preclean
[16.378s][info][gc] GC(9) Concurrent Abortable Preclean 0.006ms
[16.378s][info][gc] GC(9) Pause Remark 17M->17M(19M) 0.344ms
[16.378s][info][gc] GC(9) Concurrent Sweep
[16.378s][info][gc] GC(9) Concurrent Sweep 0.248ms
[16.378s][info][gc] GC(9) Concurrent Reset
[16.378s][info][gc] GC(9) Concurrent Reset 0.013ms
17MB
CodeHeap 'non-nmethods' ?committed:2555904 ?used:1120512
Metaspace ?committed:4980736 ?used:883760
CodeHeap 'profiled nmethods' ?committed:2555904 ?used:422016
Compressed Class Space ?committed:524288 ?used:92432
Par Eden Space ?committed:3407872 ?used:3213392
Par Survivor Space ?committed:393216 ?used:0
CodeHeap 'non-profiled nmethods' ?committed:2555904 ?used:88064
CMS Old Gen ?committed:16777216 ?used:16452312
[18.380s][info][gc] GC(10) Pause Initial Mark 18M->18M(19M) 0.187ms
[18.380s][info][gc] GC(10) Concurrent Mark
[18.384s][info][gc] GC(11) Pause Young (Allocation Failure) 18M->18M(19M) 0.186ms
[18.386s][info][gc] GC(10) Concurrent Mark 5.435ms
[18.395s][info][gc] GC(12) Pause Full (Allocation Failure) 18M->18M(19M) 10.572ms
[18.400s][info][gc] GC(13) Pause Full (Allocation Failure) 18M->18M(19M) 5.348ms
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
? ?at OldOOM.main(OldOOM.java:20)
```
最后 JVM 在一陣瘋狂的 GC 日志輸出后,進程停止了。在現實情況中,JVM 在停止工作之前,很多會垂死掙扎一段時間,這個時候,GC 線程會造成 CPU 飆升,但其實它已經不能工作了。
VisualVM 的截圖展示了這個溢出結果。可以看到 Eden 區剛開始還是運行平穩的,內存泄漏之后就開始瘋狂回收(其實是提升),老年代內存一直增長,直到 OOM。

很多參數會影響對象的分配行為,但不是非常必要,我們一般不去調整它們。為了觀察這些參數的默認值,我們通常使用 -XX:+PrintFlagsFinal 參數,輸出一些設置信息。
命令:
```
# java -XX:+PrintFlagsFinal 2>&1 | grep SurvivorRatio
? ?uintx SurvivorRatio ? ? ? ? ? ? ? ? ? ? ? ? ? ?= 8 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? {product} {default}
```
Java13 輸出了幾百個參數和默認值,我們通過修改一些參數來觀測一些不同的行為。
**NewRatio** 默認值為 2,表示年輕代是老年代的 1/2。追加參數 “-XX:NewRatio=1”,可以把年輕代和老年代的空間大小調成一樣大。在實踐中,我們一般使用 -Xmn 來設置一個固定值。注意,這兩個參數不要用在 G1 垃圾回收器中。
**SurvivorRatio** 默認值為 8。表示伊甸區和幸存區的比例。在上面的例子中,Eden 的內存大小為:0.8*4MB。S 分區不到 1MB,根本存不下我們的 1MB 數據。
**MaxTenuringThreshold** ?這個值在 CMS 下默認為 6,G1 下默認為 15。這是因為 G1 存在動態閾值計算。這個值和我們前面提到的對象提升有關,如果你想要對象盡量長的時間存在于年輕代,則在 CMS 中,可以把它調整到 15。
```
java -XX:+PrintFlagsFinal -XX:+UseConcMarkSweepGC 2>&1 | grep MaxTenuringThreshold
java -XX:+PrintFlagsFinal -XX:+UseG1GC 2>&1 | grep MaxTenuringThreshold
```
**PretenureSizeThreshold**?這個參數默認值是 0,意味著所有的對象年輕代優先分配。我們把這個值調小一點,再觀測 JVM 的行為。追加參數 -XX:PretenureSizeThreshold=1024,可以看到 VisualVm 中老年代的區域增長。
**TargetSurvivorRatio** 默認值為 50。在動態計算對象提升閾值的時候使用。計算時,會從年齡最小的對象開始累加,如果累加的對象大小大于幸存區的一半,則將當前的對象 age 作為新的閾值,年齡大于此閾值的對象直接進入老年代。工作中不建議調整這個值,如果要調,請調成比 50 大的值。
你可以嘗試著更改其他參數,比如垃圾回收器的種類,動態看一下效果。尤其注意每一項內存區域的內容變動,你會對垃圾回收器有更好的理解。
**UseAdaptiveSizePolicy** ,因為它和 CMS 不兼容,所以 CMS 下默認為 false,但 G1 下默認為 true。這是一個非常智能的參數,它是用來自適應調整空間大小的參數。它會在每次 GC 之后,重新計算 Eden、From、To 的大小。很多人在 Java 8 的一些配置中會見到這個參數,但其實在 CMS 和 G1 中是不需要顯式設置的。
值的注意的是,Java 8 默認垃圾回收器是 Parallel Scavenge,它的這個參數是默認開啟的,有可能會發生把幸存區自動調小的可能,造成一些問題,顯式的設置 SurvivorRatio 可以解決這個問題。
下面這張截圖,是切換到 G1 之后的效果。
```
java -Xmx20m ? -XX:+UseG1GC ?-verbose:gc -Xlog:gc,gc+ref=debug,gc+heap=debug,gc+age=trace:file=/tmp/logs/gc_%p.log:tags,uptime,time,level -Xlog:safepoint:file=/tmp/logs/safepoint_%p.log:tags,uptime,time,level -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/logs -XX:ErrorFile=/tmp/logs/hs_error_pid%p.log -XX:-OmitStackTraceInFastThrow ?OOMTest
```

可以通過下面這個命令調整小堆區的大小,來看一下這個過程。
```
-XX:G1HeapRegionSize=<N>M
```
#### 元空間溢出
堆一般都是指定大小的,但元空間不是。所以如果元空間發生內存溢出會更加嚴重,會造成操作系統的內存溢出。我們在使用的時候,也會給它設置一個上限 for safe。
元空間溢出主要是由于加載的類太多,或者動態生成的類太多。下面是一段模擬代碼。通過訪問 http://localhost:8888 觸發后,它將會發生元空間溢出。
```
import?com.sun.net.httpserver.HttpContext;
import?com.sun.net.httpserver.HttpExchange;
import?com.sun.net.httpserver.HttpServer;
import?java.io.OutputStream;
import?java.lang.reflect.InvocationHandler;
import?java.lang.reflect.Method;
import?java.lang.reflect.Proxy;
import?java.net.InetSocketAddress;
import?java.net.URL;
import?java.net.URLClassLoader;
import?java.util.HashMap;
import?java.util.Map;
public?class?MetaspaceOOMTest?{
???public?interface?Facade?{
???????void?m(String?input);
???}
???public?static?class?FacadeImpl?implements?Facade?{
???????@Override
???????public?void?m(String?name)?{
???????}
???}
???public?static?class?MetaspaceFacadeInvocationHandler?implements?InvocationHandler?{
???????private?Object?impl;
???????public?MetaspaceFacadeInvocationHandler(Object?impl)?{
???????????this.impl?=?impl;
???????}
???????@Override
???????public?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{
???????????return?method.invoke(impl,?args);
???????}
???}
???private?static?Map<String,?Facade>?classLeakingMap?=?new?HashMap<String,?Facade>();
???private?static?void?oom(HttpExchange?exchange)?{
???????try?{
???????????String?response?=?"oom?begin!";
???????????exchange.sendResponseHeaders(200,?response.getBytes().length);
???????????OutputStream?os?=?exchange.getResponseBody();
???????????os.write(response.getBytes());
???????????os.close();
???????}?catch?(Exception?ex)?{
???????}
???????try?{
???????????for?(int?i?=?0;?;?i++)?{
???????????????String?jar?=?"file:"?+?i?+?".jar";
???????????????URL[]?urls?=?new?URL[]{new?URL(jar)};
???????????????URLClassLoader?newClassLoader?=?new?URLClassLoader(urls);
???????????????Facade?t?=?(Facade)?Proxy.newProxyInstance(newClassLoader,
???????????????????????new?Class<?>[]{Facade.class},
???????????????????????new?MetaspaceFacadeInvocationHandler(new?FacadeImpl()));
???????????????classLeakingMap.put(jar,?t);
???????????}
???????}?catch?(Exception?e)?{
???????}
???}
???private?static?void?srv()?throws?Exception?{
???????HttpServer?server?=?HttpServer.create(new?InetSocketAddress(8888),?0);
???????HttpContext?context?=?server.createContext("/");
???????context.setHandler(MetaspaceOOMTest::oom);
???????server.start();
???}
???public?static?void?main(String[]?args)?throws?Exception?{
???????srv();
???}
}
```
這段代碼將使用 Java 自帶的動態代理類,不斷的生成新的 class。
```
java -Xmx20m? -Xmn4m? ?-XX:+UseG1GC? -verbose:gc -Xlog:gc,gc+ref=debug,gc+heap=debug,gc+age=trace:file=/tmp/logs/gc_%p.log:tags,uptime,time,level -Xlog:safepoint:file=/tmp/logs/safepoint_%p.log:tags,uptime,time,level -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/logs -XX:ErrorFile=/tmp/logs/hs_error_pid%p.log -XX:-OmitStackTraceInFastThrow -XX:MetaspaceSize=16M -XX:MaxMetaspaceSize=16M? MetaspaceOOMTest
```
我們在啟動的時候,限制 Metaspace 空間大小為 16MB。可以看到運行一小會之后,Metaspace 會發生內存溢出。
```
[6.509s][info][gc] GC(28) Pause Young (Concurrent Start) (Metadata GC Threshold) 9M->9M(20M) 1.186ms
[6.509s][info][gc] GC(30) Concurrent Cycle
[6.534s][info][gc] GC(29) Pause Full (Metadata GC Threshold) 9M->9M(20M) 25.165ms
[6.556s][info][gc] GC(31) Pause Full (Metadata GC Clear Soft References) 9M->9M(20M) 21.136ms
[6.556s][info][gc] GC(30) Concurrent Cycle 46.668ms
java.lang.OutOfMemoryError: Metaspace
Dumping heap to /tmp/logs/java_pid36723.hprof ...
Heap dump file created [17362313 bytes in 0.134 secs]
```

但假如你把堆 Metaspace 的限制給去掉,會更可怕。它占用的內存會一直增長。
#### 堆外內存溢出
嚴格來說,上面的 Metaspace 也是屬于堆外內存的。但是我們這里的堆外內存指的是 Java 應用程序通過直接方式從操作系統中申請的內存。所以嚴格來說,這里是指直接內存。
程序將通過 ByteBuffer 的 allocateDirect 方法每 1 秒鐘申請 1MB 的直接內存。不要忘了通過鏈接觸發這個過程。
但是,使用 VisualVM 看不到這個過程,使用 JMX 的 API 同樣也看不到。關于這部分內容,我們將在堆外內存排查課時進行詳細介紹。
```
import?com.sun.net.httpserver.HttpContext;
import?com.sun.net.httpserver.HttpExchange;
import?com.sun.net.httpserver.HttpServer;
import?java.io.OutputStream;
import?java.lang.management.ManagementFactory;
import?java.lang.management.MemoryPoolMXBean;
import?java.net.InetSocketAddress;
import?java.nio.ByteBuffer;
import?java.util.ArrayList;
import?java.util.List;
public?class?OffHeapOOMTest?{
???public?static?final?int?_1MB?=?1024?*?1024;
???static?List<ByteBuffer>?byteList?=?new?ArrayList<>();
???private?static?void?oom(HttpExchange?exchange)?{
???????try?{
???????????String?response?=?"oom?begin!";
???????????exchange.sendResponseHeaders(200,?response.getBytes().length);
???????????OutputStream?os?=?exchange.getResponseBody();
???????????os.write(response.getBytes());
???????????os.close();
???????}?catch?(Exception?ex)?{
???????}
???????for?(int?i?=?0;?;?i++)?{
???????????ByteBuffer?buffer?=?ByteBuffer.allocateDirect(_1MB);
???????????byteList.add(buffer);
???????????System.out.println(i?+?"MB");
???????????memPrint();
???????????try?{
???????????????Thread.sleep(1000);
???????????}?catch?(Exception?e)?{
???????????}
???????}
???}
???private?static?void?srv()?throws?Exception?{
???????HttpServer?server?=?HttpServer.create(new?InetSocketAddress(8888),?0);
???????HttpContext?context?=?server.createContext("/");
???????context.setHandler(OffHeapOOMTest::oom);
???????server.start();
???}
???public?static?void?main(String[]?args)?throws?Exception?{
???????srv();
???}
???static?void?memPrint()?{
???????for?(MemoryPoolMXBean?memoryPoolMXBean?:?ManagementFactory.getMemoryPoolMXBeans())?{
???????????System.out.println(memoryPoolMXBean.getName()?+
???????????????????"??committed:"?+?memoryPoolMXBean.getUsage().getCommitted()?+
???????????????????"??used:"?+?memoryPoolMXBean.getUsage().getUsed());
???????}
???}
}
```
通過 top 或者操作系統的監控工具,能夠看到內存占用的明顯增長。為了限制這些危險的內存申請,如果你確定在自己的程序中用到了大量的 JNI 和 JNA 操作,要顯式的設置 MaxDirectMemorySize 參數。
以下是程序運行一段時間拋出的錯誤。
```
Exception in thread "Thread-2" java.lang.OutOfMemoryError: Direct buffer memory
? ?at java.nio.Bits.reserveMemory(Bits.java:694)
? ?at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
? ?at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
? ?at OffHeapOOMTest.oom(OffHeapOOMTest.java:27)
? ?at com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:79)
? ?at sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:83)
? ?at com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:82)
? ?at sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:675)
? ?at com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:79)
? ?at sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:647)
? ?at sun.net.httpserver.ServerImpl$DefaultExecutor.execute(ServerImpl.java:158)
? ?at sun.net.httpserver.ServerImpl$Dispatcher.handle(ServerImpl.java:431)
? ?at sun.net.httpserver.ServerImpl$Dispatcher.run(ServerImpl.java:396)
? ?at java.lang.Thread.run(Thread.java:748)
```
啟動命令。
```
java -XX:MaxDirectMemorySize=10M -Xmx10M OffHeapOOMTest
```
#### 棧溢出
還記得我們的虛擬機棧么?棧溢出指的就是這里的數據太多造成的泄漏。通過 -Xss 參數可以設置它的大小。比如下面的命令就是設置棧大小為 128K。
```
-Xss128K
```
從這里我們也能了解到,由于每個線程都有一個虛擬機棧。線程的開銷也是要占用內存的。如果系統中的線程數量過多,那么占用內存的大小也是非常可觀的。
棧溢出不會造成 JVM 進程死亡,危害“相對較小”。下面是一個簡單的模擬棧溢出的代碼,只需要遞歸調用就可以了。
```
public?class?StackOverflowTest?{
???static?int?count?=?0;
???static?void?a()?{
???????System.out.println(count);
???????count++;
???????b();
???}
???static?void?b()?{
???????System.out.println(count);
???????count++;
???????a();
???}
???public?static?void?main(String[]?args)?throws?Exception?{
???????a();
???}
}
```
運行后,程序直接報錯。
```
Exception in thread "main" java.lang.StackOverflowError
? ?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 StackOverflowTest.a(StackOverflowTest.java:5)
```
如果你的應用經常發生這種情況,可以試著調大這個值。但一般都是因為程序錯誤引起的,最好檢查一下自己的代碼。
#### 進程異常退出
上面這幾種溢出場景,都有明確的原因和報錯,排查起來也是非常容易的。但是還有一類應用,死亡的時候,靜悄悄的,什么都沒留下。
以下問題已經不止一個同學問了:我的 Java 進程沒了,什么都沒留下,直接蒸發不見了
why?是因為對象太多了么?
這是趣味性和技巧性非常突出的一個問題。讓我們執行 dmesg 命令,大概率會看到你的進程崩潰信息躺在那里。

為了能看到發生的時間,我們習慣性加上參數 T(dmesg -T)。
這個現象,其實和 Linux 的內存管理有關。由于 Linux 系統采用的是虛擬內存分配方式,JVM 的代碼、庫、堆和棧的使用都會消耗內存,但是申請出來的內存,只要沒真正 access過,是不算的,因為沒有真正為之分配物理頁面。
隨著使用內存越用越多。第一層防護墻就是 SWAP;當 SWAP 也用的差不多了,會嘗試釋放 cache;當這兩者資源都耗盡,殺手就出現了。oom-killer 會在系統內存耗盡的情況下跳出來,選擇性的干掉一些進程以求釋放一點內存。
所以這時候我們的 Java 進程,是操作系統“主動”終結的,JVM 連發表遺言的機會都沒有。這個信息,只能在操作系統日志里查找。
要解決這種問題,首先不能太貪婪。比如一共 8GB 的機器,你把整整 7.5GB 都分配給了 JVM。當操作系統內存不足時,你的 JVM 就可能成為 oom-killer 的獵物。
相對于被動終結,還有一種主動求死的方式。有些同學,會在程序里面做一些判斷,直接調用 System.exit() 函數。
這個函數危險得很,它將強制終止我們的應用,而且什么都不會留下。你應該掃描你的代碼,確保這樣的邏輯不會存在。
再聊一種最初級最常見還經常發生的,會造成應用程序意外死亡的情況,那就是對 Java 程序錯誤的啟動方式。
很多同學對 Linux 不是很熟悉,使用 XShell 登陸之后,調用下面的命令進行啟動。
```
java com.cn.AA &
```
這樣調用還算有點意識,在最后使用了“&”號,以期望進程在后臺運行。但可惜的是,很多情況下,隨著 XShell Tab 頁的關閉,或者等待超時,后面的 Java 進程就隨著一塊停止了,很讓人困惑。
正確的啟動方式,就是使用 nohup 關鍵字,或者阻塞在其他更加長命的進程里(比如docker)。
```
nohup java com.cn.AA &
```
進程這種靜悄悄的死亡方式,通常會給我們的問題排查帶來更多的困難。
在發生問題時,要確保留下了足夠的證據,來支持接下來的分析。不能喊一句“出事啦”,然后就陷入無從下手的尷尬境地。
通常,我們在關閉服務的時候,會使用“kill -15”,而不是“kill -9”,以便讓服務在臨死之前喘口氣。信號9和15的區別,是面試經常問的一個問題,也是一種非常有效的手段。
#### 小結
本課時我們簡單模擬了堆、元空間、棧的溢出。并使用 VisualVM 觀察了這個過程。
接下來,我們了解到進程靜悄悄消失的三種情況。如果你的應用也這樣消失過,試著這樣找找它。這三種情況也是一個故障排查流程中要考慮的環節,屬于非常重要的邊緣檢查點。相信聰明的你,會將這些情況揉進自己的面試體系去,真正成為自己的實戰經驗。
#### 課后問答
* 1、老師您好,能否把第二個例子詳細說一下,為什么gc是那樣變化?這是我的配置參數-Xmx20m -Xmn4m -XX:+UseG1GC -XX:+PrintGCDetails -XX:G1HeapRegionSize=10M我沒弄清楚,我這樣設置,gc的變化原因
答案:第二個例子?限制元空間(堆外)的參數是MaxMetaspaceSize,沒看到你的設置。另外G1HeapRegionSize大小應該是2的冪,你這么設置它的實際大小是8M。你現在一共才給了堆空間20M,小堆區最多才2個,這些設置參數怎么看都是不合理的。
* 2、我本地執行堆溢出模擬代碼時,發現經過180秒后,程序都沒有拋出OutOfMenoryErr,不知道為什么
答案:你要指定最大堆的大小,否則會默認使用操作系統的2/3
* 3、context.setHandler(OOMTest::oom); 老師您好,這個: : 能稍微說下嗎,想了解一下這個寫法
答案:你指的是雙冒號?這是Java 8中的Lambda寫法之一,意思是方法引用。直接搜索“java 雙冒號”可深入了解。
* 4、進程突然死掉,沒有留下任何信息時如何進行排查 只能依靠dump文件嗎
答案:OOM才會產生DUMP。靜悄悄死亡,這種情況dump文件都不存在,只能依靠dmesg信息和死亡之前的一些監控信息和日志信息。
* 5、“如果幸存區中相同年齡對象大小的和,大于幸存區的一半,大于或等于 age 的對象將會直接進入老年代”應該為小于等于某一年齡的對象大小總和?
答案:感謝提醒,參考代碼share/gc/shared/ageTable.cpp中的compute_tenuring_threshold函數,重新表述如下:從年齡最小的對象開始累加,如果累加的對象大小,大于幸存區的一半,則講當前的對象age將作為新的閾值,年齡大于此閾值的對象直接進入老年代。
* 6、另外“幸存區的一半”最好提一下 TargetSurvivorRatio 這個參數。
答案: 值的注意的是。使用“grep -rn -i --color TargetSurvivorRatio .”搜索(jdk13),可以看到這個參數只影響serial和G1收集器,還稍微影響PLAB緩沖區的大小。
- 前言
- 開篇詞
- 基礎原理
- 第01講:一探究竟:為什么需要 JVM?它處在什么位置?
- 第02講:大廠面試題:你不得不掌握的 JVM 內存管理
- 第03講:大廠面試題:從覆蓋 JDK 的類開始掌握類的加載機制
- 第04講:動手實踐:從棧幀看字節碼是如何在 JVM 中進行流轉的
- 垃圾回收
- 第05講:大廠面試題:得心應手應對 OOM 的疑難雜癥
- 第06講:深入剖析:垃圾回收你真的了解嗎?(上)
- 第06講:深入剖析:垃圾回收你真的了解嗎?(下)
- 第07講:大廠面試題:有了 G1 還需要其他垃圾回收器嗎?
- 第08講:案例實戰:億級流量高并發下如何進行估算和調優
- 實戰部分
- 第09講:案例實戰:面對突如其來的 GC 問題如何下手解決
- 第10講:動手實踐:自己模擬 JVM 內存溢出場景
- 第11講:動手實踐:遇到問題不要慌,輕松搞定內存泄漏
- 第12講:工具進階:如何利用 MAT 找到問題發生的根本原因
- 第13講:動手實踐:讓面試官刮目相看的堆外內存排查
- 第14講:預警與解決:深入淺出 GC 監控與調優
- 第15講:案例分析:一個高死亡率的報表系統的優化之路
- 第16講:案例分析:分庫分表后,我的應用崩潰了
- 進階部分
- 第17講:動手實踐:從字節碼看方法調用的底層實現
- 第18講:大廠面試題:不要搞混 JMM 與 JVM
- 第19講:動手實踐:從字節碼看并發編程的底層實現
- 第20講:動手實踐:不為人熟知的字節碼指令
- 第21講:深入剖析:如何使用 Java Agent 技術對字節碼進行修改
- 第22講:動手實踐:JIT 參數配置如何影響程序運行?
- 第23講:案例分析:大型項目如何進行性能瓶頸調優?
- 彩蛋
- 第24講:未來:JVM 的歷史與展望
- 第25講:福利:常見 JVM 面試題補充