本課時我們主要講解讓面試官刮目相看的堆外內存排查。
第 02 課時講了 JVM 的內存布局,同時也在第 08 課時中看到了由于 Metaspace 設置過小而引起的問題,接著,第 10 課時講了一下元空間和直接內存引起的內存溢出實例。
Metaspace 屬于堆外內存,但由于它是單獨管理的,所以排查起來沒什么難度。你平常可能見到的使用堆外內存的場景還有下面這些:
* JNI 或者 JNA 程序,直接操縱了本地內存,比如一些加密庫;
* 使用了Java 的 Unsafe 類,做了一些本地內存的操作;
* Netty 的直接內存(Direct Memory),底層會調用操作系統的 malloc 函數。
使用堆外內存可以調用一些功能完備的庫函數,而且減輕了 GC 的壓力。這些代碼,有可能是你了解的人寫的,也有可能隱藏在第三方的 jar 包里。雖然有一些好處,但是問題排查起來通常會比較的困難。
在第 10 課時,介紹了 MaxDirectMemorySize 可以控制直接內存的申請。其實,通過這個參數,仍然限制不住所有堆外內存的使用,它只是限制了使用 DirectByteBuffer 的內存申請。很多時候(比如直接使用了 sun.misc.Unsafe 類),堆外內存會一直增長,直到機器物理內存爆滿,被 oom killer。
```
import?sun.misc.Unsafe;
import?java.lang.reflect.Field;
public?class?UnsafeDemo?{
????public?static?final?int?_1MB?=?1024?*?1024;
????public?static?void?main(String[]?args)?throws?Exception?{
????????Field?field?=?Unsafe.class.getDeclaredField("theUnsafe");
????????field.setAccessible(true);
????????Unsafe?unsafe?=?(Unsafe)?field.get(null);
????????for?(;?;?)?{
????????????unsafe.allocateMemory(_1MB);
????????}
????}
```
上面這段代碼,就會持續申請堆外內存,但它返回的是 long 類型的地址句柄,所以堆內內存的使用會很少。
我們使用下面的命令去限制堆內和直接內存的使用,結果發現程序占用的操作系統內存在一直上升,這兩個參數在這種場景下沒有任何效果。這段程序搞死了我的機器很多次,運行的時候要小心。
```
java?-XX:MaxDirectMemorySize=10M?-Xmx10M??UnsafeDemo
```
相信這種情況也困擾了你,因為使用一些 JDK 提供的工具,根本無法發現這部門內存的使用。我們需要一些更加底層的工具來發現這些游離的內存分配。其實,很多內存和性能問題,都逃不過下面要介紹的這些工具的聯合分析。本課時將會結合一個實際的例子,來看一下一個堆外內存的溢出情況,了解常見的套路。
#### 1. 現象
我們有一個服務,非常的奇怪,在某個版本之后,占用的內存開始增長,直到虛擬機分配的內存上限,但是并不會 OOM。如果你開啟了 SWAP,會發現這個應用也會毫不猶豫的將它吞掉,有多少吞多少。
說它的內存增長,是通過 top 命令去觀察的,看它的 RES 列的數值;反之,如果使用 jmap 命令去看內存占用,得到的只是堆的大小,只能看到一小塊可憐的空間。

