2萬字JVM面試題含答案解析(2020年78道JVM面試題總結)

前言

關於JVM系列面試知識點總結了一個思維導圖,分享給大家

1、java中會存在內存泄漏嗎,請簡單描述。

會。自己實現堆載的數據結構時有可能會出現內存泄露。

2、64 位 JVM 中,int 的長度是多數?

Java 中,int 類型變量的長度是一個固定值,與平臺無關,都是 32 位。意思就是說,在 32 位 和 64 位 的 Java 虛擬機中,int 類型的長度是相同的。

3、Serial 與 Parallel GC 之間的不同之處?

Serial 與 Parallel 在 GC 執行的時候都會引起 stop-the-world。它們之間主要不同 serial 收集器是默認的複製收集器,執行 GC 的時候只有一個線程,而parallel 收集器使用多個 GC 線程來執行。

4、32 位和 64 位的 JVM,int 類型變量的長度是多數?

32 位和 64 位的 JVM 中,int 類型變量的長度是相同的,都是 32 位或者 4個字節。

5、Java 中 WeakReference 與 SoftReference 的區別?

雖然 WeakReference 與 SoftReference 都有利於提高 GC 和 內存的效率,但是 WeakReference ,一旦失去最後一個強引用,就會被 GC回收,而軟引用雖然不能阻止被回收,但是可以延遲到 JVM 內存不足的時候。

6、JVM 選項 -XX:+UseCompressedOops 有什麼作用?爲什麼要使用

當你將你的應用從 32 位的 JVM 遷移到 64 位的 JVM 時,由於對象的指針從32 位增加到了 64 位,因此堆內存會突然增加,差不多要翻倍。這也會對 CPU緩存(容量比內存小很多)的數據產生不利的影響。因爲,遷移到 64 位的 JVM主要動機在於可以指定最大堆大小,通過壓縮OOP 可以節省一定的內存。通過-XX:+UseCompressedOops 選項,JVM 會使用 32 位的 OOP,而不是 64 位的 OOP。

7、怎樣通過 Java 程序來判斷 JVM 是 32 位 還是 64位?

你可以檢查某些系統屬性如 sun.arch.data.model 或 os.arch 來獲取該信息。

8、32 位 JVM 和 64 位 JVM 的最大堆內存分別是多數?

理論上說上 32 位的 JVM 堆內存可以到達 2^32,即 4GB,但實際上會比這個小很多。不同操作系統之間不同,如 Windows 系統大約 1.5GB,Solaris 大約3GB。64 位 JVM 允許指定最大的堆內存,理論上可以達到 2^64,這是一個非常大的數字,實際上你可以指定堆內存大小到 100GB。甚至有的 JVM,如 Azul,堆內存到 1000G 都是可能的。

9、JRE、JDK、JVM 及 JIT 之間有什麼不同?

JRE 代表 Java 運行時(Java run-time),是運行 Java 引用所必須的。JDK 代表 Java 開發工具(Java development kit),是 Java 程序打開發工具,如 Java編譯器,它也包含 JRE。JVM 代表 Java 虛擬機(Java virtual machine),它的責任是運行 Java 應用。JIT 代表即時編譯(Just In Time compilation),當代碼執行的次數超過一定的閾值時,會將 Java 字節碼轉換爲本地代碼,如,主要的熱點代碼會被準換爲本地代碼,這樣有利大幅度提高 Java 應用的性能。

10、解釋 Java 堆空間及 GC?

當通過 Java 命令啓動 Java 進程的時候,會爲它分配內存。內存的一部分用於創建堆空間,當程序中創建對象的時候,就從對空間中分配內存。GC 是 JVM 內部的一個進程,回收無效對象的內存用於將來的分配。

11、JVM 內存區域

JVM 內存區域主要分爲線程私有區域【程序計數器、虛擬機棧、本地方法區】、線程共享區域【JAVA 堆、方法區】、直接內存。

線程私有數據區域生命週期與線程相同, 依賴用戶線程的啓動/結束 而 創建/銷燬(在 Hotspot VM 內, 每個線程都與操作系統的本地線程直接映射, 因此這部分內存區域的存/否跟隨本地線程的生/死對應)。

線程共享區域隨虛擬機的啓動/關閉而創建/銷燬。

直接內存並不是 JVM 運行時數據區的一部分, 但也會被頻繁的使用: 在 JDK 1.4 引入的 NIO 提供了基於Channel與 Buffer的IO方式, 它可以使用Native函數庫直接分配堆外內存, 然後使用DirectByteBuffer 對象作爲這塊內存的引用進行操作(詳見: Java I/O 擴展), 這樣就避免了在 Java堆和 Native 堆中來回複製數據, 因此在一些場景中可以顯著提高性能。

12、程序計數器(線程私有)

一塊較小的內存空間, 是當前線程所執行的字節碼的行號指示器,每條線程都要有一個獨立的程序計數器,這類內存也稱爲“線程私有” 的內存。

正在執行 java 方法的話,計數器記錄的是虛擬機字節碼指令的地址(當前指令的地址) 。如果還是 Native 方法,則爲空。

這個內存區域是唯一一個在虛擬機中沒有規定任何 OutOfMemoryError 情況的區域。

13、虛擬機棧(線程私有)

是描述java方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。 每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

