本課時我們主要從一個實戰案例入手分析面對突如其來的 GC 問題該如何下手解決。
想要下手解決 GC 問題,我們首先需要掌握下面這三種問題。
* 如何使用 jstat 命令查看 JVM 的 GC 情況?
* 面對海量 GC 日志參數,如何快速抓住問題根源?
* 你不得不掌握的日志分析工具。
工欲善其事,必先利其器。我們前面課時講到的優化手段,包括代碼優化、擴容、參數優化,甚至我們的估算,都需要一些支撐信息加以判斷。

對于 JVM 來說,一種情況是 GC 時間過長,會影響用戶的體驗,這個時候就需要調整某些 JVM 參數、觀察日志。
另外一種情況就比較嚴重了,發生了 OOM,或者操作系統的內存溢出。服務直接宕機,我們要尋找背后的原因。
這時,GC 日志能夠幫我們找到問題的根源。本課時,我們就簡要介紹一下如何輸出這些日志,以及如何使用這些日志的支撐工具解決問題。
#### GC 日志輸出
你可能感受到,最近幾年 Java 的版本更新速度是很快的,JVM 的參數配置其實變化也很大。就拿 GC 日志這一塊來說,Java 9 幾乎是推翻重來。網絡上的一些文章,把這些參數寫的亂七八糟,根本不能投入生產。如果你碰到不能被識別的參數,先確認一下自己的 Java 版本。
在事故出現的時候,通常并不是那么溫柔。你可能在半夜里就能接到報警電話,這是因為很多定時任務都設定在夜深人靜的時候執行。
這個時候,再去看 jstat 已經來不及了,我們需要保留現場。這個便是看門狗的工作,看門狗可以通過設置一些 JVM 參數進行配置。
那在實踐中,要怎么用呢?請看下面命令行。
#### Java 8
我們先看一下 JDK8 中的使用。
```
#!/bin/sh
LOG_DIR="/tmp/logs"
JAVA_OPT_LOG="?-verbose:gc"
JAVA_OPT_LOG="${JAVA_OPT_LOG}?-XX:+PrintGCDetails"
JAVA_OPT_LOG="${JAVA_OPT_LOG}?-XX:+PrintGCDateStamps"
JAVA_OPT_LOG="${JAVA_OPT_LOG}?-XX:+PrintGCApplicationStoppedTime"
JAVA_OPT_LOG="${JAVA_OPT_LOG}?-XX:+PrintTenuringDistribution"
JAVA_OPT_LOG="${JAVA_OPT_LOG}?-Xloggc:${LOG_DIR}/gc_%p.log"
JAVA_OPT_OOM="?-XX:+HeapDumpOnOutOfMemoryError?-XX:HeapDumpPath=${LOG_DIR}?-XX:ErrorFile=${LOG_DIR}/hs_error_pid%p.log?"
JAVA_OPT="${JAVA_OPT_LOG}?${JAVA_OPT_OOM}"
JAVA_OPT="${JAVA_OPT}?-XX:-OmitStackTraceInFastThrow"
```
合成一行。
```
-verbose:gc?-XX:+PrintGCDetails?-XX:+PrintGCDateStamps?
-XX:+PrintGCApplicationStoppedTime?-XX:+PrintTenuringDistribution?
-Xloggc:/tmp/logs/gc_%p.log?-XX:+HeapDumpOnOutOfMemoryError?
-XX:HeapDumpPath=/tmp/logs?-XX:ErrorFile=/tmp/logs/hs_error_pid%p.log?
-XX:-OmitStackTraceInFastThrow
```
然后我們來解釋一下這些參數:
| 參數 | 意義 |
| --- | --- |
| -verbose:gc | 打印 GC 日志 |
| PrintGCDetails | 打印詳細 GC 日志 |
| PrintGCDateStamps | 系統時間,更加可讀,PrintGCTimeStamps 是 JVM 啟動時間 |
| PrintGCApplicationStoppedTime | 打印 STW 時間 |
| PrintTenuringDistribution | 打印對象年齡分布,對調優 MaxTenuringThreshold 參數幫助很大 |
| loggc | 將以上 GC 內容輸出到文件中 |
再來看下 OOM 時的參數:
| 參數 | 意義 |
| --- | --- |
| HeapDumpOnOutOfMemoryError | OOM 時 Dump 信息,非常有用 |
| HeapDumpPath | Dump 文件保存路徑 |
| ErrorFile | 錯誤日志存放路徑 |
注意到我們還設置了一個參數 OmitStackTraceInFastThrow,這是 JVM 用來縮簡日志輸出的。
開啟這個參數之后,如果你多次發生了空指針異常,將會打印以下信息。
```
java.lang.NullPointerException
java.lang.NullPointerException
java.lang.NullPointerException
java.lang.NullPointerException
```
在實際生產中,這個參數是默認開啟的,這樣就導致有時候排查問題非常不方便(很多研發對此無能為力),我們這里把它關閉,但這樣它會輸出所有的異常堆棧,日志會多很多。
#### Java 13
再看下 JDK 13 中的使用。
從 Java 9 開始,移除了 40 多個 GC 日志相關的參數。具體參見 JEP 158。所以這部分的日志配置有很大的變化。
我們同樣看一下它的生成腳本。
```
#!/bin/sh
LOG_DIR="/tmp/logs"
JAVA_OPT_LOG="?-verbose:gc"
JAVA_OPT_LOG="${JAVA_OPT_LOG}?-Xlog:gc,gc+ref=debug,gc+heap=debug,gc+age=trace:file=${LOG_DIR}/gc_%p.log:tags,uptime,time,level"
JAVA_OPT_LOG="${JAVA_OPT_LOG}?-Xlog:safepoint:file=${LOG_DIR}/safepoint_%p.log:tags,uptime,time,level"
JAVA_OPT_OOM="?-XX:+HeapDumpOnOutOfMemoryError?-XX:HeapDumpPath=${LOG_DIR}?-XX:ErrorFile=${LOG_DIR}/hs_error_pid%p.log?"
JAVA_OPT="${JAVA_OPT_LOG}?${JAVA_OPT_OOM}"
JAVA_OPT="${JAVA_OPT}?-XX:-OmitStackTraceInFastThrow"
echo?$JAVA_OPT
```
合成一行展示。
```
-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
```
可以看到 GC 日志的打印方式,已經完全不一樣,但是比以前的日志參數規整了許多。
我們除了輸出 GC 日志,還輸出了 safepoint 的日志。這個日志對我們分析問題也很重要,那什么叫 safepoint 呢?
safepoint 是 JVM 中非常重要的一個概念,指的是可以安全地暫停線程的點。
當發生 GC 時,用戶線程必須全部停下來,才可以進行垃圾回收,這個狀態我們可以認為 JVM 是安全的(safe),整個堆的狀態是穩定的。

