萬字長文總結!吐血推薦的JVM面試題乾貨

1.什麼是JVM

說一說什麼是JVM

JVM,即 Java Virtual Machine,Java 虛擬機。它通過模擬一個計算機來達到一個計算機所具有的的計算功能。JVM 能夠跨計算機體系結構來執行 Java 字節碼,主要是由於 JVM 屏蔽了與各個計算機平臺相關的軟件或者硬件之間的差異,使得與平臺相關的耦合統一由 JVM 提供者來實現。

2.JVM基本結構

說說JVM的基本結構是什麼樣子

jvm的基本結構主要分爲3類:

  1. 類加載子系統
    JVM 啓動時或者類運行時將需要的 class 加載到 JVM 中
  2. 運行時數據區
    將內存劃分成若干個區以模擬實際機器上的存儲、記錄和調度功能模塊,如實際機器上的各種功能的寄存器或者 PC 指針的記錄器等
  3. 執行引擎
    執行引擎的任務是負責執行 class 文件中包含的字節碼指令,相當於實際機器上的 CPU

3.運行時數據區

運行時數據區都由什麼組成,具體到每個區存放什麼

運行時數據區分爲線程私有共享數據區兩大類
線程私有:程序計數器、虛擬機棧和本地方法棧
共享數據區:Java堆,方法區(java8 元空間)

  • 程序計數器:記錄當前線程指定指令的位置

  • 虛擬機棧:棧幀構成,每調用一個方法就壓入一個棧幀,棧幀中包含操作數棧,局部變量表,動態鏈接和方法出口,其中局部變量表存放的類型是8種基本類型和一個引用類型

  • 本地方法棧:具有和虛擬機棧類似的特點和功能,它服務的對象是Native方法

  • 堆:存放所有的對象實例和數組

  • 方法區:虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據

4 hotspot方法區的實現

hotspot 虛擬機的方法區存放了什麼,1.7之前和1.8之後有什麼區別

對於常用的hotspot虛擬機,方法區分爲1.7和1.8版本:1.7及之前,方法區也稱爲永久代。存放類信息、常量、靜態變量、即時編譯器編譯後的代碼,1.8之後,使用元空間實現方法區,永久代被廢棄,元空間存放在本地內存中。類信息存元空間中,常量池靜態變量放到了Java
元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。

爲什麼jdk1.8要把方法區從JVM裏(永久代)移到直接內存(元空間)

原因一:
從數據流的角度,非直接內存:本地IO --> 直接內存 --> 非直接內存 --> 直接內存 --> 本地IO
而直接內存是:本地IO --> 直接內存 --> 本地IO
原因二:
​ 整個永久代有一個 JVM 本身設置的固定大小上限,無法進行調整,而元空間使用的是直接內存,受本機可用內存的限制,並且永遠不會得到java.lang.OutOfMemoryError。

5 堆的結構

堆的分區是什麼樣子的,各自有什麼特點

JVM線程共享區域可分爲3個區域:新生代,老年代,和永久代。其中JVM堆分爲新生代和老年代

新生代:
Eden空間、From Survivor空間、To Survivor空間
新對象分配內存的地方,發生minor gc會清除eden區和survival區的,把存活的對象移到另
一個Survival區
(理解記憶 Eden:伊甸園 Survivor:存活)

老年代:
​ 對象創建在新生代,經過很多次回收還依然存活的,會進入老年代。

6 爲何新生代要設置兩個survivor區

爲何新生代要在eden區外設置兩個survivor區

survivor區域是爲了方便實現複製算法:將原有的內存空間劃分成兩塊,每次只使用其中一塊,在垃圾回收的時候,將正在使用的內存中的存活對象複製到另一塊內存區域中,然後清除正使用過的內存區域,交換兩個區域的角色,完成垃圾回收。

複製算法,爲什麼要在新生代中使用複製算法:
因爲新生代gc比較頻繁、對象存活率低,用複製算法在回收時的效率會更高,也不會產生內存碎片。但複製算法的代價就是要將內存摺半,爲了不浪費過多的內存,就劃分了兩塊相同大小的內存區域survivor from和survivor to。在每次gc後就會把存活對象給複製到另一個survivor上,然後清空Eden和剛使用過的survivor。

7 對象訪問定位

對象訪問定位有哪些方法

  • 句柄方式訪問
    移動對象方便,GC時快

  • 直接指針訪問(Hotspot所選)
    訪問對象更快,省一次尋址時間

8 判斷對象存活方式

