最后一課時我們來分析常見的 JVM 面試題。
市面上關于 JVM 的面試題實在太多了,本課程中的第 02 ~ 06 課時是理論面試題的重災區,并且是比較深入的題目,而本課時則選取了一些基礎且常見的題目。
有些面試題是開放性的,而有些面試題是知識性的,要注意區別。面試題并沒有標準答案,尤其是開放性題目,你需要整理成白話文,來盡量的展示自己。如果你在回答的過程中描述了一些自己不是很熟悉的內容,可能會受到追問。所以,根據問題,建議整理一份適合自己的答案,這比拿來主義更讓人印象深刻。
#### 勘誤
我們來回憶一下課程中曾講解過的容易出錯或模糊的知識點。
不知你是否還記得?我們在每一課時的講解中,都有聚焦的點,不同的問法可能會有不同的回答,要注意。
#### 對象在哪里分配?
在第 02 課時中,談到了數組和對象是堆上分配,當學完第 22 課時的逃逸分析后,我們了解到并不完全是這樣的。由于 JIT 的存在,如果發現某些對象沒有逃逸出方法,那么就有可能被優化成了棧上分配。
#### CMS 是老年代垃圾回收器?
初步印象是,但實際上不是。根據 CMS 的各個收集過程,它其實是一個涉及年輕代和老年代的綜合性垃圾回收器。在很多文章和書籍的劃分中,都將 CMS 劃分為了老年代垃圾回收器,加上它主要作用于老年代,所以一般誤認為是。
#### 常量池問題
常量池的表述有些模糊,在此細化一下,注意我們指的是 Java 7 版本之后。
JVM 中有多個常量池:
* 字符串常量池,存放在堆上,也就是執行 intern 方法后存的地方,class 文件的靜態常量池,如果是字符串,則也會被裝到字符串常量池中。
* 運行時常量池,存放在方法區,屬于元空間,是類加載后的一些存儲區域,大多數是類中 constant_pool 的內容。
* 類文件常量池,也就是 constant_pool,這個是概念性的,并沒有什么實際存儲區域。
在平常的交流過程中,聊的最多的是字符串常量池,具體可參考官網。
#### ZGC 支持的堆上限?
Java 13 增加到 16TB,Java 11 還是 4 TB,技術在發展,請保持關注。
年輕代提升閾值動態計算的描述
在第 06 課時中對于年輕代“動態對象年齡判定”的表述是錯誤的。
參考代碼 share/gc/shared/ageTable.cpp 中的 compute_tenuring_threshold 函數,重新表述為:程序從年齡最小的對象開始累加,如果累加的對象大小,大于幸存區的一半,則將當前的對象 age 作為新的閾值,年齡大于此閾值的對象則直接進入老年代。
這里說的一半,是通過 TargetSurvivorRatio 參數進行設置的。
#### 永久代
雖然課程一直在強調,是基于 Java 8+ 版本進行講解的,但還是有讀者提到了永久代。這部分知識容易發生混淆,面試頻率也很高,建議集中消化一下。

上面是第 02 課時中的一張圖,注意左半部分是 Java 8 版本之前的內存區域,右半部分是 Java 8 的內存區域,主要區別就在 Perm 區和 Metaspace 區。
Perm 區屬于堆,獨立控制大小,在 Java 8 中被移除了(JEP122),原來的方法區就在這里;Metaspace 是非堆,默認空間無上限,方法區移動到了這里。
#### 常見面試題
* [ ] JVM 有哪些內存區域?(JVM 的內存布局是什么?)
JVM 包含堆、元空間、Java 虛擬機棧、本地方法棧、程序計數器等內存區域,其中,堆是占用內存最大的一塊,如下圖所示。

* [ ] Java 的內存模型是什么?(JMM 是什么?)
JVM 試圖定義一種統一的內存模型,能將各種底層硬件以及操作系統的內存訪問差異進行封裝,使 Java 程序在不同硬件以及操作系統上都能達到相同的并發效果。它分為工作內存和主內存,線程無法對主存儲器直接進行操作,如果一個線程要和另外一個線程通信,那么只能通過主存進行交換,如下圖所示。

* [ ] JVM 垃圾回收時如何確定垃圾?什么是 GC Roots?
JVM 采用的是可達性分析算法。JVM 是通過 GC Roots 來判定對象存活的,從 GC Roots 向下追溯、搜索,會產生一個叫做 Reference Chain 的鏈條。當一個對象不能和任何一個 GC Root 產生關系時,就判定為垃圾,如下圖所示。

