天天看點

JVM相關知識點小結

1.JVM的主要組成部分?及其作用?

  • 類加載器(ClassLoader)
  • 運作時資料區(Runtime Data Area)
  • 執行引擎(Execution Engine)
  • 本地庫接口(Native Interface)

元件的作用: 首先通過類加載器(ClassLoader)會把 Java 代碼轉換成位元組碼,運作時資料區(Runtime Data Area)再把位元組碼加載到記憶體中,而位元組碼檔案隻是 JVM 的一套指令集規範,并不能直接交給底層作業系統去執行,是以需要特定的指令解析器執行引擎(Execution Engine),将位元組碼翻譯成底層系統指令,再交由 CPU 去執行,而這個過程中需要調用其他語言的本地庫接口(Native Interface)來實作整個程式的功能。

2.JVM運作時資料區(JVM記憶體模型)

java虛拟機在執行程式過程中會把它所管理的記憶體劃分為幾個不同的區域:

程式計數器、java堆、java棧、本地方法棧、方法區

其中,java棧、本地方法棧、程式計數器是線程隔離的區域,每個線程都有各自的一份,而方法區,java堆是線程共享的。

  • 程式計數器

    程式計數器又叫指令計數器,用來确定cpu下一條要執行的指令的位址。如果目前線程正在執行一個java方法,則程式計數器記錄正在執行的java位元組碼位址,如果目前線程正在執行一個本地方法,那麼程式計數器為空。

  • java堆

    堆在JVM啟動的時候會被建立,我們可以把它了解成一個記憶體池,用來存放所有的java對象,所有的線程共享java堆。

  • java棧

    每一個線程都有一個私有的java棧,java棧線上程建立的時候被建立,其中儲存着幀資訊。每個java方法在執行的同時都會建立一個棧幀,用于儲存局部變量、方法參數、方法出口等資訊,每一個方法從調用到執行完成的過程,都對應着一個棧幀在java棧中入棧到出棧的過程)棧的優勢 通路速度比堆要快,僅次于程式計數器。

  • 本地方法棧

    本地方法棧和java棧非常類似,不同之處在于java棧用于java方法的調用,而本地方法棧用于本地方法的調用。注意:本地方法棧不是用java實作的,而是使用C實作的。

  • 方法區(包含常量池)

    方法區和堆一樣是所有線程共享的區域,主要儲存的資訊是類的中繼資料,包括類的類型資訊、常量、靜态變量、域資訊、方法資訊等資料。

3.說一下堆棧的差別?

  • 功能方面:堆是用來存放對象的,棧是用來執行程式的。
  • 共享性:堆是線程共享的,棧是線程私有的。
  • 空間大小:堆大小遠遠大于棧。

4.隊列和棧是什麼?有什麼差別?

隊列和棧都是被用來預存儲資料的。

隊列允許先進先出檢索元素,但也有例外的情況,Deque 接口允許從兩端檢索元素。

棧和隊列很相似,但它運作對元素進行後進先出進行檢索。

5.什麼是雙親委派模型?

在介紹雙親委派模型之前先說下類加載器。對于任意一個類,都需要由加載它的類加載器和這個類本身一同确立在 JVM 中的唯一性,每一個類加載器,都有一個獨立的類名稱空間。類加載器就是根據指定全限定名稱将 class 檔案加載到 JVM 記憶體,然後再轉化為 class 對象。

類加載器分類:

  • 啟動類加載器(Bootstrap ClassLoader),是虛拟機自身的一部分,用來加載Java_HOME/lib/目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中并且被虛拟機識别的類庫;
  • 擴充類加載器(Extension ClassLoader):負責加載<java_home style=“box-sizing: border-box; outline: 0px !important;”>\lib\ext目錄或Java. ext. dirs系統變量指定的路徑中的所有類庫;
  • 應用程式類加載器(Application ClassLoader)。負責加載使用者類路徑(classpath)上的指定類庫,我們可以直接使用這個類加載器。一般情況,如果我們沒有自定義類加載器預設就是用這個加載器。
  • 其他類加載器:

