JVM 面試突破

本文將重點介紹面試過程中常見的 JVM 題目。

1.1 JDK、 JRE、JVM 的關係是什麼?

什麼是 JVM ?

英文名稱 ( Java Virtual Machine ),就是 JAVA 虛擬機, 它只識別 .class 類型文件,它能夠將 class 文件中的字節碼指令進行識別並調用操作系統向上的 API 完成動作。

什麼是 JRE ?

英文名稱( Java Runtime Environment ),Java 運行時環境。它主要包含兩個部分:JVM 的標準實現和 Java 的一些基本類庫。相對於 JVM 來說,JRE多出來一部分 Java 類庫。

什麼是 JDK? 英文名稱( Java Development Kit ),Java 開發工具包。JDK 是整個 Java 開發的核心,它集成了 JRE 和一些好用的小工具。例如:javac.exe、java.exe、jar.exe 等。

這三者的關係:一層層的嵌套關係。JDK > JRE > JVM。

1.2 JVM 的內存模型以及分區情況和作用

如下圖所示:


黃色部分為線程共有,藍色部分為線程私有。

方法區

用於存儲虛擬機加載的類信息,常量,靜態變量等數據。

存放對象實例,所有的對象和數組都要在堆上分配。 是 JVM 所管理的內存中最大的一塊區域。

Java 方法執行的內存模型:存儲局部變量表,操作數棧,動態鏈接,方法出口等信息。生命週期與線程相同。

本地方法棧

作用與虛擬機棧類似,不同點本地方法棧為 native 方法執行服務,虛擬機棧為虛擬機執行的 Java 方法服務。

程序計數器

當前線程所執行的行號指示器。是 JVM 內存區域最小的一塊區域。執行字節碼工作時就是利用程序計數器來選取下一條需要執行的字節碼指令。

1.3 JVM 對象創建步驟流程是什麼?

整體流程如下圖所示:


第 1 步:虛擬機遇到一個 new 指令,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用, 並且檢查這個符號引用的類是否已經被加載&解析&初始化。

第 2 步:如果類已經被加載那麼進行第 3 步; 如果沒有進行加載, 那麼就就需要先進行類的加載。

第 3 步:類加載檢查通過之後, 接下來進行新生對象的內存分配。

第 4 步:對象生成需要的內存大小在類加載完成後便可完全確定,為對象分配空間等同於把一塊確定大小的內存從 Java 堆中劃分出來

第 5 步:

內存大小的劃分分為兩種情況: 第一種情況:JVM 的內存是規整的, 所有的使用的內存都放到一邊, 空閒的內存在另外一邊, 中間放一個指針作為分界點的指示器。 那麼這時候分配內存就比較簡單, 只要講指針向空閒空間那邊挪動一段與對象大小相同的距離。 這種就是“指針碰撞”。

第二種情況:JVM 的內存不是規整的, 也就是說已使用的內存與未使用的內存相互交錯。 這時候就沒辦法利用指正碰撞了。 這時候我們就需要維護一張表,用於記錄那些內存可用, 在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例, 並更新到記錄表上。

第 6 步:空間申請完成之後, JVM 需要將內存的空間都初始化為 0 值。如果使用 TLAB, 就可以在 TLAB 分配的時候就可以進行該工作。

第 7 步: JVM 對對象進行必要的設置。 例如, 這個對象是哪個類的實例、對象的哈希碼、GC 年代等信息。

第 8 步:完成了上面的步驟之後 從 JVM 來看一個對象基本上完成了, 但從 Java 程序代碼絕對來看, 對象創建才剛剛開始, 需要執行 < init > 方法, 按照程序中設定的初始化操作初始化, 這時候一個真正的程序對象生成了。

1.4 垃圾回收算法有幾種類型? 他們對應的優缺點又是什麼?

常見的垃圾回收算法有:

標記-清除算法、複製算法、標記-整理算法、分代收集算法

標記-清除算法

標記—清除算法包括兩個階段:“標記”和“清除”。 標記階段:確定所有要回收的對象,並做標記。 清除階段:將標記階段確定不可用的對象清除。

缺點:

標記和清除的效率都不高。會產生大量的碎片,而導致頻繁的回收。

複製算法

內存分成大小相等的兩塊,每次使用其中一塊,當垃圾回收的時候, 把存活的對象複製到另一塊上,然後把這塊內存整個清理掉。