如果在 GC 前,有線程遲遲進入不了 safepoint,那么整個 JVM 都在等待這個阻塞的線程,會造成了整體 GC 的時間變長。
所以呢,并不是只有 GC 會掛起 JVM,進入 safepoint 的過程也會。這個概念,如果你有興趣可以自行深挖一下,一般是不會出問題的。
如果面試官問起你在項目中都使用了哪些打印 GC 日志的參數,上面這些信息肯定是不很好記憶。你需要進行以下總結。比如:
“我一般在項目中輸出詳細的 GC 日志,并加上可讀性強的 GC 日志的時間戳。特別情況下我還會追加一些反映對象晉升情況和堆詳細信息的日志,用來排查問題。另外,OOM 時自動 Dump 堆棧,我一般也會進行配置”。
#### GC 日志的意義
我們首先看一段日志,然后簡要看一下各個階段的意義。

* 1 表示 GC 發生的時間,一般使用可讀的方式打印;
* 2 表示日志表明是 G1 的“轉移暫停: 混合模式”,停頓了約 223ms;
* 3 表明由 8 個 Worker 線程并行執行,消耗了 214ms;
* 4 表示 Diff 越小越好,說明每個工作線程的速度都很均勻;
* 5 表示外部根區掃描,外部根是堆外區。JNI 引用,JVM 系統目錄,Classloaders 等;
* 6 表示更新 RSet 的時間信息;
* 7 表示該任務主要是對 CSet 中存活對象進行轉移(復制);
* 8 表示花在 GC 之外的工作線程的時間;
* 9 表示并行階段的 GC 總時間;
* 10 表示其他清理活動;
* 11表示收集結果統計;
* 12 表示時間花費統計。
可以看到 GC 日志描述了垃圾回收器過程中的幾乎每一個階段。但即使你了解了這些數值的意義,在分析問題時,也會感到吃力,我們一般使用圖形化的分析工具進行分析。
尤其注意的是最后一行日志,需要詳細描述。可以看到 G C花費的時間,竟然有 3 個數值。這個數值你可能在多個地方見過。如果你手頭有 Linux 機器,可以執行以下命令:
time?ls?/