棧幀( Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接(Dynamic Linking)、 方法返回值和異常分派(Dispatch Exception)。 棧幀隨着方法調用而創建,隨着方法結束而銷燬——無論方法是正常完成還是異常完成(拋出了在方法內未被捕獲的異常)都算作方法結束。

14、本地方法區(線程私有)

本地方法區和 Java Stack 作用類似, 區別是虛擬機棧爲執行 Java 方法服務, 而本地方法棧則爲Native 方法服務, 如果一個 VM 實現使用 C-linkage 模型來支持 Native 調用, 那麼該棧將會是一個C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機棧合二爲一 。

15、你能保證 GC 執行嗎?

不能,雖然你可以調用 System.gc() 或者 Runtime.gc(),但是沒有辦法保證 GC的執行。

16、怎麼獲取 Java 程序使用的內存?堆使用的百分比?

可以通過 java.lang.Runtime 類中與內存相關方法來獲取剩餘的內存,總內存及最大堆內存。通過這些方法你也可以獲取到堆使用的百分比及堆內存的剩餘空間。Runtime.freeMemory() 方法返回剩餘空間的字節數,Runtime.totalMemory()方法總內存的字節數,Runtime.maxMemory() 返回最大內存的字節數。

17、Java 中堆和棧有什麼區別?

JVM 中堆和棧屬於不同的內存區域,使用目的也不同。棧常用於保存方法幀和局部變量,而對象總是在堆上分配。棧通常都比堆小,也不會在多個線程之間共享,而堆被整個 JVM 的所有線程共享。

18、描述一下 JVM 加載 class 文件的原理機制

JVM 中類的裝載是由類加載器(ClassLoader)和它的子類來實現的,Java 中各類加載器是一個重要的 Java 運行時系統組件,它負責在運行時查找和裝入類文件中的類。

由於 Java 的跨平臺性,經過編譯的 Java 源程序並不是一個可執行程序,而是一個或多個類文件。當 Java 程序需要使用某個類時,JVM 會確保這個類已經被加載、連接(驗證、準備和解析)和初始化。類的加載是指把類的.class 文件中的數據讀入到內存中,通常是創建一個字節數組讀入.class 文件,然後產生與所加載類對應的 Class 對象。

加載完成後,Class 對象還不完整,所以此時的類還不可用。當類被加載後就進入連接階段,這一階段包括驗證、準備(爲靜態變量分配內存並設置默認的初始值)和解析(將符號引用替換爲直接引用)三個步驟。最後 JVM 對類進行初始化,包括:1)如果類存在直接的父類並且這個類還沒有被初始化,那麼就先初始化父類;2)如果類中存在初始化語句,就依次執行這些初始化語句。

類的加載是由類加載器完成的,類加載器包括:根加載器(BootStrap)、擴展加載器(Extension)、系統加載器(System)和用戶自定義類加載器(java.lang.ClassLoader 的子類)。

從 Java 2(JDK 1.2)開始,類加載過程採取了父親委託機制(PDM)。PDM 更好的保證了 Java 平臺的安全性,在該機制中,JVM 自帶的Bootstrap 是根加載器,其他的加載器都有且僅有一個父類加載器。類的加載首先請求父類加載器加載,父類加載器無能爲力時才由其子類加載器自行加載。JVM 不會向 Java 程序提供對 Bootstrap 的引用。下面是關於幾個類

加載器的說明:

(1)Bootstrap:一般用本地代碼實現,負責加載 JVM 基礎核心類庫(rt.jar);

(2)Extension:從 java.ext.dirs 系統屬性所指定的目錄中加載類庫,它的父加載器是 Bootstrap;

(3)System:又叫應用類加載器,其父類是 Extension。它是應用最廣泛的類加載器。它從環境變量 classpath 或者系統屬性

java.class.path 所指定的目錄中記載類,是用戶自定義加載器的默認父加載器。

19、GC 是什麼?爲什麼要有 GC?

GC 是垃 圾收 集的 意思 ,內存 處理 是編 程人 員容 易出 現問 題的 地方 ,忘記 或者 錯誤的內 存回 收會 導致 程序 或系 統的 不穩 定甚 至崩 潰, Java 提供 的 GC 功能 可以 自動監測 對象 是否 超過 作用 域從 而達 到自 動回 收內 存的 目的 ,Java 語言 沒有 提供 釋放已分配內存的 顯示 操作 方法 。Java 程序 員不 用擔 心內 存管 理, 因爲 垃圾 收集 器會自動 進行 管理 。要 請求 垃圾 收集 ,可 以調 用下 面的 方法 之一 :System.gc() 或Runtime.getRuntime().gc() ,但 JVM 可以 屏蔽 掉線 示的 垃圾 回收 調用 。

垃圾回收可以有效的防止內存泄露,有效的使用可以使用的內存。垃圾回收器通常是作爲一個單獨的低優先級的線程運行,不可預知的情況下對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收,程序員不能實時的調用垃圾回收器對某個對象或所有對象進行垃圾回收。在 Java 誕生初期,垃圾回收是 Java最大的亮點之一,因爲服務器端的編程需要有效的防止內存泄露問題,然而時過境遷,如今 Java 的垃圾回收機制已經成爲被詬病的東。移動智能終端用戶通常覺得 iOS 的系統比 Android 系統有更好的用戶體驗,其中一個深層次的原因就在於 Android 系統中垃圾回收的不可預知性。

20、堆(Heap-線程共享) -運行時數據區

是被線程共享的一塊內存區域, 創建的對象和數組都保存在 Java 堆內存中,也是垃圾收集器進行垃圾收集的最重要的內存區域。 由於現代VM 採用分代收集算法, 因此 Java 堆從 GC 的角度還可以細分爲: 新生代(Eden 區、 From Survivor 區和 To Survivor 區)和老年代。