使用 ps 也能看到相同的效果。我們觀測到,除了虛擬內存比較高,達到了 17GB 以外,實際使用的內存 RSS 也夸張的達到了 7 GB,遠遠超過了 -Xmx 的設定。
```
[root]$?ps?-p?75?-o?rss,vsz??
RSS????VSZ?7152568?17485844
```
使用 jps?查看啟動參數,發現分配了大約 3GB 的堆內存。實際內存使用超出了最大內存設定的一倍還多,這明顯是不正常的,肯定是使用了堆外內存。
#### 2. 模擬程序
為了能夠使用這些工具實際觀測這個內存泄漏的過程,我這里準備了一份小程序。程序將會持續的使用 Java 的 Zip 函數進行壓縮和解壓,這種操作在一些對傳輸性能較高的的場景經常會用到。
程序將會申請 1kb 的隨機字符串,然后持續解壓。為了避免讓操作系統陷入假死狀態,我們每次都會判斷操作系統內存使用率,在達到 60% 的時候,我們將掛起程序;通過訪問 8888 端口,將會把內存閾值提高到 85%。我們將分析這兩個處于相對靜態的虛擬快照。
```
import?com.sun.management.OperatingSystemMXBean;
import?com.sun.net.httpserver.HttpContext;
import?com.sun.net.httpserver.HttpServer;
import?java.io.*;
import?java.lang.management.ManagementFactory;
import?java.net.InetSocketAddress;
import?java.util.Random;
import?java.util.concurrent.ThreadLocalRandom;
import?java.util.zip.GZIPInputStream;
import?java.util.zip.GZIPOutputStream;
/**
?*?@author?xjjdog
?*/
public?class?LeakExample?{
????/**
?????*?構造隨機的字符串
?????*/
????public?static?String?randomString(int?strLength)?{
????????Random?rnd?=?ThreadLocalRandom.current();
????????StringBuilder?ret?=?new?StringBuilder();
????????for?(int?i?=?0;?i?<?strLength;?i++)?{
????????????boolean?isChar?=?(rnd.nextInt(2)?%?2?==?0);
????????????if?(isChar)?{
????????????????int?choice?=?rnd.nextInt(2)?%?2?==?0???65?:?97;
????????????????ret.append((char)?(choice?+?rnd.nextInt(26)));
????????????}?else?{
????????????????ret.append(rnd.nextInt(10));
????????????}
????????}
????????return?ret.toString();
????}
????public?static?int?copy(InputStream?input,?OutputStream?output)?throws?IOException?{
????????long?count?=?copyLarge(input,?output);
????????return?count?>?2147483647L???-1?:?(int)?count;
????}
????public?static?long?copyLarge(InputStream?input,?OutputStream?output)?throws?IOException?{
????????byte[]?buffer?=?new?byte[4096];
????????long?count?=?0L;
????????int?n;
????????for?(;?-1?!=?(n?=?input.read(buffer));?count?+=?(long)?n)?{
????????????output.write(buffer,?0,?n);
????????}
????????return?count;
????}
????public?static?String?decompress(byte[]?input)?throws?Exception?{
????????ByteArrayOutputStream?out?=?new?ByteArrayOutputStream();
????????copy(new?GZIPInputStream(new?ByteArrayInputStream(input)),?out);
????????return?new?String(out.toByteArray());
????}
????public?static?byte[]?compress(String?str)?throws?Exception?{
????????ByteArrayOutputStream?bos?=?new?ByteArrayOutputStream();
????????GZIPOutputStream?gzip?=?new?GZIPOutputStream(bos);
????????try?{
????????????gzip.write(str.getBytes());
????????????gzip.finish();
????????????byte[]?b?=?bos.toByteArray();
????????????return?b;
????????}finally?{
????????????try?{?gzip.close();?}catch?(Exception?ex?){}
????????????try?{?bos.close();?}catch?(Exception?ex?){}
????????}
????}
????private?static?OperatingSystemMXBean?osmxb?=?(OperatingSystemMXBean)?ManagementFactory.getOperatingSystemMXBean();
????public?static?int?memoryLoad()?{
????????double?totalvirtualMemory?=?osmxb.getTotalPhysicalMemorySize();
????????double?freePhysicalMemorySize?=?osmxb.getFreePhysicalMemorySize();
????????double?value?=?freePhysicalMemorySize?/?totalvirtualMemory;
????????int?percentMemoryLoad?=?(int)?((1?-?value)?*?100);
????????return?percentMemoryLoad;
????}
????private?static?volatile?int?RADIO?=?60;
????public?static?void?main(String[]?args)?throws?Exception?{
????????HttpServer?server?=?HttpServer.create(new?InetSocketAddress(8888),?0);
????????HttpContext?context?=?server.createContext("/");
????????context.setHandler(exchange?->?{
????????????try?{
????????????????RADIO?=?85;
????????????????String?response?=?"OK!";
????????????????exchange.sendResponseHeaders(200,?response.getBytes().length);
????????????????OutputStream?os?=?exchange.getResponseBody();
????????????????os.write(response.getBytes());
????????????????os.close();
????????????}?catch?(Exception?ex)?{
????????????}
????????});
????????server.start();
????????//1kb
????????int?BLOCK_SIZE?=?1024;
????????String?str?=?randomString(BLOCK_SIZE?/?Byte.SIZE);
????????byte[]?bytes?=?compress(str);
????????for?(;?;?)?{
????????????int?percent?=?memoryLoad();
????????????if?(percent?>?RADIO)?{
????????????????Thread.sleep(1000);
????????????}?else?{
????????????????decompress(bytes);
????????????????Thread.sleep(1);
????????????}
```
程序將使用下面的命令行進行啟動。為了簡化問題,這里省略了一些無關的配置。
```
java?-Xmx1G?-Xmn1G?-XX:+AlwaysPreTouch??-XX:MaxMetaspaceSize=10M?-XX:MaxDirectMemorySize=10M?-XX:NativeMemoryTracking=detail?LeakExample
```
#### 3. NMT
首先介紹一下上面的幾個 JVM 參數,分別使用 Xmx、MaxMetaspaceSize、MaxDirectMemorySize 這三個參數限制了堆、元空間、直接內存的大小。
然后,使用 AlwaysPreTouch 參數。其實,通過參數指定了 JVM 大小,只有在 JVM 真正使用的時候,才會分配給它。這個參數,在 JVM 啟動的時候,就把它所有的內存在操作系統分配了。在堆比較大的時候,會加大啟動時間,但在這個場景中,我們為了減少內存動態分配的影響,把這個值設置為 True。
接下來的 NativeMemoryTracking,是用來追蹤 Native 內存的使用情況。通過在啟動參數上加入 -XX:NativeMemoryTracking=detail 就可以啟用。使用 jcmd 命令,就可查看內存分配。
```
jcmd?$pid??VM.native_memory?summary
```
我們在一臺 4GB 的虛擬機上使用上面的命令。啟動程序之后,發現進程使用的內存迅速升到 2.4GB。
```
#?jcmd?2154??VM.native_memory?summary
2154:
Native?Memory?Tracking:
Total:?reserved=2370381KB,?committed=1071413KB
-?????????????????Java?Heap?(reserved=1048576KB,?committed=1048576KB)
????????????????????????????(mmap:?reserved=1048576KB,?committed=1048576KB)
-?????????????????????Class?(reserved=1056899KB,?committed=4995KB)
????????????????????????????(classes?#432)
????????????????????????????(malloc=131KB?#328)
????????????????????????????(mmap:?reserved=1056768KB,?committed=4864KB)
-????????????????????Thread?(reserved=10305KB,?committed=10305KB)
????????????????????????????(thread?#11)
????????????????????????????(stack:?reserved=10260KB,?committed=10260KB)
????????????????????????????(malloc=34KB?#52)
????????????????????????????(arena=12KB?#18)
-??????????????????????Code?(reserved=249744KB,?committed=2680KB)
????????????????????????????(malloc=144KB?#502)
????????????????????????????(mmap:?reserved=249600KB,?committed=2536KB)
-????????????????????????GC?(reserved=2063KB,?committed=2063KB)
????????????????????????????(malloc=7KB?#80)
????????????????????????????(mmap:?reserved=2056KB,?committed=2056KB)
-??????????????????Compiler?(reserved=138KB,?committed=138KB)
????????????????????????????(malloc=8KB?#38)
????????????????????????????(arena=131KB?#5)
-??????????????????Internal?(reserved=789KB,?committed=789KB)
????????????????????????????(malloc=757KB?#1272)
????????????????????????????(mmap:?reserved=32KB,?committed=32KB)
-????????????????????Symbol?(reserved=1535KB,?committed=1535KB)
????????????????????????????(malloc=983KB?#114)
????????????????????????????(arena=552KB?#1)
-????Native?Memory?Tracking?(reserved=159KB,?committed=159KB)
????????????????????????????(malloc=99KB?#1399)
????????????????????????????(tracking?overhead=60KB)
-???????????????Arena?Chunk?(reserved=174KB,?committed=174KB)
????????????????????????????(mall
```
可惜的是,這個名字讓人振奮的工具并不能如它描述的一樣,看到我們這種泄漏的場景。下圖這點小小的空間,是不能和 2GB 的內存占用相比的。