可以看到一段命令的執行,同樣有三種緯度的時間統計。接下來解釋一下這三個字段的意思。
* real 實際花費的時間,指的是從開始到結束所花費的時間。比如進程在等待 I/O 完成,這個阻塞時間也會被計算在內;
* user 指的是進程在用戶態(User Mode)所花費的時間,只統計本進程所使用的時間,注意是指多核;
* sys 指的是進程在核心態(Kernel Mode)花費的 CPU 時間量,指的是內核中的系統調用所花費的時間,只統計本進程所使用的時間。
在上面的 GC 日志中,real < user + sys,因為我們使用了多核進行垃圾收集,所以實際發生的時間比 (user + sys) 少很多。在多核機器上,這很常見。
[Times: user=1.64 sys=0.00, real=0.23 secs]
下面是一個串行垃圾收集器收集的 GC 時間的示例。由于串行垃圾收集器始終僅使用一個線程,因此實際使用的時間等于用戶和系統時間的總和:
[Times: user=0.29 sys=0.00, real=0.29 secs]
那我們統計 GC 以哪個時間為準呢?一般來說,用戶只關心系統停頓了多少秒,對實際的影響時間非常感興趣。至于背后是怎么實現的,是多核還是單核,是用戶態還是內核態,它們都不關心。所以我們直接使用 real 字段。
#### GC日志可視化
肉眼可見的這些日志信息,讓人非常頭暈,尤其是日志文件特別大的時候。所幸現在有一些在線分析平臺,可以幫助我們分析這個過程。下面我們拿常用的 gceasy 來看一下。
以下是一個使用了 G1 垃圾回收器,堆內存為 6GB 的服務,運行 5 天的 GC 日志。
(1)堆信息

我們可以從圖中看到堆的使用情況。
(2)關鍵信息
從圖中我們可以看到一些性能的關鍵信息。
吞吐量:98.6%(一般超過 95% 就 ok 了);
最大延遲:230ms,平均延遲:42.8ms;
延遲要看服務的接受程度,比如 SLA 定義 50ms 返回數據,上面的最大延遲就會有一點問題。本服務接近 99% 的停頓在 100ms 以下,可以說算是非常優秀了。

你在看這些信息的時候,一定要結合宿主服務器的監控去看。比如 GC 發生期間,CPU 會突然出現尖鋒,就證明 GC 對 CPU 資源使用的有點多。但多數情況下,如果吞吐量和延遲在可接受的范圍內,這些對 CPU 的超額使用是可以忍受的。
(3)交互式圖表

可以對有問題的區域進行放大查看,圖中表示垃圾回收后的空間釋放,可以看到效果是比較好的。
(4)G1 的時間耗時

如圖展示了 GC 的每個階段花費的時間。可以看到平均耗時最長的階段,就是 Concurrent Mark 階段,但由于是并發的,影響并不大。隨著時間的推移,YoungGC 竟然達到了 136485 次。運行 5 天,光花在 GC 上的時間就有 2 個多小時,還是比較可觀的。
(5)其他