21、方法區/永久代(線程共享)

即我們常說的永久代(Permanent Generation), 用於存儲被 JVM 加載的類信息、常量、靜態變量即、時編譯器編譯後的代碼等數據.HotSpot VM把GC分代收集擴展至方法區, 即使用Java堆的永久代來實現方法區, 這樣 HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分內存,而不必爲方法區開發專門的內存管理器(永久帶的內存回收的主要目標是針對常量池的回收和類型的卸載, 因此收益一般很小) 。

運行時常量池(Runtime Constant Pool)是方法區的一部分。 Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池 (Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。 Java 虛擬機對 Class 文件的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用於存儲哪種數據都必須符合規範上的要求,這樣纔會被虛擬機認可、裝載和執行。

22、JVM 運行時內存

Java 堆從 GC 的角度還可以細分爲: 新生代(Eden 區、 From Survivor 區和 To Survivor 區)和老年代。

23、新生代

是用來存放新生的對象。一般佔據堆的 1/3 空間。由於頻繁創建對象,所以新生代會頻繁觸發MinorGC 進行垃圾回收。新生代又分爲 Eden區、 ServivorFrom、 ServivorTo 三個區。

Eden 區

Java 新對象的出生地(如果新創建的對象佔用內存很大,則直接分配到老年代)。當 Eden 區內存不夠的時候就會觸發 MinorGC,對新生代區進行一次垃圾回收。

ServivorFrom

上一次 GC 的倖存者,作爲這一次 GC 的被掃描者。

ServivorTo

保留了一次 MinorGC 過程中的倖存者。

MinorGC 的過程(複製->清空->互換)

MinorGC 採用複製算法。

(1) eden、 servicorFrom 複製到 ServicorTo,年齡+1

首先,把 Eden 和 ServivorFrom 區域中存活的對象複製到 ServicorTo 區域(如果有對象的年齡以及達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(如果 ServicorTo 不夠位置了就放到老年區);

(2)清空 eden、 servicorFrom

然後,清空 Eden 和 ServicorFrom 中的對象;

(3) ServicorTo 和 ServicorFrom 互換

最後, ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成爲下一次 GC 時的 ServicorFrom區。

24、老年代

主要存放應用程序中生命週期長的內存對象。

老年代的對象比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行了一次MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。

MajorGC 採用標記清除算法:首先掃描一次所有老年代,標記出存活的對象,然後回收沒有標記的對象。 ajorGC 的耗時比較長,因爲要掃描再回收。 MajorGC 會產生內存碎片,爲了減少內存損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)異常。

25、永久代

指內存的永久保存區域,主要存放 Class 和 Meta(元數據)的信息,Class 在被加載的時候被放入永久區域, 它和和存放實例的區域不同,GC不會在主程序運行期對永久區域進行清理。所以這也導致了永久代的區域會隨着加載的 Class 的增多而脹滿,最終拋出 OOM 異常。

26、JAVA8 與元數據

在 Java8 中, 永久代已經被移除,被一個稱爲“元數據區”(元空間)的區域所取代。元空間的本質和永久代類似,元空間與永久代之間最大的區別在於: 元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。 類的元數據放入nativememory, 字符串池和類的靜態變量放入 java 堆中, 這樣可以加載多少類的元數據就不再由MaxPermSize 控制, 而由系統的實際可用空間來控制。

27、引用計數法

在 Java 中,引用和對象是有關聯的。如果要操作對象則必須用引用進行。因此,很顯然一個簡單的辦法是通過引用計數來判斷一個對象是否可以回收。簡單說,即一個對象如果沒有任何與之關聯的引用, 即他們的引用計數都不爲 0, 則說明對象不太可能再被用到,那麼這個對象就是可回收對象。

28、可達性分析

爲了解決引用計數法的循環引用問題, Java 使用了可達性分析的方法。通過一系列的“GC roots”對象作爲起點搜索。如果在“GC roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的。要注意的是,不可達對象不等價於可回收對象, 不可達對象變爲可回收對象至少要經過兩次標記過程。兩次標記後仍然是可回收對象,則將面臨回收。

29、標記清除算法( Mark-Sweep)

最基礎的垃圾回收算法,分爲兩個階段,標註和清除。標記階段標記出所有需要回收的對象,清除階段回收被標記的對象所佔用的空間。如圖

從圖中我們就可以發現,該算法最大的問題是內存碎片化嚴重,後續可能發生大對象不能找到可利用空間的問題。

30、複製算法(copying)

爲了解決 Mark-Sweep 算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分爲等大小的兩塊。每次只使用其中一塊,當這一塊內存滿後將尚存活的對象複製到另一塊上去,把已使用的內存清掉,如圖:

這種算法雖然實現簡單,內存效率高,不易產生碎片,但是最大的問題是可用內存被壓縮到了原本的一半。且存活對象增多的話, Copying算法的效率會大大降低。

31、標記整理算法(Mark-Compact)

結合了以上兩個算法,爲了避免缺陷而提出。標記階段和 Mark-Sweep 算法相同, 標記後不是清理對象,而是將存活對象移向內存的一端。然後清除端邊界外的對象。如圖:

32、分代收集算法

分代收集法是目前大部分 JVM 所採用的方法,其核心思想是根據對象存活的不同生命週期將內存劃分爲不同的域,一般情況下將 GC 堆劃分爲老生代(Tenured/Old Generation)和新生代(YoungGeneration)。老生代的特點是每次垃圾回收時只有少量對象需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的算法。

33、新生代與複製算法

目前大部分 JVM 的 GC 對於新生代都採取 Copying 算法,因爲新生代中每次垃圾回收都要回收大部分對象,即要複製的操作比較少,但通常並不是按照 1: 1 來劃分新生代。一般將新生代劃分爲一塊較大的 Eden 空間和兩個較小的 Survivor 空間(From Space, To Space),每次使用Eden 空間和其中的一塊 Survivor 空間,當進行回收時,將該兩塊空間中還存活的對象複製到另一塊 Survivor 空間中。

34、老年代與標記複製算法

而老年代因爲每次只回收少量對象,因而採用 Mark-Compact 算法。

(1)JAVA 虛擬機提到過的處於方法區的永生代(Permanet Generation), 它用來存儲 class 類,常量,方法描述等。對永生代的回收主要包括廢棄常量和無用的類。

(2)對象的內存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放對象的那一塊),少數情況會直接分配到老生代。