雙親委派模型:如果一個類加載器收到了類加載的請求,它首先不會自己去加載這個類,而是把這個請求委派給父類加載器去完成,每一層的類加載器都是如此,這樣所有的加載請求都會被傳送到頂層的啟動類加載器中,隻有當父加載無法完成加載請求(它的搜尋範圍中沒找到所需的類)時,子加載器才會嘗試去加載類。如下圖:

JVM相關知識點小結

雙親委派的好處:它使得類有了層次的劃分。就拿java.lang.Object來說,你加載它經過一層層委托最終是由Bootstrap ClassLoader來加載的,也就是最終都是由Bootstrap ClassLoader去找<JAVA_HOME>\lib中rt.jar裡面的java.lang.Object加載到JVM中。

這樣如果有不法分子自己造了個java.lang.Object,裡面嵌了不好的代碼,如果我們是按照雙親委派模型來實作的話,最終加載到JVM中的隻會是我們rt.jar裡面的東西,也就是這些核心的基礎類代碼得到了保護。因為這個機制使得系統中隻會出現一個java.lang.Object。不會亂套了。你想想如果我們JVM裡面有兩個Object,那豈不是天下大亂了。

詳情連結:面試官:說說雙親委派模型?

6.說一下類裝載的執行過程?

類裝載分為以下 5 個步驟:

  • 加載:根據查找路徑找到相應的 class 檔案然後導入;
  • 檢查:檢查加載的 class 檔案的正确性;
  • 準備:給類中的靜态變量配置設定記憶體空間,并且賦予初值;
  • 解析:虛拟機将常量池中的符号引用替換成直接引用的過程。符号引用就了解為一個标示,而在直接引用直接指向記憶體中的位址;
  • 初始化:對靜态變量和靜态代碼塊執行初始化工作。

詳情連結:面試官:請你談談Java的類加載過程

7.怎麼判斷對象是否可以被回收?

一般有兩種方法來判斷:

  • 引用計數器:為每個對象建立一個引用計數,有對象引用時計數器 +1,引用被釋放時計數 -1,當計數器為 0 時就可以被回收。它有一個缺點不能解決循環引用的問題;
  • 可達性分析:從 GC Roots 開始向下搜尋,搜尋所走過的路徑稱為引用鍊。當一個對象到 GC Roots 沒有任何引用鍊相連時,則證明此對象是可以被回收的。

    即使在可達性分析算法中不可達的對象,也并非"非死不可"的,這時候他們暫時處在"緩刑"階段。要宣告一個對象的真正死亡,至少要經曆兩次标記過程: 如果對象在進行可達性分析之後發現沒有與GC Roots相連接配接的引用鍊,那它将會被第一次标記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆寫finalize()方法或者finalize()方法已經被JVM調用過,虛拟機會将這兩種情況都視為"沒有必要執行",此時的對象才是真正"死"的對象。

    如果這個對象被判定為有必要執行finalize()方法,那麼這個對象将會被放置在一個叫做F-Queue的隊列之中,并在稍後由一個虛拟機自動建立的、低優先級的Finalizer線程去執行它(這裡所說的執行指的是虛拟機會觸發finalize()方法)。finalize()方法是對象逃脫死亡的最後一次機會,稍後GC将對F-Queue中的對象進行第二次小規模标記,如果對象在finalize()中成功拯救自己(隻需要重新與引用鍊上的任何一個對象建立起關聯關系即可),那在第二次标記時它将會被移除出"即将回收"的集合;如果對象這時候還是沒有逃脫,那基本上它就是真的被回收了。

詳情連結:

深入了解JVM的垃圾回收機制

GC:如何确定一個對象是否可以被回收

8.Java 中都有哪些引用類型?

  • 強引用:發生 gc 的時候不會被回收。
  • 軟引用:有用但不是必須的對象,在發生記憶體溢出之前會被回收。
  • 弱引用:有用但不是必須的對象,在下一次GC時會被回收
  • 虛引用(幽靈引用/幻影引用):無法通過虛引用獲得對象,用 PhantomReference 實作虛引用,虛引用的用途是在 gc 時傳回一個通知。