NMT 能看到堆內內存、Code 區域或者使用 unsafe.allocateMemory 和 DirectByteBuffer 申請的堆外內存,雖然是個好工具但問題并不能解決。
使用 jmap 工具,dump 一份堆快照,然后使用 MAT 分析,依然不能找到這部分內存。
#### 4. pmap
像是 EhCache 這種緩存框架,提供了多種策略,可以設定將數據存儲在非堆上,我們就是要排查這些影響因素。如果能夠在代碼里看到這種可能性最大的代碼塊,是最好的。
為了進一步分析問題,我們使用 pmap 命令查看進程的內存分配,通過 RSS 升序序排列。結果發現除了地址 00000000c0000000 上分配的 1GB 堆以外(也就是我們的堆內存),還有數量非常多的 64M 一塊的內存段,還有巨量小的物理內存塊映射到不同的虛擬內存段上。但到現在為止,我們不知道里面的內容是什么,是通過什么產生的。
```
#?pmap?-x?2154??|?sort?-n?-k3
Address???????????Kbytes?????RSS???Dirty?Mode??Mapping
----------------?-------?-------?-------
0000000100080000?1048064???????0???????0?-----???[?anon?]
00007f2d4fff1000??????60???????0???????0?-----???[?anon?]
00007f2d537fb000????8212???????0???????0?-----???[?anon?]
00007f2d57ff1000??????60???????0???????0?-----???[?anon?]
.....省略N行
00007f2e3c000000???65524???22064???22064?rw---???[?anon?]
00007f2e00000000???65476???22068???22068?rw---???[?anon?]
00007f2e18000000???65476???22072???22072?rw---???[?anon?]
00007f2e30000000???65476???22076???22076?rw---???[?anon?]
00007f2dc0000000???65520???22080???22080?rw---???[?anon?]
00007f2dd8000000???65520???22080???22080?rw---???[?anon?]
00007f2da8000000???65524???22088???22088?rw---???[?anon?]
00007f2e8c000000???65528???22088???22088?rw---???[?anon?]
00007f2e64000000???65520???22092???22092?rw---???[?anon?]
00007f2e4c000000???65520???22096???22096?rw---???[?anon?]
00007f2e7c000000???65520???22096???22096?rw---???[?anon?]
00007f2ecc000000???65520???22980???22980?rw---???[?anon?]
00007f2d84000000???65476???23368???23368?rw---???[?anon?]
00007f2d9c000000??131060???43932???43932?rw---???[?anon?]
00007f2d50000000???57324???56000???56000?rw---???[?anon?]
00007f2d4c000000???65476???64160???64160?rw---???[?anon?]
00007f2d5c000000???65476???64164???64164?rw---???[?anon?]
00007f2d64000000???65476???64164???64164?rw---???[?anon?]
00007f2d54000000???65476???64168???64168?rw---???[?anon?]
00007f2d7c000000???65476???64168???64168?rw---???[?anon?]
00007f2d60000000???65520???64172???64172?rw---???[?anon?]
00007f2d6c000000???65476???64172???64172?rw---???[?anon?]
00007f2d74000000???65476???64172???64172?rw---???[?anon?]
00007f2d78000000???65520???64176???64176?rw---???[?anon?]
00007f2d68000000???65520???64180???64180?rw---???[?anon?]
00007f2d80000000???65520???64184???64184?rw---???[?anon?]
00007f2d58000000???65520???64188???64188?rw---???[?anon?]
00007f2d70000000???65520???64192???64192?rw---???[?anon?]
00000000c0000000?1049088?1049088?1049088?rw---???[?anon?]
total?kB?????????8492740?3511008?3498584
```
通過 Google,找到以下資料 Linux glibc >= 2.10 (RHEL 6) malloc may show excessive virtual memory usage)?。
文章指出造成應用程序大量申請 64M 大內存塊的原因是由 Glibc 的一個版本升級引起的,通過 export MALLOC_ARENA_MAX=4 可以解決 VSZ 占用過高的問題。雖然這也是一個問題,但卻不是我們想要的,因為我們增長的是物理內存,而不是虛擬內存,程序在這一方面表現是正常的。
#### 5. gdb
非常好奇 64M 或者其他小內存塊中是什么內容,接下來可以通過 gdb?工具將其 dump 出來。
讀取 /proc 目錄下的 maps 文件,能精準地知曉目前進程的內存分布。以下腳本通過傳入進程 id,能夠將所關聯的內存全部 dump 到文件中。注意,這個命令會影響服務,要慎用。
```
pid=$1;grep?rw-p?/proc/$pid/maps?|?sed?-n?'s/^\([0-9a-f]*\)-\([0-9a-f]*\)?.*$/\1?\2/p'?|?while?read?start?stop;?do?gdb?--batch?--pid?$pid?-ex?"dump?memory?$1-$start-$stop.dump?0x$start?0x$stop";?done
```
這個命令十分霸道,甚至把加載到內存中的 class 文件、堆文件一塊給 dump 下來。這是機器的原始內存,大多數文件我們打不開。?