如何判定對象是否存活

  • 引用計數法

    實例對象中存在計數器,如果某個地方引用了這個對象就+1,如果失效了就-1,當爲 0 就會被回收。JVM沒有使用它的原因是無法解決循環引用問題

  • 可達性分析(JVM所選)
    從GC Roots起始向下搜索,不可達對象被回收
    GC Roots對象:

    • 虛擬機棧中引用對象
    • 方法區中類靜態屬性引用對象
    • 方法區中常量引用對象
    • 本地方法棧中JNI(Native方法)引用對象

9 GC安全點

safepoint 是什麼,如何選定安全點

HotSpot 通過GC Roots枚舉判定待回收的對象。
找到對象哪些是GC Roots。有兩種方法:
一種是遍歷方法區和棧區查找(保守式 GC)。
一種是通過 OopMap 數據結構來記錄 GC Roots 的位置(準確式 GC)。
保守式GC 的成本太高。因此在HotSpot中,使用OopMap的結構來標記對象引用的位置。OopMap 記錄了棧中變量到堆上對象的引用關係,通過OopMap,HotSpot可以快速準確地定位到GC Roots,進行GC。
在執行 GC 操作時需要STW(stop the world,所有的工作線程必須停頓)
安全點意味着在這個點時,所有工作線程的狀態是確定的,JVM 就可以安全地執行 GC 。
安全點太多,GC 過於頻繁,增大運行時負荷;安全點太少,GC 等待時間太長。
一般會在如下幾個位置選擇安全點:
1、循環的末尾
2、方法臨返回前
3、調用方法之後
4、拋異常的位置

爲什麼選定這些位置作爲安全點:
避免程序長時間無法進入 Safe Point。比如 JVM 在做 GC 之前要等所有的應用線程進入安全點,如果有一個線程一直沒有進入安全點,就會導致 GC 時 JVM 停頓時間延長

如何在 GC 發生時,所有線程都跑到最近的 Safe Point 上再停下來?
主要有兩種方式:
搶斷式中斷:在 GC 發生時,首先中斷所有線程,如果發現線程未執行到 Safe Point,就恢復線程讓其運行到 Safe Point 上。

主動式中斷:在 GC 發生時,不直接操作線程中斷,而是簡單地設置一個標誌,讓各個線程執行時主動輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起。

JVM 採取的就是主動式中斷。輪詢標誌的地方和安全點是重合的。

10 GC

聊一聊GC機制

主要是三個問題:

  1. 什麼是垃圾
  2. 在哪裏回收垃圾
  3. 怎麼回收垃圾

1.在內存上存在着無數對象,之前需要準確將這些對象標記出來,分爲存活對象與垃圾對象。標記方法是可達性分析,前面已經說過。
2.發生在運行時數據區中。其中,隨線程消亡,線程獨享內存(棧,程序計數器和本地方棧)被回收。
3.目前主流 GC 算法主要分爲三種:

  • 標記-清除算法

    先通過 GC Roots 標記出可達對象,再清理未標記對象

    缺點:內存碎片,效率不高

  • 複製算法

    用完一塊內存,將對象複製到另外一塊上

    缺點:空間換時間,犧牲一部分內存

  • 標記-整理算法

    通過 GC Roots 標記存活對象

    將存活對象往一端移動,按照內存地址一次排序,然後將末端邊界之外內存直接清理。

    效率低,甚至不如標記清除

    圖例:

標記-清除

複製算法:

​標記-整理

JVM中怎麼使用的這些算法

從上面三種 GC 算法可以看到,並沒有一種空間與時間效率都是比較完美的算法,所以採用分代方式使用這些算法

JVM根據對象存活週期劃分新生代,老年代。新對象一般情況都會優先分配在新生代,新生代對象若存活時間大於一定閾值之後,將會移到至老年代。

新生代每次 GC 之後都可以回收大批量對象,所以比較適合複製算法。這裏內存劃分並沒有按照 1:1 劃分,默認將會按照 8:1:1 劃分成 Eden 與兩塊 Survivor空間。每次將 Eden 與一塊Survivor共同存儲對象,GC時存活對象都複製到另一塊空閒的Survivor區,然後這兩塊Survivor功能互換,以此類推。當Survivor空間並不能保存剩餘存活對象,就將這些對象通過分配擔保進制移動至老年代。

老年代中對象存活率將會特別高,且沒有額外空間進行分配擔保,所以需要使用標記-清除或標記-整理算法。

11 內存回收和分配策略