如圖所示,整個 JVM 創建了 100 多 T 的數據,其中有 2.4TB 被 promoted 到老年代。
另外,還有一些 safepoint 的信息等,你可以自行探索。
那到底什么樣的數據才是有問題的呢?gceasy 提供了幾個案例。比如下面這個就是停頓時間明顯超長的 GC 問題。

下面這個是典型的內存泄漏。

上面這些問題都是非常明顯的。但大多數情況下,問題是偶發的。從基本的衡量指標,就能考量到整體的服務水準。如果這些都沒有問題,就要看曲線的尖峰。
一般來說,任何不平滑的曲線,都是值得懷疑的,那就需要看一下當時的業務情況具體是什么樣子的。是用戶請求突增引起的,還是執行了一個批量的定時任務,再或者查詢了大批量的數據,這要和一些服務的監控一起看才能定位出根本問題。
只靠 GC 來定位問題是比較困難的,我們只需要知道它有問題就可以了。后面,會介紹更多的支持工具進行問題的排解。
為了方便你調試使用,我在 GitHub 上上傳了兩個 GC 日志。其中 gc01.tar.gz 就是我們現在正在看的,解壓后有 200 多兆;另外一個 gc02.tar.gz 是一個堆空間為 1GB 的日志文件,你也可以下載下來體驗一下。
GitHub 地址:
https://gitee.com/xjjdog/jvm-lagou-res
另外,GCViewer 這個工具也是常用的,可以下載到本地,以 jar 包的方式運行。
在一些極端情況下,也可以使用腳本簡單過濾一下。比如下面行命令,就是篩選停頓超過 100ms 的 GC 日志和它的行數(G1)。
```
#?grep?-n?real?gc.log?|?awk?-F"=|?"?'{?if($8>0.1){?print?}}'
1975:?[Times:?user=2.03?sys=0.93,?real=0.75?secs]
2915:?[Times:?user=1.82?sys=0.65,?real=0.64?secs]
16492:?[Times:?user=0.47?sys=0.89,?real=0.35?secs]
16627:?[Times:?user=0.71?sys=0.76,?real=0.39?secs]
16801:?[Times:?user=1.41?sys=0.48,?real=0.49?secs]
17045:?[Times:?user=0.35?sys=1.25,?real=0.41?secs]
```
#### jstat
上面的可視化工具,必須經歷導出、上傳、分析三個階段,這種速度太慢了。有沒有可以實時看堆內存的工具?
你可能會第一時間想到 jstat 命令。第一次接觸這個命令,我也是很迷惑的,主要是輸出的字段太多,不了解什么意義。
但其實了解我們在前幾節課時所講到內存區域劃分和堆劃分之后,再看這些名詞就非常簡單了。