更多時候,只需要 dump 一部分內存就可以。再次提醒操作會影響服務,注意 dump 的內存塊大小,線上一定要慎用。
我們復制 pman 的一塊 64M 內存,比如 00007f2d70000000,然后去掉前面的 0,使用下面代碼得到內存塊的開始和結束地址。
```
cat?/proc/2154/maps?|?grep?7f2d70000000
7f2d6fff1000-7f2d70000000?---p?00000000?00:00?0?7f2d70000000-7f2d73ffc000?rw-p?00000000?00:00?0
```
接下來就 dump 這 64MB 的內存。
```
gdb?--batch?--pid?2154?-ex?"dump?memory?a.dump?0x7f2d70000000?0x7f2d73ffc000"
```
使用 du 命令查看具體的內存塊大小,不多不少正好 64M。
```
#?du?-h?a.dump
64M?a.dump
```
是時候查看里面的內容了,使用 strings 命令可以看到內存塊里一些可以打印的內容。
```
#?strings?-10?a.dump
0R4f1Qej1ty5GT8V1R8no6T44564wz499E6Y582q2R9h8CC175GJ3yeJ1Q3P5Vt757Mcf6378kM36hxZ5U8uhg2A26T5l7f68719WQK6vZ2BOdH9lH5C7838qf1
...
```
等等?這些內容不應該在堆里面么?為何還會使用額外的內存進行分配?那么還有什么地方在分配堆外內存呢?
這種情況,只可能是 native 程序對堆外內存的操作。
#### 6. perf
下面介紹一個神器 perf,除了能夠進行一些性能分析,它還能幫助我們找到相應的 native 調用。這么突出的堆外內存使用問題,肯定能找到相應的調用函數。
使用?perf record -g -p 2154?開啟監控棧函數調用,然后訪問服務器的 8888 端口,這將會把內存使用的閾值增加到 85%,我們的程序會逐漸把這部分內存占滿,你可以 syi。perf 運行一段時間后 Ctrl+C 結束,會生成一個文件 perf.data。
執行 `perf report -i perf.data` 查看報告。?