什麼時候對象會進入老年代

  1. 大對象直接進入老年代

    • 哪些屬於大對象呢?

    一般來說大對象指的是很長的字符串及數組,或者靜態對象

    • 那麼需要滿足多大才是大對象呢?

    這個虛擬機提供了一個參數-XX:PretenureSizeThreshold=n,只需要大於這個參數所設置的值,就可以直接進入到老年代。

  2. 長期存活的對象將進去老年代

    對象熬過以此Minor GC就增長一歲,默認閾值15歲進入老年代

  3. 動態年齡判斷

    Survivor空間相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於等於該年齡的對象就可以直接進入老年代

什麼是空間分配擔保策略

Minor GC之前,先檢查老年代最大可用連續空間是否大於新生代所有對象總空間,再檢查空間是否大於歷次晉升到老年代對象的平均大小,大於則Minor GC,大於則Full GC

12 GC收集器

介紹下JVM的垃圾收集器

上面的表示是年輕代的垃圾回收器:Serial、ParNew、Parallel Scavenge,下面表示是老年代的垃圾回收器:CMS、Parallel Old、Serial Old,以及不分老年代和年輕代的G1。連線表示可以相互配合使用。

停頓時間:GC中斷執行的時間 吞吐量:執行時間(排除GC時間)佔總時間的佔比 1- 1/(1+n)

CMS和G1是重點,單獨分析

收集器 串行、並行or併發 新生代/老年代 算法 目標 適用場景
Serial 串行 新生代 複製算法 響應速度優先 單CPU環境下的Client模式
Serial Old 串行 老年代 標記-整理 響應速度優先 單CPU環境下的Client模式、CMS的後備預案
ParNew 並行 新生代 複製算法 響應速度優先 多CPU環境時在Server模式下與CMS配合
Parallel Scavenge 並行 新生代 複製算法 吞吐量優先 在後臺運算而不需要太多交互的任務
Parallel Old 並行 老年代 標記-整理 吞吐量優先 在後臺運算而不需要太多交互的任務

說一下 CMS 垃圾回收器

CMS(Concurrent Mark Sweep)收集器 目標:最短回收停頓時間,“標記-清除”實現,應用場景廣泛,比較主流

  • 工作流程

    1. 初始標記:僅標記一下GC Roots能直接關聯到的對象,速度很快,“Stop The World”
    2. 併發標記:從第一步標記的對象出發,併發標記所有可達對象。
    3. 重新標記:修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。“Stop The World”。
    4. 併發清除。
  • 優缺點:

    優:併發收集,停頓時間短

    缺:

    1. 標記清除算法的碎片問題
    2. concurrent mode failureCMS GC和業務線程都在執行,兩個情況導致:(1)Minor GC完畢後需要將部分存活對象放入老年代,老年代還未來得及清理,空間不足;(2)做Minor GC的時候,新生代放不下,老年代也放不下
    3. promotion failed
      Minor GC時,Survivor Space放不下,對象只能放入老年代,而此時老年代也放不下造成。多數是由於老年代有足夠的空閒空間,但是碎片較多,找不到一段連續區域存放這個對象導致的。
  • CMS 缺點解決辦法

    1. 垃圾碎片的問題

      設置參數:-XX:CMSFullGCsBeforeCompaction=n 上一次CMS併發GC執行過後,還要再執行多少次full GC纔會做壓縮。默認0,即每次CMS GC頂不住了而要轉入full GC的時候都會做壓縮。

    2. concurrent mode failure問題

      設置參數-XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60:是指設定CMS在對內存佔用率達到60%的時候開始GC

      由於CMS GC過程中需要預留有足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集。

    3. promotion failed問題

      讓CMS在進行一定次數的Full GC時候進行一次標記整理算法,CMS提供了以下參數來控制:

      -XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
      

      即CMS在進行5次Full GC之後進行一次標記整理算法,從而可以控制老年帶的碎片在一定的數量以內

    總結一句話:使用標記整理清除碎片和提早進行CMS操作。

介紹一下G1 收集器

傳統的GC收集器將連續的內存空間劃分爲新生代、老年代和永久代(JDK 8 元空間Metaspace),這種特點是各代的邏輯存儲地址是連續的。而G1的各代存儲地址是不連續的,每一代都使用了n個不連續的大小相同的Region,每個Region佔有一塊連續的虛擬內存地址。

有一些Region標明瞭H,代表Humongous,表示這些Region存儲的是巨大對象(humongous object,H-obj),即>=region一半存儲的對象。

H-obj特徵:

  • H-obj直接分配到了老年代,防止了反覆拷貝移動
  • H-obj在global concurrent marking階段的cleanup 和 full GC階段回收
  • 分配H-obj之前先檢查是否超過Java堆佔用率閾值, 如果超過的話就啓動併發標記,爲的是提早回收從而防止 Evacuation Failures 和 Full GC