GC Roots 大體包括:
* 活動線程相關的各種引用,比如虛擬機棧中 棧幀里的引用;
* 類的靜態變量引用;
* JNI 引用等。
注意:要想回答的更詳細一些,請參照第 05 課時中的內容。
* [ ] 能夠找到 Reference Chain 的對象,就一定會存活么?
不一定,還要看 Reference 類型,弱引用在 GC 時會被回收,軟引用在內存不足的時候會被回收,但如果沒有 Reference Chain 對象時,就一定會被回收。
* [ ] 強引用、軟引用、弱引用、虛引用是什么?
* 普通的對象引用關系就是強引用。
* 軟引用用于維護一些可有可無的對象。只有在內存不足時,系統則會回收軟引用對象,如果回收了軟引用對象之后仍然沒有足夠的內存,才會拋出內存溢出異常。
* 弱引用對象相比軟引用來說,要更加無用一些,它擁有更短的生命周期,當 JVM 進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。
* 虛引用是一種形同虛設的引用,在現實場景中用的不是很多,它主要用來跟蹤對象被垃圾回收的活動。
* [ ] 你說你做過 JVM 參數調優和參數配置,請問如何查看 JVM 系統默認值
使用 -XX:+PrintFlagsFinal 參數可以看到參數的默認值,這個默認值還和垃圾回收器有關,比如 UseAdaptiveSizePolicy。
* [ ] 你平時工作中用過的 JVM 常用基本配置參數有哪些?
主要有 Xmx、Xms、Xmn、MetaspaceSize 等。
更加詳細的可參照第 23 課時的參數總結,你只需要記憶 10 個左右即可,建議記憶 G1 相關的參數。面試時間有限,不會在這上面糾結,除非你表現的太囂張了。
* [ ] 請你談談對 OOM 的認識
OOM 是非常嚴重的問題,除了程序計數器,其他內存區域都有溢出的風險。和我們平常工作最密切的,就是堆溢出,另外,元空間在加載的類非常多的情況下也會溢出,還有就是棧溢出,這個通常影響比較小。堆外也有溢出的可能,這個就比較難排查了。
* [ ] 你都有哪些手段用來排查內存溢出?
這個話題很大,可以從實踐環節中隨便摘一個進行總結,下面舉一個最普通的例子。
內存溢出包含很多種情況,我在平常工作中遇到最多的就是堆溢出。有一次線上遇到故障,重新啟動后,使用 jstat 命令,發現 Old 區一直在增長。我使用 jmap 命令,導出了一份線上堆棧,然后使用 MAT 進行分析,通過對 GC Roots 的分析,發現了一個非常大的 HashMap 對象,這個原本是其他同事做緩存用的,但是一個無界緩存,造成了堆內存占用一直上升,后來,將這個緩存改成 guava 的 Cache,并設置了弱引用,故障就消失了。
* [ ] GC 垃圾回收算法與垃圾收集器的關系?
常用的垃圾回收算法有標記清除、標記整理、復制算法等,引用計數器也算是一種,但垃圾回收器不使用這種算法,因為有循環依賴的問題。

很多垃圾回收器都是分代回收的:
* 對于年輕代,主要有 Serial、ParNew 等垃圾回收器,回收過程主要使用復制算法;
* 老年代的回收算法有 Serial、CMS 等,主要使用標記清除、標記整理算法等。
我們線上使用較多的是 G1,也有年輕代和老年代的概念,不過它是一個整堆回收器,它的回收對象是小堆區 。
* [ ] 生產上如何配置垃圾收集器?

首先是內存大小問題,基本上每一個內存區域我都會設置一個上限,來避免溢出問題,比如元空間。通常,堆空間我會設置成操作系統的 2/3,超過 8GB 的堆,優先選用 G1。
然后我會對 JVM 進行初步優化,比如根據老年代的對象提升速度,來調整年輕代和老年代之間的比例。
接下來是專項優化,判斷的主要依據是系統容量、訪問延遲、吞吐量等,我們的服務是高并發的,所以對 STW 的時間非常敏感。
我會通過記錄詳細的 GC 日志,來找到這個瓶頸點,借用 GCeasy 這樣的日志分析工具,很容易定位到問題。
* [ ] 怎么查看服務器默認的垃圾回收器是哪一個?
這通常會使用另外一個參數,即 -XX:+PrintCommandLineFlags,來打印所有的參數,包括使用的垃圾回收器。
* [ ] 假如生產環境 CPU 占用過高,請談談你的分析思路和定位。

首先,使用 top -H 命令獲取占用 CPU 最高的線程,并將它轉化為十六進制。
然后,使用 jstack 命令獲取應用的棧信息,搜索這個十六進制,這樣就能夠方便地找到引起 CPU 占用過高的具體原因。
* [ ] 對于 JDK 自帶的監控和性能分析工具用過哪些?
* jps:用來顯示 Java 進程;
* jstat:用來查看 GC;
* jmap:用來 dump 堆;
* jstack:用來 dump 棧;
* jhsdb:用來查看執行中的內存信息。
* [ ] 棧幀都有哪些數據?
棧幀包含:局部變量表、操作數棧、動態連接、返回地址等。

