原文地址 :?26 福利:常見 JVM 面試題補充 (lianglianglee.com)
CMS 是老年代垃圾回收器?
初步印象是,但實際上不是。根據 CMS 的各個收集過程,它其實是一個涉及年輕代和老年代的綜合性垃圾回收器。在很多文章和書籍的劃分中,都將 CMS 劃分為了老年代垃圾回收器,加上它主要作用于老年代,所以一般誤認為是。
常量池問題
常量池的表述有些模糊,在此細化一下,注意我們指的是 Java 7 版本之后。
JVM 中有多個常量池:
- 字符串常量池,存放在堆上,也就是執行 intern 方法后存的地方,class 文件的靜態常量池,如果是字符串,則也會被裝到字符串常量池中。
- 運行時常量池,存放在方法區,屬于元空間,是類加載后的一些存儲區域,大多數是類中 constant_pool 的內容。
- 類文件常量池,也就是 constant_pool,這個是概念性的,并沒有什么實際存儲區域。
在平常的交流過程中,聊的最多的是字符串常量池,具體可參考官網。
永久代
常見面試題
JVM 有哪些內存區域?(JVM 的內存布局是什么?)
JVM 包含堆、元空間、Java 虛擬機棧、本地方法棧、程序計數器等內存區域,其中,堆是占用內存最大的一塊,如下圖所示。
Java 的內存模型是什么?(JMM 是什么?)
JVM 試圖定義一種統一的內存模型,能將各種底層硬件以及操作系統的內存訪問差異進行封裝,使 Java 程序在不同硬件以及操作系統上都能達到相同的并發效果。它分為工作內存和主內存,線程無法對主存儲器直接進行操作,如果一個線程要和另外一個線程通信,那么只能通過主存進行交換,如下圖所示。
JVM 垃圾回收時如何確定垃圾?什么是 GC Roots?
JVM 采用的是可達性分析算法。JVM 是通過 GC Roots 來判定對象存活的,從 GC Roots 向下追溯、搜索,會產生一個叫做 Reference Chain 的鏈條。當一個對象不能和任何一個 GC Root 產生關系時,就判定為垃圾,如下圖所示。
GC Roots 包括:
- Java 線程中,當前所有正在被調用的方法的引用類型參數、局部變量、臨時值等。也就是與我們棧幀相關的各種引用。
- 所有當前被加載的 Java 類。
- Java 類的引用類型靜態變量。
- 運行時常量池里的引用類型常量(String 或 Class 類型)。
- JVM 內部數據結構的一些引用,比如 sun.jvm.hotspot.memory.Universe 類。
- 用于同步的監控對象,比如調用了對象的 wait() 方法。
- JNI handles,包括 global handles 和 local handles。
這些 GC Roots 大體可以分為三大類,下面這種說法更加好記一些:
- 活動線程相關的各種引用。
- 類的靜態變量的引用。
- JNI 引用。
能夠找到 Reference Chain 的對象,就一定會存活么?
不一定,還要看 Reference 類型,弱引用在 GC 時會被回收,軟引用在內存不足的時候會被回收,但如果沒有 Reference Chain 對象時,就一定會被回收。
強引用、軟引用、弱引用、虛引用是什么?
普通的對象引用關系就是強引用。
軟引用用于維護一些可有可無的對象。只有在內存不足時,系統則會回收軟引用對象,如果回收了軟引用對象之后仍然沒有足夠的內存,才會拋出內存溢出異常。
弱引用對象相比軟引用來說,要更加無用一些,它擁有更短的生命周期,當 JVM 進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。
虛引用是一種形同虛設的引用,在現實場景中用的不是很多,它主要用來跟蹤對象被垃圾回收的活動。
你說你做過 JVM 參數調優和參數配置,請問如何查看 JVM 系統默認值
使用 -XX:+PrintFlagsFinal 參數可以看到參數的默認值,這個默認值還和垃圾回收器有關,比如 UseAdaptiveSizePolicy。
你平時工作中用過的 JVM 常用基本配置參數有哪些?
主要有 Xmx、Xms、Xmn、MetaspaceSize 等。
請你談談對 OOM 的認識
OOM 是非常嚴重的問題,除了程序計數器,其他內存區域都有溢出的風險。和我們平常工作最密切的,就是堆溢出,另外,元空間在加載的類非常多的情況下也會溢出,還有就是棧溢出,這個通常影響比較小。堆外也有溢出的可能,這個就比較難排查了。
你都有哪些手段用來排查內存溢出?
這個話題很大,可以從實踐環節中隨便摘一個進行總結,下面舉一個最普通的例子。
內存溢出包含很多種情況,我在平常工作中遇到最多的就是堆溢出。有一次線上遇到故障,重新啟動后,使用 jstat 命令,發現 Old 區一直在增長。我使用 jmap 命令,導出了一份線上堆棧,然后使用 MAT 進行分析,通過對 GC Roots 的分析,發現了一個非常大的 HashMap 對象,這個原本是其他同事做緩存用的,但是一個無界緩存,造成了堆內存占用一直上升,后來,將這個緩存改成 guava 的 Cache,并設置了弱引用,故障就消失了。
GC 垃圾回收算法與垃圾收集器的關系?
常用的垃圾回收算法有標記清除、標記整理、復制算法等,引用計數器也算是一種,但垃圾回收器不使用這種算法,因為有循環依賴的問題。
很多垃圾回收器都是分代回收的:
- 對于年輕代,主要有 Serial、ParNew 等垃圾回收器,回收過程主要使用復制算法;
- 老年代的回收算法有 Serial、CMS 等,主要使用標記清除、標記整理算法等。
我們線上使用較多的是 G1,也有年輕代和老年代的概念,不過它是一個整堆回收器,它的回收對象是小堆區 。
怎么查看服務器默認的垃圾回收器是哪一個?
這通常會使用另外一個參數,即 -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 已經是最上層了,它直接獲取了
簡單描述一下(分代)垃圾回收的過程
分代回收器有兩個分區:老生代和新生代,新生代默認的空間占總空間的 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 資源,在應用運行的同時進行堆掃描;
- 停頓時間是不可預期的。
什么情況會造成元空間溢出?
元空間默認是沒有上限的,不加限制比較危險。當應用中的 Java 類過多時,比如 Spring 等一些使用動態代理的框架生成了很多類,如果占用空間超出了我們的設定值,就會發生元空間溢出。
什么時候會造成堆外內存溢出?
使用了 Unsafe 類申請內存,或者使用了 JNI 對內存進行操作,這部分內存是不受 JVM 控制的,不加限制使用的話,會很容易發生內存溢出。
SWAP 會影響性能么?
當操作系統內存不足時,會將部分數據寫入到 SWAP ,但是 SWAP 的性能是比較低的。如果應用的訪問量較大,需要頻繁申請和銷毀內存,那么很容易發生卡頓。一般在高并發場景下,會禁用 SWAP。
有什么堆外內存的排查思路?
進程占用的內存,可以使用 top 命令,看 RES 段占用的值,如果這個值大大超出我們設定的最大堆內存,則證明堆外內存占用了很大的區域。 使用 gdb 命令可以將物理內存 dump 下來,通常能看到里面的內容。更加復雜的分析可以使用 Perf 工具,或者谷歌開源的 GPerftools。那些申請內存最多的 native 函數,就很容易找到。
HashMap 中的 key,可以是普通對象么?有什么需要注意的地方?
Map 的 key 和 value 可以是任何類型,但要注意的是,一定要重寫它的 equals 和 hashCode 方法,否則容易發生內存泄漏。
怎么看死鎖的線程?
通過 jstack 命令,可以獲得線程的棧信息,死鎖信息會在非常明顯的位置(一般是最后)進行提示。
MinorGC、MajorGC、FullGC 都什么時候發生?
MinorGC 在年輕代空間不足的時候發生,MajorGC 指的是老年代的 GC,出現 MajorGC 一般經常伴有 MinorGC。
FullGC 有三種情況:第一,當老年代無法再分配內存的時候;第二,元空間不足的時候;第三,顯示調用 System.gc 的時候。另外,像 CMS 一類的垃圾回收器,在 MinorGC 出現 promotion failure 的時候也會發生 FullGC。
類加載有幾個過程?
加載、驗證、準備、解析、初始化。
什么情況下會發生棧溢出?
棧的大小可以通過 -Xss 參數進行設置,當遞歸層次太深的時候,則會發生棧溢出。