爲了減少連續H-objs分配對GC的影響,需要把大對象變爲普通的對象,建議增大Region size。

  • GC過程

    G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是完全Stop The World的。

    • Young GC:選定所有年輕代裏的Region。通過控制年輕代的region個數,即年輕代內存大小,來控制young GC的時間開銷。
    • Mixed GC:選定所有年輕代裏的Region,外加根據global concurrent marking統計得出收集收益高的若干老年代Region。在用戶指定的開銷目標範圍內儘可能選擇收益高的老年代Region。

    詳細過程參考

G1比CMS好在哪兒

  • G1是一個有整理內存過程的垃圾收集器,不會產生很多內存碎片。
  • G1的Stop The World(STW)更可控,G1在停頓時間上添加了預測機制,用戶可以指定期望停頓時間。
  • G1 從整體來看是基於“標記—整理”算法實現的收集器,從局部(兩個 Region 之間)上來看是基於“複製”算法實現的

13 類加載過程

描述一下 JVM 加載 Class 文件的原理機制

Java中的所有類,都需要由類加載器裝載到JVM中才能運行。類加載器本身也是一個類,功能是把class文件從硬盤讀取到內存中。除了反射等顯式加載類以外,幾乎不需要關心類的加載,這些都是隱式裝載的

Java類的加載是動態的,保證程序運行的基礎類(像是基類)完全加載到jvm中,其他類則在需要的時候才加載。節省內存開銷。

說一下JVM類加載的過程

類加載過程:加載->連接->初始化。連接過程又可分爲三步:驗證->準備->解析

整個過程:通過全限定名來加載生成class對象到內存中,然後進行驗證這個class文件,包括文件格式校驗、元數據驗證,字節碼校驗等。準備是對這個對象分配內存。解析是將符號引用轉化爲直接引用(指針引用),初始化就是開始執行構造器的代碼

JVM中有哪些類加載器

JVM 預定義的類加載器

JVM 中內置了三個重要的 ClassLoader,除BootstrapClassLoader 外,其它均由 Java 實現且全部繼承自java.lang.ClassLoader

  • 啓動類加載器 BootstrapClassLoader
    負責加載系統類,加載 %JAVA_HOME%/lib目錄下的jar包和類
  • 擴展類加載器 ExtensionClassLoader
    主要負責加載目錄 %JRE_HOME%/lib/ext 目錄下的jar包和類,或被 java.ext.dirs 系統變量所指定的路徑下的jar包。
  • 應用程序類加載器 AppClassLoader
    面向用戶的加載器,負責加載當前應用classpath下的所有jar包和類

除此之外,還可以用戶自定義類加載器

說一說雙親委派模型

在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則纔會嘗試加載。首先它會把這個類請求委派給父類加載器去完成,一直遞歸到頂層,當父加載器無法完成這個請求時,子類纔會嘗試去加載。(這裏的雙親其實就指的是父類,沒有mother。父類也不是指繼承關係,只是調用邏輯是這樣)當父類加載器爲null時,會使用啓動類加載器 BootstrapClassLoader 作爲父類加載器。

雙親委派模型不是一種強制性約束,是一種JAVA設計者推薦使用類加載器的方式。

雙親委派模型有什麼好處

(1)安全性,避免用戶自己編寫的類動態替換 Java的一些核心類,比如 String。

(2)避免了類的重複加載,因爲 JVM中區分不同類,不僅僅是根據類名,相同的 class文件被不同的 ClassLoader加載就是不同的兩個類。

有沒有破壞雙親委派模型的方式

某些情況下,需要由子類加載器去加載class文件,這時就需要破壞雙親委派模型。避免雙親委託機制,可以自定義一個類加載器,然後重寫 loadClass() 即可。

經典如Tomcat,t造了一堆自己的classloader,**每個Tomcat的webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器。**目的:

  1. 各個webapp中的class和lib,需要相互隔離,不能出現一個應用中加載的類庫會影響另一個應用的情況;而對於許多應用,需要有共享的lib以便不浪費資源(舉個例子,如果webapp1和webapp2都用到了log4j,可以將log4j提到tomcat/lib中,表示所有應用共享此類庫,試想如果log4j很大,並且20個應用都分別加載,那實在是沒有必要的。)
  2. 使用單獨的classloader去裝載tomcat自身的類庫,以免其他惡意或無意的破壞
  3. 熱部署,定期檢查是否需要熱部署,如果需要,則將類裝載器也重新裝載,並且去重新裝載其他相關類
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章