缺點:

需要浪費額外的內存作為複製區。當存活率較高時,複製算法效率會下降。

標記-整理算法

標記—整理算法不是把存活對象複製到另一塊內存,而是把存活對象往內存的一端移動,然後直接回收邊界以外的內存。

缺點: 算法複雜度大,執行步驟較多

分代收集算法

目前大部分 JVM 的垃圾收集器採用的算法。根據對象存活的生命週期將內存劃分為若干個不同的區域。一般情況下將堆區劃分為新生代( Young Generation 和老年代( Tenured Generation ),永久代( Permanet Generation )。

老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點採取最適合的收集算法。

如下圖所示:


Young:存放新創建的對象,對象生命週期非常短,幾乎用完可以立即回收,也叫 Eden 區。

Tenured: young 區多次回收後存活下來的對象將被移到 tenured 區,也叫 old 區。

Perm:永久帶,主要存加載的類信息,生命週期長,幾乎不會被回收。

缺點: 算法複雜度大,執行步驟較多。

1.5 簡單介紹一下什麼是類加載機制?

Class 文件由類裝載器裝載後,在 JVM 中將形成一份描述 Class 結構的元信息對象,通過該元信息對象可以獲知 Class 的結構信息:如構造函數,屬性和方法等。

虛擬機把描述類的數據從 class 文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型,這就是虛擬機的類加載機制。

1.6 類的加載過程是什麼?簡單描述一下每個步驟

類加載的過程包括了:

加載、驗證、準備、解析、初始化五個階段

第一步:加載

查找並加載類的二進制數據。

加載是類加載過程的第一個階段,虛擬機在這一階段需要完成以下三件事情:

通過類的全限定名來獲取其定義的二進制字節流將字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構在 Java 堆中生成一個代表這個類的 java.lang.Class 對象,作為對方法區中這些數據的訪問入口

第二步:驗證

確保被加載的類的正確性。

這一階段是確保 Class 文件的字節流中包含的信息符合當前虛擬機的規範,並且不會損害虛擬機自身的安全。包含了四個驗證動作:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。

第三步:準備

為類的靜態變量分配內存,並將其初始化為默認值。

準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。

第四步:解析

把類中的符號引用轉換為直接引用。

解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符 7 類符號引用進行。

第五步:初始化

類變量進行初始化

為類的靜態變量賦予正確的初始值,JVM 負責對類進行初始化,主要對類變量進行初始化。

1.7 JVM 預定義的類加載器有哪幾種?分別什麼作用?

啟動(Bootstrap)類加載器、標準擴展(Extension)類加載器、應用程序類加載器(Application)

啟動(Bootstrap)類加載器

引導類裝入器是用本地代碼實現的類裝入器,它負責將 < JavaRuntimeHome >/lib 下面的類庫加載到內存中。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用。

標準擴展(Extension)類加載器

擴展類加載器負責將 < Java_Runtime_Home >/lib/ext 或者由系統變量 java.ext.dir 指定位置中的類庫加載到內存中。開發者可以直接使用標準擴展類加載器。

應用程序類加載器(Application)

應用程序類加載器(Application ClassLoader):負責加載用戶路徑(classpath)上的類庫。

1.8 什麼是雙親委派模式?有什麼作用?

基本定義: 雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去加載這個類,而是把請求委託給父加載器去完成,依次向上,因此,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,只有當父加載器沒有找到所需的類時,子加載器才會嘗試去加載該類。

雙親委派機制:

當 AppClassLoader 加載一個 class 時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器 ExtClassLoader 去完成。當 ExtClassLoader 加載一個 class 時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給 BootStrapClassLoader 去完成。如果 BootStrapClassLoader 加載失敗,會使用 ExtClassLoader 來嘗試加載;若 ExtClassLoader 也加載失敗,則會使用 AppClassLoader 來加載,如果 AppClassLoader 也加載失敗,則會報出異常 ClassNotFoundException。

如下圖所示:


雙親委派作用:

通過帶有優先級的層級關可以避免類的重複加載;保證 Java 程序安全穩定運行,Java 核心 API 定義類型不會被隨意替換。

1.9 介紹一下 JVM 中垃圾收集器有哪些? 他們特點分別是什麼?

新生代垃圾收集器

Serial 收集器

特點: Serial 收集器只能使用一條線程進行垃圾收集工作,並且在進行垃圾收集的時候,所有的工作線程都需要停止工作,等待垃圾收集線程完成以後,其他線程才可以繼續工作。

使用算法:複製算法

ParNew 收集器

特點: ParNew 垃圾收集器是Serial收集器的多線程版本。為了利用 CPU 多核多線程的優勢,ParNew 收集器可以運行多個收集線程來進行垃圾收集工作。這樣可以提高垃圾收集過程的效率。

使用算法:複製算法

Parallel Scavenge 收集器

特點: Parallel Scavenge 收集器是一款多線程的垃圾收集器,但是它又和 ParNew 有很大的不同點。

Parallel Scavenge 收集器和其他收集器的關注點不同。其他收集器,比如 ParNew 和 CMS 這些收集器,它們主要關注的是如何縮短垃圾收集的時間。而 Parallel Scavenge 收集器關注的是如何控制系統運行的吞吐量。這裡說的吞吐量,指的是 CPU 用於運行應用程序的時間和 CPU 總時間的佔比,吞吐量 = 代碼運行時間 / (代碼運行時間 + 垃圾收集時間)。如果虛擬機運行的總的 CPU 時間是 100 分鐘,而用於執行垃圾收集的時間為 1 分鐘,那麼吞吐量就是 99%。

使用算法:複製算法

老年代垃圾收集器

Serial Old 收集器

特點: Serial Old 收集器是 Serial 收集器的老年代版本。這款收集器主要用於客戶端應用程序中作為老年代的垃圾收集器,也可以作為服務端應用程序的垃圾收集器。

使用算法:標記-整理

Parallel Old 收集器

特點: Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本這個收集器是在 JDK1.6 版本中出現的,所以在 JDK1.6 之前,新生代的 Parallel Scavenge 只能和 Serial Old 這款單線程的老年代收集器配合使用。Parallel Old 垃圾收集器和 Parallel Scavenge 收集器一樣,也是一款關注吞吐量的垃圾收集器,和 Parallel Scavenge 收集器一起配合,可以實現對 Java 堆內存的吞吐量優先的垃圾收集策略。

使用算法:標記-整理

CMS 收集器

特點: CMS 收集器是目前老年代收集器中比較優秀的垃圾收集器。CMS 是 Concurrent Mark Sweep,從名字可以看出,這是一款使用"標記-清除"算法的併發收集器。

CMS 垃圾收集器是一款以獲取最短停頓時間為目標的收集器。如下圖所示:


從圖中可以看出,CMS 收集器的工作過程可以分為 4 個階段:

初始標記(CMS initial mark)階段併發標記(CMS concurrent mark)階段重新標記(CMS remark)階段併發清除((CMS concurrent sweep)階段

使用算法:複製+標記清除

其他

G1 垃圾收集器

特點: 主要步驟:初始標記,併發標記,重新標記,複製清除。

使用算法:複製 + 標記整理

1.10 什麼是 Class 文件? Class 文件主要的信息結構有哪些?

Class 文件是一組以 8 位字節為基礎單位的二進制流。各個數據項嚴格按順序排列。

Class 文件格式採用一種類似於 C 語言結構體的偽結構來存儲數據。這樣的偽結構僅僅有兩種數據類型:無符號數和表。

無符號數:是基本數據類型。以 u1、u2、u4、u8 分別代表 1 個字節、2 個字節、4 個字節、8 個字節的無符號數,能夠用來描寫敘述數字、索引引用、數量值或者依照 UTF-8 編碼構成的字符串值。

表:由多個無符號數或者其它表作為數據項構成的複合數據類型。全部表都習慣性地以 _info結尾。

1.11 對象“對象已死” 是什麼概念?

對象不可能再被任何途徑使用,稱為對象已死。 判斷對象已死的方法有:引用計數法與可達性分析算法。

進階

2.1 Java 語言怎麼實現跨平臺的?

我們編寫的 Java 源碼,編譯後會生成一種 .class 文件,稱為字節碼文件。字節碼不能直接運行,必須通過 JVM 翻譯成機器碼才能運行。

JVM 是一個”橋樑“,是一個”中間件“,是實現跨平臺的關鍵。Java 代碼首先被編譯成字節碼文件,再由 JVM 將字節碼文件翻譯成機器語言,從而達到運行 Java 程序的目的。

2.2 JVM 數據運行區,哪些會造成 OOM 的情況?

除了數據運行區,其他區域均有可能造成 OOM 的情況。

堆溢出:java.lang.OutOfMemoryError: Java heap space
棧溢出:java.lang.StackOverflowError
永久代溢出:java.lang.OutOfMemoryError: PermGen space


2.3 詳細介紹一下對象在分帶內存區域的分配過程?

JVM 會試圖為相關 Java 對象在 Eden 中初始化一塊內存區域。當 Eden 空間足夠時,內存申請結束;否則到下一步。JVM 試圖釋放在 Eden 中所有不活躍的對象(這屬於 1 或更高級的垃圾回收)。釋放後若 Eden 空間仍然不足以放入新對象,則試圖將部分 Eden 中活躍對象放入 Survivor 區。Survivor 區被用來作為 Eden 及 Old 的中間交換區域,當 Old 區空間足夠時,Survivor 區的對象會被移到 Old 區,否則會被保留在 Survivor 區。當 Old 區空間不夠時,JVM 會在 Old 區進行完全的垃圾收集。完全垃圾收集後,若 Survivor 及 Old 區仍然無法存放從 Eden 複製過來的部分對象,導致 JVM 無法在 Eden 區為新對象創建內存區域,則出現 “ out of memory ” 錯誤。

1.4 G1 與 CMS 兩個垃圾收集器的對比

細節方面不同

G1 在壓縮空間方面有優勢。G1 通過將內存空間分成區域(Region)的方式避免內存碎片問題。Eden, Survivor, Old 區不再固定、在內存使用效率上來說更靈活。G1 可以通過設置預期停頓時間(Pause Time)來控制垃圾收集時間避免應用雪崩現象。G1 在回收內存後會馬上同時做合併空閒內存的工作、而 CMS 默認是在 STW(stop the world)的時候做。G1 會在 Young GC 中使用、而 CMS 只能在 O 區使用。

整體內容不同

吞吐量優先:G1
響應優先:CMS

CMS 的缺點是對 cpu 的要求比較高。G1 是將內存化成了多塊,所有對內段的大小有很大的要求。

CMS 是清除,所以會存在很多的內存碎片。G1 是整理,所以碎片空間較小。

2.5 線上常用的 JVM 參數有哪些?

數據區設置

Xms:初始堆大小Xmx:最大堆大小Xss:Java 每個線程的Stack大小XX:NewSize=n:設置年輕代大小XX:NewRatio=n:設置年輕代和年老代的比值。如:為 3,表示年輕代與年老代比值為 1:3,年輕代佔整個年輕代年老代和的 1/4。XX:SurvivorRatio=n:年輕代中 Eden 區與兩個 Survivor 區的比值。注意 Survivor 區有兩個。如:3,表示 Eden:Survivor=3:2,一個 Survivor 區佔整個年輕代的 1/5。XX:MaxPermSize=n:設置持久代大小。

收集器設置

XX:+UseSerialGC:設置串行收集器XX:+UseParallelGC::設置並行收集器XX:+UseParalledlOldGC:設置並行年老代收集器XX:+UseConcMarkSweepGC:設置併發收集器

GC日誌打印設置

XX:+PrintGC:打印 GC 的簡要信息XX:+PrintGCDetails:打印 GC 詳細信息XX:+PrintGCTimeStamps:輸出 GC 的時間戳

2.6 對象什麼時候進入老年代?

對象優先在 Eden 區分配內存

當對象首次創建時, 會放在新生代的 eden 區, 若沒有 GC 的介入,會一直在 eden 區,GC 後,是可能進入 survivor 區或者年老代

大對象直接進入老年代

所謂的大對象是指需要大量連續內存空間的 Java 對象,最典型的大對象就是那種很長的字符串以及數組,大對象對虛擬機的內存分配就是壞消息,尤其是一些朝生夕滅的短命大對象,寫程序時應避免。

長期存活的對象進入老年代

虛擬機給每個對象定義了一個對象年齡(Age)計數器,對象在 Survivor 區中每熬過一次 Minor GC,年齡就增加 1,當他的年齡增加到一定程度(默認是 15 歲), 就將會被晉升到老年代中。

2.7 什麼是內存溢出, 內存洩露? 他們的區別是什麼?

內存溢出 out of memory

,是指程序在申請內存時,沒有足夠的內存空間供其使用,出現 out of memory;

內存洩露 memory leak,是指程序在申請內存後,無法釋放已申請的內存空間,一次內存洩露危害可以忽略,但內存洩露堆積後果很嚴重,無論多少內存,遲早會被佔光。

內存溢出就是你要求分配的內存超出了系統能給你的,系統不能滿足需求,於是產生溢出。

內存洩漏是指你向系統申請分配內存進行使用(new),可是使用完了以後卻不歸還(delete),結果你申請到的那塊內存你自己也不能再訪問(也許你把它的地址給弄丟了),而系統也不能再次將它分配給需要的程序。

2.8 引起類加載操作的行為有哪些?

遇到 new、getstatic、putstatic 或 invokestatic 這四條字節碼指令。反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。子類初始化的時候,如果其父類還沒初始化,則需先觸發其父類的初始化。虛擬機執行主類的時候(有 main( string[] args))。JDK1.7 動態語言支持。

2.9 介紹一下 JVM 提供的常用工具

jps:
用來顯示本地的 Java 進程,可以查看本地運行著幾個 Java 程序,並顯示他們的進程號。 命令格式:jpsjinfo:運行環境參數:Java System 屬性和 JVM 命令行參數,Java class path 等信息。命令格式:jinfo 進程 pidjstat:監視虛擬機各種運行狀態信息的命令行工具。 命令格式:jstat -gc 123 250 20jstack:可以觀察到 JVM 中當前所有線程的運行情況和線程當前狀態。 命令格式:jstack 進程 pidjmap:觀察運行中的 JVM 物理內存的佔用情況(如:產生哪些對象,及其數量)。 命令格式:jmap [option] pid

2.10 Full GC 、 Major GC 、Minor GC 之間區別?

Minor GC: 從新生代空間(包括 Eden 和 Survivor 區域)回收內存被稱為 Minor GC。

Major GC: 清理 Tenured 區,用於回收老年代,出現 Major GC 通常會出現至少一次 Minor GC。

Full GC: Full GC 是針對整個新生代、老年代、元空間(metaspace,java8 以上版本取代 perm gen)的全局範圍的 GC。

2.11 什麼時候觸發 Full GC ?

調用 System.gc 時,系統建議執行 Full GC,但是不必然執行。老年代空間不足。方法區空間不足。通過 Minor GC 後進入老年代的平均大小大於老年代的可用內存。由 Eden 區、survivor space1(From Space)區向 survivor space2(To Space)區複製時,對象大小大於 To Space 可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小。

2.12 什麼情況下會出現棧溢出

方法創建了一個很大的對象,如 List,Array。是否產生了循環調用、死循環。是否引用了較大的全局變量。

2.13 說一下強引用、軟引用、弱引用、虛引用以及他們之間和 gc 的關係

強引用:new 出的對象之類的引用,只要強引用還在,永遠不會回收。軟引用:引用但非必須的對象,內存溢出異常之前,回收。弱引用:非必須的對象,對象能生存到下一次垃圾收集發生之前。虛引用:對生存時間無影響,在垃圾回收時得到通知。

2.14 Eden 和 Survivor 的比例分配是什麼情況?為什麼?

默認比例 8:1。 大部分對象都是朝生夕死。 複製算法的基本思想就是將內存分為兩塊,每次只用其中一塊,當這一塊內存用完,就將還活著的對象複製到另外一塊上面。複製算法不會產生內存碎片。

實戰

3.1 CPU 資源佔用過高

top 查看當前 CPU 情況,找到佔用 CPU 過高的進程 PID=123。top -H -p123 找出兩個 CPU 佔用較高的線程,記錄下來 PID=2345, 3456 轉換為十六進制。jstack -l 123 > temp.txt 打印出當前進程的線程棧。查找到對應於第二步的兩個線程運行棧,分析代碼。

3.2 OOM 異常排查

使用 top 指令查詢服務器系統狀態。ps -aux|grep java 找出當前 Java 進程的 PID。jstat -gcutil pid interval 查看當前 GC 的狀態。jmap -histo:live pid 可用統計存活對象的分佈情況,從高到低查看佔據內存最多的對象。jmap -dump:format=b,file= 文件名 [pid] 利用 Jmap dump。使用性能分析工具對上一步 dump 出來的文件進行分析,工具有 MAT 等。

總結

上面介紹了 JVM 常見的面試題目,希望對大家接下里的面試或者對於 JVM 的深入學習有所幫助。