(3)當新生代的 Eden Space 和 From Space 空間不足時就會發生一次 GC,進行 GC 後, EdenSpace 和 From Space 區的存活對象會被挪到 To Space,然後將 Eden Space 和 FromSpace 進行清理。

(4)如果 To Space 無法足夠存儲某個對象,則將這個對象存儲到老生代。

(5)在進行 GC 後,使用的便是 Eden Space 和 To Space 了,如此反覆循環。

(6)當對象在 Survivor 去躲過一次 GC 後,其年齡就會+1。 默認情況下年齡到達 15 的對象會被移到老生代中。

35、JAVA 強引用

在 Java 中最常見的就是強引用, 把一個對象賦給一個引用變量,這個引用變量就是一個強引用。當一個對象被強引用變量引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即使該對象以後永遠都不會被用到 JVM 也不會回收。因此強引用是造成 Java 內存泄漏的主要原因之一。

36、JAVA軟引用

軟引用需要用 SoftReference 類來實現,對於只有軟引用的對象來說,當系統內存足夠時它不會被回收,當系統內存空間不足時它會被回收。軟引用通常用在對內存敏感的程序中。

37、JAVA弱引用

弱引用需要用 WeakReference 類來實現,它比軟引用的生存期更短,對於只有弱引用的對象來說,只要垃圾回收機制一運行,不管 JVM 的內存空間是否足夠,總會回收該對象佔用的內存。

38、JAVA虛引用

虛引用需要 PhantomReference 類來實現,它不能單獨使用,必須和引用隊列聯合使用。 虛引用的主要作用是跟蹤對象被垃圾回收的狀態。

39、分代收集算法

當前主流 VM 垃圾收集都採用”分代收集” (Generational Collection)算法, 這種算法會根據對象存活週期的不同將內存劃分爲幾塊, 如 JVM 中的新生代、老年代、永久代, 這樣就可以根據各年代特點分別採用最適當的 GC 算法

40、在新生代-複製算法

每次垃圾收集都能發現大批對象已死, 只有少量存活. 因此選用複製算法, 只需要付出少量存活對象的複製成本就可以完成收集

41、在老年代-標記整理算法

因爲對象存活率高、沒有額外空間對它進行分配擔保, 就必須採用“標記—清理”或“標記—整理” 算法來進行回收, 不必進行內存複製, 且直接騰出空閒內存。

42、分區收集算法

分區算法則將整個堆空間劃分爲連續的不同小區間, 每個小區間獨立使用, 獨立回收. 這樣做的好處是可以控制一次回收多少個小區間 , 根據目標停頓時間, 每次合理地回收若干個小區間(而不是整個堆), 從而減少一次 GC 所產生的停頓。

43、GC 垃圾收集器

Java 堆內存被劃分爲新生代和年老代兩部分,新生代主要使用複製和標記-清除垃圾回收算法;年老代主要使用標記-整理垃圾回收算法,因此 java 虛擬中針對新生代和年老代分別提供了多種不同的垃圾收集器, JDK1.6 中 Sun HotSpot 虛擬機的垃圾收集器如下:

44、Serial 垃圾收集器(單線程、 複製算法)

Serial(英文連續) 是最基本垃圾收集器,使用複製算法,曾經是JDK1.3.1 之前新生代唯一的垃圾收集器。 Serial 是一個單線程的收集器,它不但只會使用一個 CPU 或一條線程去完成垃圾收集工作,並且在進行垃圾收集的同時,必須暫停其他所有的工作線程,直到垃圾收集結束。

Serial 垃圾收集器雖然在收集垃圾過程中需要暫停所有其他的工作線程,但是它簡單高效,對於限定單個 CPU 環境來說,沒有線程交互的開銷,可以獲得最高的單線程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虛擬機運行在 Client 模式下默認的新生代垃圾收集器。

45、ParNew 垃圾收集器(Serial+多線程)

ParNew 垃圾收集器其實是 Serial 收集器的多線程版本,也使用複製算法,除了使用多線程進行垃圾收集之外,其餘的行爲和 Serial 收集器完全一樣, ParNew 垃圾收集器在垃圾收集過程中同樣也要暫停所有其他的工作線程。

ParNew 收集器默認開啓和 CPU 數目相同的線程數,可以通過-XX:ParallelGCThreads 參數來限制垃圾收集器的線程數。 【Parallel:平行的】

ParNew 雖然是除了多線程外和Serial 收集器幾乎完全一樣,但是ParNew垃圾收集器是很多 java虛擬機運行在 Server 模式下新生代的默認垃圾收集器。

