1. 程式人生 > >Java虛擬機器面試題精選(二)

Java虛擬機器面試題精選(二)

概述

現在面試Java開發時,基本都會問到Java虛擬機器的知識,根據職位不同問的內容深淺又有所區別。本文整理了10道面試中常問的Java虛擬機器面試題,希望對正在面試的同學有所幫助。

11.介紹下垃圾收集機制(在什麼時候,對什麼,做了什麼)?

在什麼時候?

在觸發GC的時候,具體如下,這裡只說常見的Young GC和Full GC。

觸發Young GC:當新生代中的Eden區沒有足夠空間進行分配時會觸發Young GC。

觸發Full GC:

  1. 當準備要觸發一次Young GC時,如果發現統計資料說之前Young GC的平均晉升大小比目前老年代剩餘的空間大,則不會觸發Young GC而是轉為觸發Full GC。(通常情況)
  2. 如果有永久代的話,在永久代需要分配空間但已經沒有足夠空間時,也要觸發一次Full GC。
  3. System.gc()預設也是觸發Full GC。
  4. heap dump帶GC預設也是觸發Full GC。
  5. CMS GC時出現Concurrent Mode Failure會導致一次Full GC的產生。

對什麼?

對那些JVM認為已經“死掉”的物件。即從GC Root開始搜尋,搜尋不到的,並且經過一次篩選標記沒有復活的物件。

做了什麼?

對這些JVM認為已經“死掉”的物件進行垃圾收集,新生代使用複製演算法,老年代使用標記-清除和標記-整理演算法。

12.GC Root有哪些?

在Java語言中,可作為GC Roots的物件包括下面幾種:

  • 虛擬機器棧(棧幀中的本地變量表)中引用的物件。
  • 方法區中類靜態屬性引用的物件。
  • 方法區中常量引用的物件。
  • 本地方法棧中JNI(即一般說的Native方法)引用的物件。

13.發生Young GC的時候需要掃描老年代的物件嗎?

在分代收集中,新生代的規模一般都比老年代要小許多,新生代的收集也比老年代要頻繁許多,如果回收新生代時也不得不同時掃描老年代的話,那麼Young GC的效率可能下降不少。顯然是不可能區掃描老年代的,那麼是通過什麼辦法來解決這個問題了?

在大多垃圾收集器中(G1有不同的地方),通過CardTable來維護老年代對年輕代的引用,CardTable可以說是Remembered Set(RS)的一種特殊實現,是Card的集合。Card是一塊2的冪位元組大小的記憶體區域,例如HotSpot用512位元組,裡面可能包含多個物件。CardTable要記錄的是從它覆蓋的範圍出發指向別的範圍的指標。以分代式GC的CardTable為例,要記錄老年代指向年輕代的跨代指標,被標記的Card是老年代範圍內的。當進行年輕代的垃圾收集時,只需要掃描年輕代和老年代的CardTable即可保證不對全堆掃描也不會有遺漏。CardTable通常為位元組陣列,由Card的索引(即陣列下標)來標識每個分割槽的空間地址。

14.垃圾收集器有哪些?

目前HotSpot中有7種作用於不同分代的收集器,如下圖所示,如果兩個收集器之間存在連線,就說明它們可以搭配使用。

15.介紹CMS垃圾收集器的特點?

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS收集器就非常符合這類應用的需求。

從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基於“標記—清除”演算法實現的,它的運作過程可以分為6個步驟,包括:初始標記、併發標記、預處理、重新標記、併發清除、重置。

CMS是一款優秀的收集器,它的主要優點在名字上已經體現出來了:併發收集、低停頓,但是CMS還遠達不到完美的程度,它有以下3個明顯的缺點:

  1. CMS收集器對CPU資源非常敏感。
  2. CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。
  3. CMS是一款基於“標記—清除”演算法實現的收集器,這意味著收集結束時會有大量空間碎片產生。

16.介紹下G1垃圾收集器的特點?(較複雜,可以考慮跳過)

G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一。G1是一款面向服務端應用的垃圾收集器。與其他GC收集器相比,G1具備如下特點:並行與併發、分代收集、空間整合、可預測的停頓。

在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域,雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。

G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。

Mixed GC是G1垃圾收集器特有的收集方式,Mixed GC大致可劃分為全域性併發標記(global concurrent marking)拷貝存活物件(evacuation)兩個大部分:

global concurrent marking是基於SATB形式的併發標記,包括以下4個階段:初始標記(Initial Marking)、併發標記(Concurrent Marking)、最終標記(Final Marking)、清理(Clean Up)。Evacuation階段是全暫停的。它負責把一部分region裡的活物件拷貝到空region裡去,然後回收原本的region的空間。

17.類載入的過程。

類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝7個階段。其中驗證、準備、解析3個部分統稱為連線。

載入:

“類載入”過程的一個階段,在載入階段,虛擬機器需要完成以下3件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進位制位元組流。
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
  3. 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。

驗證:

連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。從整體上看,驗證階段大致上會完成下面4個階段的檢驗動作:檔案格式驗證、元資料驗證、位元組碼驗證、符號引用驗證。

準備:

該階段是正式為類變數(static修飾的變數)分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。這裡所說的初始值“通常情況”下是資料型別的零值,下表列出了Java中所有基本資料型別的零值。


解析:

該階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。

初始化:

初始化階段是執行類構造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的所有類變數(static修飾的變數)的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序所決定的。如果該類存在父類,則虛擬機器會保證在執行子類的<clinit>()方法前,父類的<clinit>()方法已經執行完畢。因此在虛擬機器中第一個被執行<clinit>()方法的類肯定是java.lang.Object。

18.Java虛擬機器中有哪些類載入器?

從Java虛擬機器的角度來講,只存在兩種不同的類載入器:一種是啟動類載入器(Bootstrap ClassLoader),這個類載入器使用C++語言實現,是虛擬機器自身的一部分;另一種就是所有其他的類載入器,這些類載入器都由Java語言實現,獨立於虛擬機器外部,並且全都繼承自抽象類java.lang.ClassLoader。

從Java開發人員的角度來看,絕大部分Java程式都會使用到以下3種系統提供的類載入器。

啟動類載入器(Bootstrap ClassLoader):

這個類載入器負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的(僅按照檔名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器記憶體中。

擴充套件類載入器(Extension ClassLoader):

這個載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。

應用程式類載入器(Application ClassLoader):

這個類載入器由sun.misc.Launcher$AppClassLoader實現。由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

我們的應用程式都是由這3種類載入器互相配合進行載入的,如果有必要,還可以加入自己定義的類載入器。這些類載入器之間的關係一般如圖所示。


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

如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

20.使用雙親委派模型的好處?

使用雙親委派模型來組織類載入器之間的關係,有一個顯而易見的好處就是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類載入器自行去載入的話,如果使用者自己編寫了一個稱為java.lang.Object的類,並放在程式的ClassPath中,那系統中將會出現多個不同的Object類,Java 型別體系中最基礎的行為也就無法保證,應用程式也將會變得一片混亂。

—————END—————