?
* [ ] JIT 是什么?
為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,并進行各種層次的優化,完成這個任務的編譯器,就稱為即時編譯器(Just In Time Compiler),簡稱 JIT 編譯器。
* [ ] Java 的雙親委托機制是什么?
雙親委托的意思是,除了頂層的啟動類加載器以外,其余的類加載器,在加載之前,都會委派給它的父加載器進行加載,這樣一層層向上傳遞,直到祖先們都無法勝任,它才會真正的加載,Java 默認是這種行為。

* [ ] 有哪些打破了雙親委托機制的案例?
* Tomcat 可以加載自己目錄下的 class 文件,并不會傳遞給父類的加載器;
* Java 的 SPI,發起者是 BootstrapClassLoader,BootstrapClassLoader 已經是最上層了,它直接獲取了 AppClassLoader 進行驅動加載,和雙親委派是相反的。
* [ ] 簡單描述一下(分代)垃圾回收的過程

分代回收器有兩個分區:老生代和新生代,新生代默認的空間占總空間的 1/3,老生代的默認占比是 2/3。
新生代使用的是復制算法,新生代里有 3 個分區:Eden、To Survivor、From Survivor,它們的默認占比是 8:1:1。
當年輕代中的 Eden 區分配滿的時候,就會觸發年輕代的 GC(Minor GC),具體過程如下:
* 在 Eden 區執行了第一次 GC 之后,存活的對象會被移動到其中一個 Survivor 分區(以下簡稱 from);
* Eden 區再次 GC,這時會采用復制算法,將 Eden 和 from 區一起清理,存活的對象會被復制到 to 區,接下來,只要清空 from 區就可以了。
* [ ] CMS 分為哪幾個階段?
* 初始標記
* 并發標記
* 并發預清理
* 并發可取消的預清理
* 重新標記
* 并發清理
由于《深入理解 Java 虛擬機》一書的流行,面試時省略**并發清理、并發可取消的預清理**這兩個階段,一般也是沒問題的。
* [ ] CMS 都有哪些問題?
* 內存碎片問題,Full GC 的整理階段,會造成較長時間的停頓;
* 需要預留空間,用來分配收集階段產生的“浮動垃圾”;
* 使用更多的 CPU 資源,在應用運行的同時進行堆掃描;
* 停頓時間是不可預期的。
* [ ] 你使用過 G1 垃圾回收器的哪幾個重要參數?