46、Parallel Scavenge 收集器(多線程複製算法、高效)

Parallel Scavenge 收集器也是一個新生代垃圾收集器,同樣使用複製算法,也是一個多線程的垃圾收集器, 它重點關注的是程序達到一個可控制的吞吐量(Thoughput, CPU 用於運行用戶代碼的時間/CPU 總消耗時間,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)),高吞吐量可以最高效率地利用 CPU 時間,儘快地完成程序的運算任務,主要適用於在後臺運算而不需要太多交互的任務。 自適應調節策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個重要區別。

47、Serial Old 收集器(單線程標記整理算法 )

Serial Old 是 Serial 垃圾收集器年老代版本,它同樣是個單線程的收集器,使用標記-整理算法,這個收集器也主要是運行在 Client 默認的

java 虛擬機默認的年老代垃圾收集器。在 Server 模式下,主要有兩個用途:

(1)在 JDK1.5 之前版本中與新生代的 Parallel Scavenge 收集器搭配使用。

(2)作爲年老代中使用 CMS 收集器的後備垃圾收集方案。新生代 Serial 與年老代 Serial Old 搭配垃圾收集過程圖:

新生代 Parallel Scavenge 收集器與 ParNew 收集器工作原理類似,都是多線程的收集器,都使用的是複製算法,在垃圾收集過程中都需要暫停所有的工作線程。新生代 ParallelScavenge/ParNew 與年老代 Serial Old 搭配垃圾收集過程圖:

48、Parallel Old 收集器(多線程標記整理算法)

Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多線程的標記-整理算法,在 JDK1.6纔開始提供。

在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保證新生代的吞吐量優先,無法保證整體的吞吐量, Parallel Old 正是爲了在年老代同樣提供吞吐量優先的垃圾收集器, 如果系統對吞吐量要求比較高,可以優先考慮新生代Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。

新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配運行過程圖

49、CMS 收集器(多線程標記清除算法)

Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾回收停頓時間, 和其他年老代使用標記-整理算法不同,它使用多線程的標記-清除算法。最短的垃圾收集停頓時間可以爲交互比較高的程序提高用戶體驗。CMS 工作機制相比其他的垃圾收集器來說更復雜。整個過程分爲以下 4 個階段:

初始標記

只是標記一下 GC Roots 能直接關聯的對象,速度很快,仍然需要暫停所有的工作線程。

併發標記

進行 GC Roots 跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。

重新標記

爲了修正在併發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,仍然需要暫停所有的工作線程。

併發清除

清除 GC Roots 不可達對象,和用戶線程一起工作,不需要暫停工作線程。由於耗時最長的併發標記和併發清除過程中,垃圾收集線程可以和用戶現在一起併發工作, 所以總體上來看CMS 收集器的內存回收和用戶線程是一起併發地執行。CMS 收集器工作過程

50、G1 收集器

Garbage first 垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與 CMS 收集器, G1 收集器兩個最突出的改進是:

(1)基於標記-整理算法,不產生內存碎片。

(2)可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。G1 收集器避免全區域垃圾收集,它把堆內存劃分爲大小固定的幾個獨立區域,並且跟蹤這些區域的垃圾收集進度,同時在後臺維護一個優先級列表,每次根據所允許的收集時間, 優先回收垃圾最多的區域。區域劃分和優先級區域回收機制,確保 G1 收集器可以在有限時間獲得最高的垃圾收集效率

51、JVM 類加載機制

JVM 類加載機制分爲五個部分:加載,驗證,準備,解析,初始化,下面我們就分別來看一下這五個過程。

加載

加載是類加載過程中的一個階段, 這個階段會在內存中生成一個代表這個類的 java.lang.Class 對象, 作爲方法區這個類的各種數據的入口。注意這裏不一定非得要從一個 Class 文件獲取,這裏既可以從 ZIP 包中讀取(比如從 jar 包和 war 包中讀取),也可以在運行時計算生成(動態代理),也可以由其它文件生成(比如將 JSP 文件轉換成對應的 Class 類)。

驗證

這一階段的主要目的是爲了確保 Class 文件的字節流中包含的信息是否符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

準備

準備階段是正式爲類變量分配內存並設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間。注意這裏所說的初始值概念,比如一個類變量定義爲:

實際上變量 v 在準備階段過後的初始值爲 0 而不是 8080, 將 v 賦值爲 8080 的 put static 指令是程序被編譯後, 存放於類構造器方法之中。

但是注意如果聲明爲:

public static final int v = 8080;

在編譯階段會爲 v 生成 ConstantValue 屬性,在準備階段虛擬機會根據 ConstantValue 屬性將 v賦值爲 8080。

解析

解析階段是指虛擬機將常量池中的符號引用替換爲直接引用的過程。符號引用就是 class 文件中的

public static int v = 8080;

實際上變量 v 在準備階段過後的初始值爲 0 而不是 8080, 將 v 賦值爲 8080 的 put static 指令是程序被編譯後, 存放於類構造器方法之中。但是注意如果聲明爲:

在編譯階段會爲 v 生成 ConstantValue 屬性,在準備階段虛擬機會根據 ConstantValue 屬性將 v賦值爲 8080。

解析

解析階段是指虛擬機將常量池中的符號引用替換爲直接引用的過程。符號引用就是 class 文件中的

public static final int v = 8080;

在編譯階段會爲 v 生成 ConstantValue 屬性,在準備階段虛擬機會根據 ConstantValue 屬性將 v賦值爲 8080。