如圖,一般第三方 JNI 程序,或者 JDK 內的模塊,都會調用相應的本地函數,在 Linux 上,這些函數庫的后綴都是 so。
我們依次瀏覽用的可疑資源,發現了“libzip.so”,還發現了不少相關的調用。搜索 zip(輸入 / 進入搜索模式),結果如下:

查看 JDK 代碼,發現 bzip 大量使用了 native ?方法。也就是說,有大量內存的申請和銷毀,是在堆外發生的。?

進程調用了Java_java_util_zip_Inflater_inflatBytes() 申請了內存,卻沒有調用 Deflater 釋放內存。與 pmap 內存地址相比對,確實是 zip 在搞鬼。
#### 7. gperftools
google 還有一個類似的、非常好用的工具,叫做 gperftools,我們主要用到它的 Heap Profiler,功能更加強大。
它的啟動方式有點特別,安裝成功之后,你只需要輸出兩個環境變量即可。
```
mkdir?-p?/opt/test?
export?LD_PRELOAD=/usr/lib64/libtcmalloc.so?
export?HEAPPROFILE=/opt/test/heap
```
在同一個終端,再次啟動我們的應用程序,可以看到內存申請動作都被記錄到了 opt 目錄下的 test 目錄。

接下來,我們就可以使用 pprof 命令分析這些文件。
```
cd?/opt/test
pprof?-text?*heap??|?head?-n?200
```
使用這個工具,能夠一眼追蹤到申請內存最多的函數。Java_java_util_zip_Inflater_init 這個函數立馬就被發現了。
```
Total:?25205.3?MB
?20559.2??81.6%??81.6%??20559.2??81.6%?inflateBackEnd
??4487.3??17.8%??99.4%???4487.3??17.8%?inflateInit2_
????75.7???0.3%??99.7%?????75.7???0.3%?os::malloc@8bbaa0
????70.3???0.3%??99.9%???4557.6??18.1%?Java_java_util_zip_Inflater_init
?????7.1???0.0%?100.0%??????7.1???0.0%?readCEN
?????3.9???0.0%?100.0%??????3.9???0.0%?init
?????1.1???0.0%?100.0%??????1.1???0.0%?os::malloc@8bb8d0
?????0.2???0.0%?100.0%??????0.2???0.0%?_dl_new_object
?????0.1???0.0%?100.0%??????0.1???0.0%?__GI__dl_allocate_tls
?????0.1???0.0%?100.0%??????0.1???0.0%?_nl_intern_locale_data
?????0.0???0.0%?100.0%??????0.0???0.0%?_dl_check_map_versions
?????0.0???0.0%?100.0%??????0.0???0.0%?__GI___strdup
?????0.0???0.0%?100.0%??????0.1???0.0%?_dl_map_object_deps
?????0.0???0.0%?100.0%??????0.0???0.0%?nss_parse_service_list
?????0.0???0.0%?100.0%??????0.0???0.0%?__new_exitfn
?????0.0???0.0%?100.0%??????0.0???0.0%?getpwuid
?????0.0???0.0%?100.0%??????0.0???0.0%?expand_dynamic_string_token
```
#### 8. 解決
這就是我們模擬內存泄漏的整個過程,到此問題就解決了。
GZIPInputStream 使用 Inflater 申請堆外內存、Deflater 釋放內存,調用 close() 方法來主動釋放。如果忘記關閉,Inflater 對象的生命會延續到下一次 GC,有一點類似堆內的弱引用。在此過程中,堆外內存會一直增長。
把 decompress 函數改成如下代碼,重新編譯代碼后觀察,問題解決。
```
public?static?String?decompress(byte[]?input)?throws?Exception?{
????????ByteArrayOutputStream?out?=?new?ByteArrayOutputStream();
????????GZIPInputStream?gzip?=?new?GZIPInputStream(new?ByteArrayInputStream(input));
????????try?{
????????????copy(gzip,?out);
????????????return?new?String(out.toByteArray());
????????}finally?{
????????????try{?gzip.close();?}catch?(Exception?ex){}
????????????try{?out.close();?}catch?(Exception?ex){}
????????}
????}
```
#### 9. 小結
本課時使用了非常多的工具和命令來進行堆外內存的排查,可以看到,除了使用 jmap 獲取堆內內存,還對堆外內存的獲取也有不少辦法。
現在,我們可以把堆外內存進行更加細致地劃分了。
元空間屬于堆外內存,主要是方法區和常量池的存儲之地,使用數“MaxMetaspaceSize”可以限制它的大小,我們也能觀測到它的使用。
直接內存主要是通過 DirectByteBuffer 申請的內存,可以使用參數“MaxDirectMemorySize”來限制它的大小(參考第 10 課時)。
其他堆外內存,主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申請的內存。這種情況,就沒有任何參數能夠阻擋它們,要么靠它自己去釋放一些內存,要么等待操作系統對它的審判了。
還有一種情況,和內存的使用無關,但是也會造成內存不正常使用,那就是使用了 Process 接口,直接調用了外部的應用程序,這些程序對操作系統的內存使用一般是不可預知的。
本課時介紹的一些工具,很多高級研發,包括一些面試官,也是不知道的;即使了解這個過程,不實際操作一遍,也很難有深刻的印象。通過這個例子,你可以看到一個典型的堆外內存問題的排查思路。
堆外內存的泄漏是非常嚴重的,它的排查難度高、影響大,甚至會造成宿主機的死亡。在排查內存問題時,不要忘了這一環。
- 前言
- 開篇詞
- 基礎原理
- 第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 面試題補充