最重要的是 MaxGCPauseMillis,可以通過它設定 G1 的目標停頓時間,它會盡量去達成這個目標。G1HeapRegionSize 可以設置小堆區的大小,一般是 2 的次冪。InitiatingHeapOccupancyPercent 啟動并發 GC 時的堆內存占用百分比,G1 用它來觸發并發 GC 周期,基于整個堆的使用率,而不只是某一代內存的使用比例,默認是 45%。
* [ ] GC 日志的 real、user、sys 是什么意思?
* real 指的是從開始到結束所花費的時間,比如進程在等待 I/O 完成,這個阻塞時間也會被計算在內。
* user 指的是進程在用戶態(User Mode)所花費的時間,只統計本進程所使用的時間,是指多核。
* sys ?指的是進程在核心態(Kernel Mode)所花費的 CPU 時間量,即內核中的系統調用所花費的時間,只統計本進程所使用的時間。
* [ ] 什么情況會造成元空間溢出?
元空間默認是沒有上限的,不加限制比較危險。當應用中的 Java 類過多時,比如 Spring 等一些使用動態代理的框架生成了很多類,如果占用空間超出了我們的設定值,就會發生元空間溢出。
* [ ] 什么時候會造成堆外內存溢出?
使用了 Unsafe 類申請內存,或者使用了 JNI 對內存進行操作,這部分內存是不受 JVM 控制的,不加限制使用的話,會很容易發生內存溢出。
* [ ] SWAP 會影響性能么?
當操作系統內存不足時,會將部分數據寫入到 SWAP ,但是 SWAP 的性能是比較低的。如果應用的訪問量較大,需要頻繁申請和銷毀內存,那么很容易發生卡頓。一般在高并發場景下,會禁用 SWAP。
* [ ] 有什么堆外內存的排查思路?
進程占用的內存,可以使用 top 命令,看 RES 段占用的值,如果這個值大大超出我們設定的最大堆內存,則證明堆外內存占用了很大的區域。
使用 gdb 命令可以將物理內存 dump 下來,通常能看到里面的內容。更加復雜的分析可以使用 Perf 工具,或者谷歌開源的 GPerftools。那些申請內存最多的 native 函數,就很容易找到。
* [ ] HashMap 中的 key,可以是普通對象么?有什么需要注意的地方?
Map 的 key 和 value 可以是任何類型,但要注意的是,一定要重寫它的 equals 和 hashCode 方法,否則容易發生內存泄漏。
* [ ] 怎么看死鎖的線程?
通過 jstack 命令,可以獲得線程的棧信息,死鎖信息會在非常明顯的位置(一般是最后)進行提示。
* [ ] 如何寫一段簡單的死鎖代碼?
詳情請見第 15 課時的 DeadLockDemo,筆試的話頻率也很高。
* [ ] invokedynamic 指令是干什么的?
invokedynamic 是 Java 7 版本之后新加入的字節碼指令,使用它可以實現一些動態類型語言的功能。我們使用的 Lambda 表達式,在字節碼上就是 invokedynamic 指令實現的,它的功能有點類似反射,但它是使用方法句柄實現的,執行效率更高。
* [ ] volatile 關鍵字的原理是什么?有什么作用?
使用了 volatile 關鍵字的變量,每當變量的值有變動的時候,都會將更改立即同步到主內存中;而如果某個線程想要使用這個變量,就先要從主存中刷新到工作內存,這樣就確保了變量的可見性。
一般使用一個 volatile 修飾的 bool 變量,來控制線程的運行狀態。
```
volatile?boolean?stop?=?false;
void?stop(){
this.stop?=?true;
}
void?start(){
new?Thread(()->{
while?(!stop){
//sth
}
}).start();
}
```
* [ ] 什么是方法內聯?
為了減少方法調用的開銷,可以把一些短小的方法,比如 getter/setter,納入到目標方法的調用范圍之內,這樣就少了一次方法調用,速度就能得到提升,這就是方法內聯的概念。
* [ ] 對象是怎么從年輕代進入老年代的?
在下面 4 種情況下,對象會從年輕代進入到老年代。
* 如果對象夠老,則會通過提升(Promotion)的方式進入老年代,一般根據對象的年齡進行判斷。
* 動態對象年齡判定,有的垃圾回收算法,比如 G1,并不要求 age 必須達到 15 才能晉升到老年代,它會使用一些動態的計算方法。
* 分配擔保,當 Survivor 空間不夠的時候,則需要依賴其他內存(指老年代)進行分配擔保,這個時候,對象也會直接在老年代上分配。
* 超出某個大小的對象將直接在老年代上分配,不過這個值默認為 0,意思是全部首選 Eden 區進行分配。

* [ ] safepoint 是什么?
當發生 GC 時,用戶線程必須全部停下來,才可以進行垃圾回收,這個狀態我們可以認為 JVM 是安全的(safe),整個堆的狀態是穩定的。

如果在 GC 前,有線程遲遲進入不了 safepoint,那么整個 JVM 都在等待這個阻塞的線程,造成了整體 GC 的時間變長。
* [ ] MinorGC、MajorGC、FullGC 都什么時候發生?
MinorGC 在年輕代空間不足的時候發生,MajorGC 指的是老年代的 GC,出現 MajorGC 一般經常伴有 MinorGC。
FullGC 有三種情況:第一,當老年代無法再分配內存的時候;第二,元空間不足的時候;第三,顯示調用 System.gc 的時候。另外,像 CMS 一類的垃圾回收器,在 MinorGC 出現 promotion failure 的時候也會發生 FullGC。
* [ ] 類加載有幾個過程?
加載、驗證、準備、解析、初始化。

* [ ] 什么情況下會發生棧溢出?
棧的大小可以通過 -Xss 參數進行設置,當遞歸層次太深的時候,則會發生棧溢出。
* [ ] 生產環境服務器變慢,請談談診斷思路和性能評估?
希望第 11 課時和第 16 課時中的一些思路,能夠祝你一臂之力。下圖是第 11 課時的一張影響因素的全景圖。

從各個層次分析代碼優化的手段,如下圖所示:

如果你應聘的是比較高級的職位,那么可以說一下第 23 課時中的最后總結部分。
#### 小結
本課時我們首先修正了一些表述錯誤的知識點;然后分析了一些常見的面試題,這些面試題的覆蓋率是非常有限的,因為很多細節都沒有觸及到,更多的面試題還需要你自行提取、整理,由于篇幅有限,這里不再重復。
到現在為止,我們的課程內容就結束了。本課程的特色主要體現在實踐方面,全部都是工作中的總結和思考;輔之以理論,給你一個在工作中,JVM 相關知識點的全貌。當然,有些課時的難度是比較高的,需要你真正的實際操作一下。
寫作的過程中難免會有遺漏的知識點,可通過拉勾客服人員加入本課程的讀者群一起討論;如果你覺得課程不錯,從中有所收獲的話,不要忘了推薦給身邊的朋友哦。前路漫漫,一起加油。
- 前言
- 開篇詞
- 基礎原理
- 第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 面試題補充