我們拿 -gcutil 參數來說明一下。
```
jstat -gcutil $pid 1000
```
只需要提供一個 Java 進程的 ID,然后指定間隔時間(毫秒)就 OK 了。
```
S0?S1?E?O?M?CCS?YGC?YGCT?FGC?FGCT?GCT
0.00?0.00?72.03?0.35?54.12?55.72?11122?16.019?0?0.000?16.019
0.00?0.00?95.39?0.35?54.12?55.72?11123?16.024?0?0.000?16.024
0.00?0.00?25.32?0.35?54.12?55.72?11125?16.025?0?0.000?16.025
0.00?0.00?37.00?0.35?54.12?55.72?11126?16.028?0?0.000?16.028
0.00?0.00?60.35?0.35?54.12?55.72?11127?16.028?0?0.000?16.028
```
可以看到,E 其實是 Eden 的縮寫,S0 對應的是 Surivor0,S1 對應的是 Surivor1,O 代表的是 Old,而 M 代表的是 Metaspace。
YGC 代表的是年輕代的回收次數,YGC T對應的是年輕代的回收耗時。那么 FGC 肯定代表的是 Full GC 的次數。
你在看日志的時候,一定要注意其中的規律。-gcutil 位置的參數可以有很多種。我們最常用的有 gc、gcutil、gccause、gcnew 等,其他的了解一下即可。
* gc: 顯示和 GC 相關的?堆信息;
* gcutil: 顯示?垃圾回收信息;
* gccause: 顯示垃圾回收?的相關信息(同 -gcutil),同時顯示?最后一次?或?當前?正在發生的垃圾回收的?誘因;
* gcnew: 顯示?新生代?信息;
* gccapacity: 顯示?各個代?的?容量?以及?使用情況;
* gcmetacapacity: 顯示?元空間?metaspace 的大小;
* gcnewcapacity: 顯示?新生代大小?和?使用情況;
* gcold: 顯示?老年代?和?永久代?的信息;
* gcoldcapacity: 顯示?老年代?的大小;
* printcompilation: 輸出 JIT?編譯?的方法信息;
* class: 顯示?類加載?ClassLoader 的相關信息;
* compiler: 顯示 JIT?編譯?的相關信息;
如果 GC 問題特別明顯,通過 jstat 可以快速發現。我們在啟動命令行中加上參數 -t,可以輸出從程序啟動到現在的時間。如果 FGC 和啟動時間的比值太大,就證明系統的吞吐量比較小,GC 花費的時間太多了。另外,如果老年代在 Full GC 之后,沒有明顯的下降,那可能內存已經達到了瓶頸,或者有內存泄漏問題。
下面這行命令,就追加了 GC 時間的增量和 GC 時間比率兩列。
```
jstat?-gcutil?-t?90542?1000?|?awk?'BEGIN{pre=0}{if(NR>1)?{print?$0?"\t"?($12-pre)?"\t"?$12*100/$1?;?pre=$12?}?else?{?print?$0?"\tGCT_INC\tRate"}?}'
?
Timestamp?????????S0?????S1?????E??????O??????M?????CCS????YGC?????YGCT????FGC????FGCT?????GCT????GCT_INC?Rate
???????????18.7???0.00?100.00???6.02???1.45??84.81??76.09??????1????0.002?????0????0.000????0.002?0.002?0.0106952
???????????19.7???0.00?100.00???6.02???1.45??84.81??76.09??????1????0.002?????0????0.000????0.002?0?0.0101523
```
#### GC 日志也會搞鬼
順便給你介紹一個實際發生的故障。
你知道 ElasticSearch 的速度是非常快的,我們為了壓榨它的性能,對磁盤的讀寫幾乎是全速的。它在后臺做了很多 Merge 動作,將小塊的索引合并成大塊的索引。還有 TransLog 等預寫動作,都是 I/O 大戶。
使用 iostat -x 1 可以看到具體的 I/O 使用狀況。
問題是,我們有一套 ES 集群,在訪問高峰時,有多個 ES 節點發生了嚴重的 STW 問題。有的節點竟停頓了足足有 7~8 秒。
?[Times: user=0.42 sys=0.03, real=7.62 secs]?
從日志可以看到在 GC 時用戶態只停頓了 420ms,但真實的停頓時間卻有 7.62 秒。
盤點一下資源,唯一超額利用的可能就是 I/O 資源了(%util 保持在 90 以上),GC 可能在等待 I/O。
通過搜索,發現已經有人出現過這個問題,這里直接說原因和結果。
原因就在于,寫 GC 日志的 write 動作,是統計在 STW 的時間里的。在我們的場景中,由于 ES 的索引數據,和 GC 日志放在了一個磁盤,GC 時寫日志的動作,就和寫數據文件的動作產生了資源爭用。