解析

解析階段是指虛擬機將常量池中的符號引用替換爲直接引用的過程。符號引用就是 class 文件中的:

(1) CONSTANT_Class_info

(2)CONSTANT_Field_info

(3)CONSTANT_Method_info

等類型的常量。

符號引用

符號引用與虛擬機實現的佈局無關, 引用的目標並不一定要已經加載到內存中。 各種虛擬機實現的內存佈局可以各不相同,但是它們能接受的符號引用必須是一致的,因爲符號引用的字面量形式明確定義在 Java 虛擬機規範的 Class 文件格式中。

直接引用

直接引用可以是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。

初始化

初始化階段是類加載最後一個階段,前面的類加載階段之後,除了在加載階段可以自定義類加載器以外,其它操作都由 JVM 主導。到了初始階段,纔開始真正執行類中定義的 Java 程序代碼。

類構造器

初始化階段是執行類構造器方法的過程。 方法是由編譯器自動收集類中的類變量的賦值操作和靜態語句塊中的語句合併而成的。虛擬機會保證子方法執行之前,父類的方法已經執行完畢, 如果一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯器可以不爲這個類生成()方法。注意以下幾種情況不會執行類初始化:

(1)通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。

(2)定義對象數組,不會觸發該類的初始化。

(3)常量在編譯期間會存入調用類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類。

(4)通過類名獲取 Class 對象,不會觸發類的初始化。

(5)通過 Class.forName 加載指定類時,如果指定參數 initialize 爲 false 時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。

(6)通過 ClassLoader 默認的 loadClass 方法,也不會觸發初始化動作。

52、類加載器

虛擬機設計團隊把加載動作放到 JVM 外部實現,以便讓應用程序決定如何獲取所需的類, JVM 提供了 3 種類加載器:

啓動類加載器(Bootstrap ClassLoader)

負責加載 JAVA_HOME\lib 目錄中的, 或通過-Xbootclasspath 參數指定路徑中的, 且被虛擬機認可(按文件名識別, 如 rt.jar) 的類。

擴展類加載器(Extension ClassLoader)

負責加載 JAVA_HOME\lib\ext 目錄中的,或通過 java.ext.dirs 系統變量指定路徑中的類庫。

應用程序類加載器(Application ClassLoader):

負責加載用戶路徑(classpath)上的類庫。JVM 通過雙親委派模型進行類的加載, 當然我們也可以通過繼承 java.lang.ClassLoader實現自定義的類加載器。

53、雙親委派

當一個類收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父類去完成,每一個層次類加載器都是如此,因此所有的加載請求都應該傳送到啓動類加載其中,只有當父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的Class), 子類加載器纔會嘗試自己去加載。

採用雙親委派的一個好處是比如加載位於 rt.jar 包中的類 java.lang.Object,不管是哪個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個 Object 對象

54、OSGI( 動態模型系統)

OSGi(Open Service Gateway Initiative),是面向 Java 的動態模型系統,是 Java 動態化模塊化系統的一系列規範。

55、動態改變構造

OSGi 服務平臺提供在多種網絡設備上無需重啓的動態改變構造的功能。爲了最小化耦合度和促使這些耦合度可管理, OSGi 技術提供一種面向服務的架構,它能使這些組件動態地發現對方。

56、模塊化編程與熱插拔

OSGi 旨在爲實現 Java 程序的模塊化編程提供基礎條件,基於 OSGi 的程序很可能可以實現模塊級的熱插拔功能,當程序升級更新時,可以只停用、重新安裝然後啓動程序的其中一部分,這對企業級程序開發來說是非常具有誘惑力的特性。

OSGi 描繪了一個很美好的模塊化開發目標,而且定義了實現這個目標的所需要服務與架構,同時也有成熟的框架進行實現支持。但並非所有的應用都適合採用 OSGi 作爲基礎架構,它在提供強大功能同時,也引入了額外的複雜度,因爲它不遵守了類加載的雙親委託模型。

57、JVM內存模型

線程獨佔:棧,本地方法棧,程序計數器

線程共享:堆,方法區

58、棧

又稱方法棧,線程私有的,線程執行方法是都會創建一個棧陣,用來存儲局部變量表,操作棧,動態鏈接,方法出口等信息.調用方法時執行入棧,方法返回式執行出棧.

59、本地方法棧

與棧類似,也是用來保存執行方法的信息.執行Java方法是使用棧,執行Native方法時使用本地方法棧.

60、程序計數器

保存着當前線程執行的字節碼位置,每個線程工作時都有獨立的計數器,只爲執行Java方法服務,執行Native方法時,程序計數器爲空.

61、堆

JVM內存管理最大的一塊,對被線程共享,目的是存放對象的實例,幾乎所欲的對象實例都會放在這裏,當堆沒有可用空間時,會拋出OOM異常.根據對象的存活週期不同,JVM把對象進行分代管理,由垃圾回收器進行垃圾的回收管理

62、方法區

又稱非堆區,用於存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器優化後的代碼等數據.1.7的永久代和1.8的元空間都是方法區的一種實現。

63、分代回收

分代回收基於兩個事實:大部分對象很快就不使用了,還有一部分不會立即無用,但也不會持續很長時間

年輕代->標記-複製

老年代->標記-清除

64、堆和棧的區別

棧是運行時單位,代表着邏輯,內含基本數據類型和堆中對象引用,所在區域連續,沒有碎片;堆是存儲單位,代表着數據,可被多個棧共享(包括成員中基本數據類型、引用和引用對象),所在區域不連續,會有碎片。