9.說一下 JVM 有哪些垃圾回收算法?

  • 标記-清除算法:标記無用對象,然後進行清除回收。缺點:效率不高,無法清除垃圾碎片。
  • 複制算法:按照容量劃分二個大小相等的記憶體區域,當一塊用完的時候将活着的對象複制到另一塊上,然後再把已使用的記憶體空間一次清理掉。缺點:記憶體使用率不高,隻有原來的一半。
  • 标記-整理算法:标記無用對象,讓所有存活的對象都向一端移動,然後直接清除掉端邊界以外的記憶體。
  • 分代算法:根據對象存活周期的不同将記憶體劃分為幾塊,一般是新生代和老年代,新生代基本采用複制算法,老年代采用标記整理算法。

詳情連結:

深入了解JVM的垃圾回收機制

10. 簡述分代垃圾回收器是怎麼工作的?

分代回收器有兩個分區:老生代和新生代,新生代預設的空間占比總空間的 1/3,老生代的預設占比是 2/3。

新生代使用的是複制算法,新生代裡有 3 個分區:Eden、To Survivor、From Survivor,它們的預設占比是 8:1:1,它的執行流程如下:

  • 把 Eden + From Survivor 存活的對象放入 To Survivor 區;
  • 清空 Eden 和 From Survivor 分區;
  • From Survivor 和 To Survivor 分區交換,From Survivor 變 To Survivor,To Survivor 變 From Survivor。

每次在 From Survivor 到 To Survivor 移動時都存活的對象,年齡就 +1,當年齡到達 15(預設配置是 15)時,更新為老生代。大對象也會直接進入老生代。

老生代當空間占用到達某個值之後就會觸發全局垃圾收回,一般使用标記整理的執行算法。以上這些循環往複就構成了整個分代垃圾回收的整體執行流程。

11.說一下JVM有哪些垃圾回收器?

  • Serial:最早的單線程串行垃圾回收器。
  • Serial Old:Serial 垃圾回收器的老年版本,同樣也是單線程的,可以作為 CMS 垃圾回收器的備選預案。
  • ParNew:是 Serial 的多線程版本。
  • Parallel 和 ParNew 收集器類似是多線程的,但 Parallel 是吞吐量優先的收集器,可以犧牲等待時間換取系統的吞吐量。
  • Parallel Old 是 Parallel 老生代版本,Parallel 使用的是複制的記憶體回收算法,Parallel Old 使用的是标記-整理的記憶體回收算法。
  • CMS:一種以獲得最短停頓時間為目标的收集器,非常适用 B/S 系統。
  • G1:一種兼顧吞吐量和停頓時間的 GC 實作,是 JDK 9 以後的預設 GC 選項。

    詳情連結:Jvm垃圾回收器(終結篇)

12.新生代垃圾回收器和老生代垃圾回收器都有哪些?有什麼差別?
JVM相關知識點小結

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

新生代垃圾回收器一般采用的是複制算法,複制算法的優點是效率高,缺點是記憶體使用率低;老年代回收器一般采用的是标記-整理的算法進行垃圾回收。

13.詳細介紹一下 CMS 垃圾回收器?

CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器。對于要求伺服器響應速度的應用上,這種垃圾回收器非常适合。在啟動 JVM 的參數加上“-XX:+UseConcMarkSweepGC”來指定使用 CMS 垃圾回收器。

CMS 使用的是标記-清除的算法實作的,是以在 gc 的時候回産生大量的記憶體碎片,當剩餘記憶體不能滿足程式運作要求時,系統将會出現 Concurrent Mode Failure,臨時 CMS 會采用 Serial Old 回收器進行垃圾清除,此時的性能将會被降低。

14.說一下 JVM 調優的工具?

JDK 自帶了很多監控工具,都位于 JDK 的 bin 目錄下,其中最常用的是 jconsole 和 jvisualvm 這兩款視圖監控工具。

  • jconsole:用于對 JVM 中的記憶體、線程和類等進行監控;
  • jvisualvm:JDK 自帶的全能分析工具,可以分析:記憶體快照、線程快照、程式死鎖、監控記憶體的變化、gc 變化等。