解決方式也是比較容易的,把 ES 的日志文件,單獨放在一塊普通 HDD 磁盤上就可以了。
#### 小結
本課時,我們主要介紹了比較重要的 GC 日志,以及怎么輸出它,并簡要的介紹了一段 G1 日志的意義。對于這些日志的信息,能夠幫助我們理解整個 GC 的過程,專門去記憶它投入和產出并不成正比,可以多看下 G1 垃圾回收器原理方面的東西。
接下來我們介紹了幾個圖形化分析 GC 的工具,這也是現在主流的使用方式,因為動輒幾百 MB 的 GC 日志,是無法肉眼分辨的。如果機器的 I/O 問題很突出,就要考慮把 GC 日志移動到單獨的磁盤。
我們尤其介紹了在線分析工具 gceasy,你也可以下載 gcviewer 的 jar 包本地體驗一下。
最后我們看了一個命令行的 GC 回收工具 jstat,它的格式比較規整,可以重定向到一個日志文件里,后續使用 sed、awk 等工具進行分析。關于相關的兩個命令,可以參考我以前寫的兩篇文章。
《Linux生產環境上,最常用的一套“Sed“技巧》
《Linux生產環境上,最常用的一套“AWK“技巧》
#### 課后問答
* 1、如果幸存區中相同年齡對象大小的和,大于幸存區的一半,大于或等于 age 的對象將會直接進入老年代”應該為小于等于某一年齡的對象大小總和?
答案:感謝提醒,參考代碼share/gc/shared/ageTable.cpp中的compute_tenuring_threshold函數,重新表述如下:從年齡最小的對象開始累加,如果累加的對象大小,大于幸存區的一半,則講當前的對象age將作為新的閾值,年齡大于此閾值的對象直接進入老年代。
* 2、另外“幸存區的一半”最好提一下 TargetSurvivorRatio 這個參數。
答案:值的注意的是。使用“grep -rn -i --color TargetSurvivorRatio .”搜索(jdk13),可以看到這個參數只影響serial和G1收集器,還稍微影響PLAB緩沖區的大小。
* 3、完整的項目代碼么?
答案:https://gitee.com/xjjdog/jvm-lagou-res
* 4、枚舉類的內存模型是啥
答案:java類和字節碼,沒有內存模型這個概念。Java的內存模型,指的是JMM,與多線程協作有關。像堆、虛擬機棧這些劃分,也不叫內存模型,叫內存布局。這個千萬別搞混了。 你應該是說enum的字節碼表現形式。可以使用javac E.java && javap -v -p? E看一下輸出。 public enum E{ A, B, C, D } javap輸出: public final class E extends java.lang.Enum<E> 可以看到enum只是一種語法上的便捷方式,繼承的是Enum類。
* 5、“由于常量池,在 Java 7 之后,放到了堆中,我們創建的字符串,將會在堆上分配”。但是您上文也說了,JAVA8開始,metasapce是非堆區域,而且文中也提到了該區域包含的內容是類的信息、常量池、方法數據、方法代碼。那么java字符串常量,就不應該在堆上創建了啊。勞煩您解釋下,添麻煩了,謝謝。
答案:你好,JVM中存在多個常量池。把第一個改成字符串常量池就比較好理解了。 1、字符串常量池,已經移動到堆上(jdk8之前是perm區),也就是執行intern方法后存的地方。 2、類文件常量池,constant_pool,是每個類每個接口所擁有的,第四節字節碼中“#n”的那些都是。這部分數據在方法區,也就是元數據區。而運行時常量池是在類加載后的一個內存區域,它們都在元空間。
* 6、大家都知道,JVM 在運行時,會從操作系統申請大塊的堆內內存,進行數據的存儲。但是,堆外內存也就是申請后操作系統剩余的內存,也會有部分受到 JVM 的控制。比較典型的就是一些 native 關鍵詞修飾的方法,以及對內存的申請和處理 這句話是jvm去申請了一塊操作系統的堆內內存,那圖上怎么jvm申請的內存包括了堆內存和非堆內存,有點疑惑。就是jvm申請的內存其實有這兩個?
答案:可以這樣理解: 操作系統有8G。-Xmx分配了4G(堆內內存),Metaspace使用了256M(堆外內存) 剩下的 8G-4G-256M ,就是操作系統剩下的本地內存。具體有沒有可能變成堆外內存,要看情況。 比如: (1)netty的direct buffer使用了額外的120MB內存,那么現在JVM占用的堆外內存就有 256M+120M (2)使用了jni或者jna,直接申請了內存2GB,那么現在JVM占用的堆外內存就有256M+120M+2GB (3)網絡socket連接等,占用了操作系統的50MB內存 這個時候,留給操作系統的就只剩下了:8GB-4GB-256M-120M-2GB-50M。具體“堆和堆外”一共用了多少,可以top命令,看RSS段。
* 7、被final修飾的成員變量,會被gc嗎,可否細致的講下。
答案:您好,對象是否被GC,和是否是final變量沒有關系。關于final,我們將在19小節的并發編程中詳細介紹。
* 8、想知道類加載器是加載字節碼變成機器碼給執行引擎去執行的,那么類加載器是誰來加載的?
答案:啟動類加載器,就是最上面那一個,是c代碼實現的,沒有繼承classloader類。它就是一段native邏輯,所以沒有加載這種概念。它的實現參考${openjdk}\hotspot\src\share\vm\classfile 目錄下的 classLoader.cpp 與classLoader.hpp
* 9、JMM 保證了 read、load、assign、use、store 和 write 六個操作具有原子性,可以認為除了 long 和 double 類型以外,對其他基本數據類型所對應的內存單元的訪問讀寫都是原子的。long和double沒有原子性?
答案:目前大多數機器是64位的,你可以認為是原子的。這是因為,在32位操作系統上對64位的數據的讀寫要分兩步完成,每一步取32位數據。隨著時間推移,這種知識點會越來越冷。
* 10、字節碼的執行流程可以這樣理解嗎?字節碼在Java虛擬機棧中被執行,每一項內容都可以看作是一個棧幀,棧幀的結構包括局部變量表、操作數棧、鏈接、返回地址。這時候就很明了了,棧幀的執行流程就是字節碼的執行流程了。類中變量會被解析到局部變量表,然后對操作數棧進行入棧出棧的操作,在此期間有可能引用到動態或靜態鏈接,最后把計算結果的引用地址返回。不知道這個理解是否正確,麻煩老師指正,謝謝?
答案:正確
* 11、文中說除了基本類型,其他都是在堆上分配的。之前粗略在哪個地方看到過jvm會判斷對象是否存在線程逃逸,如果不存在就直接在棧上分配對象。這種在棧上創建對象的情況是怎樣的呢。
答案:你說的沒錯,這種情況在第6小節已聊到了。不過它是分層編譯的優化手段,所以我們在后面JIT小節還會碰到它。
* 12、“它只是自定義的加載器順序不同,但對于頂層來說,還是一樣的。”這句話是什么意思呢?為什么自己寫的ArrayList不會被加載?
答案:loadClass的邏輯是可以非常靈活的,以下代碼來自tomcat-9.0.30。 // (0.2) Try loading the class with the system class loader, to prevent // the webapp from overriding Java SE classes. This implements // SRV.10.7.2 String resourceName = binaryNameToPath(name, false); ClassLoader javaseLoader = getJavaseClassLoader(); boolean tryLoadingFromJavaseLoader; 第一步就是嘗試從javabase加載哦,加載不到才走其他邏輯。感興趣可以參照WebappClassLoaderBase.java文件。
* 13、老師您好,這里對于虛擬機棧有點不太理解,原文:這里有一個兩層的棧。第一層是棧幀,對應著方法;第二層是方法的執行,對應著操作數棧;這里是說棧幀是說具體的Java方法,而真正的調用,是在棧幀中里面還建了一個操作數棧對嗎?為什么要這么做呀?
答案:線程方法棧(棧)->棧幀(元素)=>方法級別的操作。 棧幀里的操作數棧(棧)->操作數(元素)=> 字節碼指令級的操作。 主管的功能不同,層次也不同。
* 14、從文章中copy 命令是不能直接運行的,手動敲可以,肉眼對比看不出有什么不同,試后發現是copy的問題是在空格,刪了“空格”,重新輸入空格正常。是故意這樣做,還是有問題??
答案:可能是顯示問題,很多編輯器編輯之后都這樣
- 前言
- 開篇詞
- 基礎原理
- 第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 面試題補充