(1)功能不同

棧內存用來存儲局部變量和方法調用,而堆內存用來存儲Java中的對象。無論是成員變量,局部變量,還是類變量,它們指向的對象都存儲在堆內存中。

(2)共享性不同

棧內存是線程私有的。

堆內存是所有線程共有的。

(3)異常錯誤不同

如果棧內存或者堆內存不足都會拋出異常。

棧空間不足:java.lang.StackOverFlowError。

堆空間不足:java.lang.OutOfMemoryError。

(4)空間大小

棧的空間大小遠遠小於堆的

65、什麼時候會觸發FullGC

除直接調用System.gc外,觸發Full GC執行的情況有如下四種。

(1)舊生代空間不足

舊生代空間只有在新生代對象轉入及創建爲大對象、大數組時纔會出現不足的現象,當執行Full GC後空間仍然不足,則拋出如下錯

誤:

java.lang.OutOfMemoryError: Java heap space

爲避免以上兩種狀況引起的FullGC,調優時應儘量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組。

(2) Permanet Generation空間滿

PermanetGeneration中存放的爲一些class的信息等,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置爲採用CMS GC的情況下會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會拋出如下錯誤信息:

java.lang.OutOfMemoryError: PermGen space

爲避免Perm Gen佔滿造成Full GC現象,可採用的方法爲增大Perm Gen空間或轉爲使用CMS GC。

(3)CMS GC時出現promotion failed和concurrent mode failure

對於採用CMS進行舊生代GC的程序而言,尤其要注意GC日誌中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能會觸發Full GC。

promotionfailed是在進行Minor GC時,survivor space放不下、對象只能放入舊生代,而此時舊生代也放不下造成的;concurrentmode failure是在執行CMS GC的過程中同時有對象要放入舊生代,而此時舊生代空間不足造成的。

應對措施爲:增大survivorspace、舊生代空間或調低觸發併發GC的比率,但在JDK 5.0+、6.0+的版本中有可能會由於JDK的bug29導致CMS在remark完畢後很久才觸發sweeping動作。對於這種狀況,可通過設置-XX:CMSMaxAbortablePrecleanTime=5(單位爲ms)來避免。

(4)統計得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間

這是一個較爲複雜的觸發情況,Hotspot爲了避免由於新生代對象晉升到舊生代導致舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。

例如程序第一次觸發MinorGC後,有6MB的對象晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大於6MB,如果小於6MB,則執行Full GC。

當新生代採用PSGC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大於6MB,如小於,則觸發對舊生代的回收。除了以上4種狀況外,對於使用RMI來進行RPC或管理的Sun JDK應用而言,默認情況下會一小時執行一次Full GC。可通過在啓動時通過- java-Dsun.rmi.dgc.client.gcInterval=3600000來設置Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc

66、什麼是Java虛擬機?爲什麼Java被稱作是“平臺無關的編程語言”?

Java虛擬機是一個可以執行Java字節碼的虛擬機進程。Java源文件被編譯成能被Java虛擬機執行的字節碼文件。 Java被設計成允許應用程序可以運行在任意的平臺,而不需要程序員爲每一個平臺單獨重寫或者是重新編譯。Java虛擬機讓這個變爲可能,因爲它知道底層硬件平臺的指令長度和其他特性。

67、對象分配規則

(1)對象優先分配在Eden區,如果Eden區沒有足夠的空間時,虛擬機執行一次Minor GC。

(2)大對象直接進入老年代(大對象是指需要大量連續內存空間的對象)。這樣做的目的是避免在Eden區和兩個Survivor區之間發生大量的內存拷貝(新生代採用複製算法收集內存)。

(3)長期存活的對象進入老年代。虛擬機爲每個對象定義了一個年齡計數器,如果對象經過了1次Minor GC那麼對象會進入Survivor區,之後每經過一次Minor GC那麼對象的年齡加1,知道達到閥值對象進入老年區。

(4) 動態判斷對象的年齡。如果Survivor區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代。

(5) 空間分配擔保。每次進行Minor GC時,JVM會計算Survivor區移至老年區的對象的平均大小,如果這個值大於老年區的剩餘值大小則進行一次Full GC,如果小於檢查HandlePromotionFailure設置,如果true則只進行Monitor GC,如果false則進行Full GC

68、描述一下JVM加載class文件的原理機制?

JVM中類的裝載是由類加載器(ClassLoader)和它的子類來實現的,Java中的類加載器是一個重要的Java運行時系統組件,它負責在運行時查找和裝入類文件中的類。

由於Java的跨平臺性,經過編譯的Java源程序並不是一個可執行程序,而是一個或多個類文件。當Java程序需要使用某個類時,JVM會確保這個類已經被加載、連接(驗證、準備和解析)和初始化。

類的加載是指把類的.class文件中的數據讀入到內存中,通常是創建一個字節數組讀入.class文件,然後產生與所加載類對應的Class對象。加載完成後,Class對象還不完整,所以此時的類還不可用。

當類被加載後就進入連接階段,這一階段包括驗證、準備(爲靜態變量分配內存並設置默認的初始值)和解析(將符號引用替換爲直接引用)三個步驟。最後JVM對類進行初始化,

包括:

(1)如果類存在直接的父類並且這個類還沒有被初始化,那麼就先初始化父類;

(2)如果類中存在初始化語句,就依次執行這些初始化語句。 類的加載是由類加載器完成的,類加載器包括:根加載器(BootStrap)、擴展加載器(Extension)、系統加載器(System)和用戶自定義類加載器(java.lang.ClassLoader的子類)。

從Java 2(JDK 1.2)開始,類加載過程採取了父親委託機制(PDM)。PDM更好的保證了Java平臺的安全性,在該機制中,JVM自帶的Bootstrap是根加載器,其他的加載器都有且僅有一個父類加載器。類的加載首先請求父類加載器加載,父類加載器無能爲力時才由其子類加載器自行加載。JVM不會向Java程序提供對Bootstrap的引用。下面是關於幾個類加載器的說明

Bootstrap:一般用本地代碼實現,負責加載JVM基礎核心類庫(rt.jar);

Extension:從java.ext.dirs系統屬性所指定的目錄中加載類庫,它的父加載器是Bootstrap;

System:又叫應用類加載器,其父類是Extension。它是應用最廣泛的類加載器。它從環境變量classpath或者系統屬性java.class.path所指定的目錄中記載類,是用戶自定義加載器的默認父加載器。

69、Java對象創建過程

(1)JVM遇到一條新建對象的指令時首先去檢查這個指令的參數是否能在常量池中定義到一個類的符號引用。然後加載這個類(類加載過程在後邊講)

(2)爲對象分配內存。一種辦法“指針碰撞”、一種辦法“空閒列表”,最終常用的辦法“本地線程緩衝分配(TLAB)”

(3)將除對象頭外的對象內存空間初始化爲0

(4)對對象頭進行必要設置

70、簡述Java的對象結構

Java對象由三個部分組成:對象頭、實例數據、對齊填充。

對象頭由兩部分組成,第一部分存儲對象自身的運行時數據:哈希碼、GC分代年齡、鎖標識狀態、線程持有的鎖、偏向線程ID(一般佔32/64 bit)。第二部分是指針類型,指向對象的類元數據類型(即對象代表哪個類)。如果是數組對象,則對象頭中還有一部分用來記錄數組長度。

實例數據用來存儲對象真正的有效信息(包括父類繼承下來的和自己定義的)

對齊填充:JVM要求對象起始地址必須是8字節的整數倍(8字節對齊 )

71、如何判斷對象可以被回收

判斷對象是否存活一般有兩種方式:

引用計數:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時可以回收。此方法簡單,無法解決對象相互循環引用的問題。

可達性分析(Reachability Analysis):從GC Roots開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的,不可達對象。

72、JVM的永久代中會發生垃圾回收麼

垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。如果你仔細查看垃圾收集器的輸出信息,就會發現永久代也是被回收的。這就是爲什麼正確的永久代大小對避免Full GC是非常重要的原因。請參考下Java8:從永久代到元數據區 (注:Java8中已經移除了永久代,新加了一個叫做元數據區的native內存區)

73、垃圾收集算法

GC最基礎的算法有三種: 標記 -清除算法、複製算法、標記-壓縮算法,我們常用的垃圾回收器一般都採用分代收集算法。

標記 -清除算法

“標記-清除”(Mark-Sweep)算法,如它的名字一樣,算法分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。

複製算法

“複製”(Copying)的收集算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

標記-壓縮算法

標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存

分代收集算法

“分代收集”(Generational Collection)算法,把Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法

74、調優命令有哪些?

Sun JDK監控和故障處理命令有jps jstat jmap jhat jstack jinfo

(1)jps,JVM Process Status Tool,顯示指定系統內所有的HotSpot虛擬機進程。

(1)jstat,JVM statistics Monitoring是用於監視虛擬機運行時狀態信息的命令,它可以顯示出虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據。

(3)jmap,JVM Memory Map命令用於生成heap dump文件

(4) jhat,JVM Heap Analysis Tool命令是與jmap搭配使用,用來分析jmap生成的dump,jhat內置了一個微型的HTTP/HTML服務器,生成dump的分析結果後,可以在瀏覽器中查看

(5)jstack,用於生成java虛擬機當前時刻的線程快照。

(6) jinfo,JVM Configuration info 這個命令作用是實時查看和調整虛擬機運行參數

75、調優工具

常用調優工具分爲兩類,jdk自帶監控工具:jconsole和jvisualvm,第三方有:MAT(Memory AnalyzerTool)、GChisto。

(1) jconsole,Java Monitoring and Management Console是從java5開始,在JDK中自帶的java監控和管理控制檯,用於對JVM中內存,線程和類等的監控

(2)jvisualvm,jdk自帶全能工具,可以分析內存快照、線程快照;監控內存變化、GC變化等。

(3)MAT,Memory Analyzer Tool,一個基於Eclipse的內存分析工具,是一個快速、功能豐富的Javaheap分析工具,它可以幫助我們查找內存泄漏和減少內存消耗

(4)GChisto,一款專業分析gc日誌的工具

76、Minor GC與Full GC分別在什麼時候發生?

新生代內存不夠用時候發生MGC也叫YGC,JVM內存不夠的時候發生FGC

77、你知道哪些JVM性能調優

設定堆內存大小

-Xmx:堆內存最大限制。

設定新生代大小。 新生代不宜太小,否則會有大量對象湧入老年代

-XX:NewSize:新生代大小

-XX:NewRatio 新生代和老生代佔比

-XX:SurvivorRatio:伊甸園空間和倖存者空間的佔比

設定垃圾回收器 年輕代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC

最後

歡迎關注公衆號:程序員追風,回覆 003 領取2020最新Java面試題手冊(200多頁PDF總結)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章