2019秋招:460道Java後端面試高頻題答案版【模塊四:Java虛擬機】

由於之前分享的 460道Java後端高頻面試題 中只分享了題目,大家都建議附有答案。所以最近根據題目整理了下答案,因爲題目比較多,所以按照原文中的模塊陸續發出。因爲個人水平有限,僅供參考,如有錯誤,可與我交流,再改正。可掃描文末二維碼加我的微信(微信號:pcwl_Java),備註:面試題。

寫在前面:

Java 虛擬機是面試中必問的考點,很少遇到在一家公司幾輪面試中沒有被問到 Java 虛擬機的問題的情況。其重要性文字告訴你:大寫加粗!下面介紹下我是如何學習 Java 虛擬機的:  

1、強推:周志明的《深入理解 Java 虛擬機》,這本書可以說基本上涵蓋了面試的常問考點。這本書的內容通俗易懂,我是從開始學習 Java 虛擬機到現在讀了 3 遍,當然後面兩遍過的比較快。爲了突顯出它的重要性以及這本書對我秋招面試中所發揮的作用,特將其封面放在下面。請大家一定好好讀,面試肯定有很大的幫助。如果你這本書已經讀了 3 遍以上,基本上沒有看本文的必要了。

2、看面經:對於 Java 虛擬機的面試高頻題要做好自己的答案。做好能加上自己的理解,因爲這部分大家可能都會看《深入理解 Java 虛擬機》這本書,那麼最好結合自己的知識體系,在答案中加入一些自己的總結;

3、調優加分:對於 Java 虛擬機這部分,除了要了解基礎原理部分之外,最好能對調優有些瞭解,比如:JDK 自帶的虛擬機監控工具、JVM 常用的參數、如何調優等等。

1、說一下 Jvm 的主要組成部分?及其作用?

1. 類加載器(ClassLoader)

2. 運行時數據區(Runtime Data Area)

3. 執行引擎(Execution Engine)

4. 本地庫接口(Native Interface)

各組件的作用:首先通過類加載器(ClassLoader)會把 Java 代碼轉換成字節碼,運行時數據區(Runtime Data Area)再把字節碼加載到內存中,而字節碼文件只是 JVM 的一套指令集規範,並不能直接交給底層操作系統去執行,因此需要特定的命令解析器執行引擎(Execution Engine),將字節碼翻譯成底層系統指令,再交由 CPU 去執行,而這個過程中需要調用其他語言的本地庫接口(Native Interface)來實現整個程序的功能。

2、談談對運行時數據區的理解?

Tip:這道題是非常重要的題目,幾乎問到 Java 虛擬機這塊都是會被問到的。建議不要簡單的只回答幾個區域的名稱,最好展開的講解下,下面的答案是比較詳細的,根據自己的理解回答其中某一段即可。

  • 1. 程序計數器

程序計數器(Program  Counter  Register):是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。

字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。程序的分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

由於 Java 虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器都只會執行一條線程中的命令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各線程之間的計數器互不影響,獨立存儲,我們程這塊內存區域爲“線程私有”的內存。

此區域是唯一 一個虛擬機規範中沒有規定任何 OutOfMemoryError 情況的區域。

  • 2. Java 虛擬機棧

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

局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用和 returnAddress 類型(指向了一條字節碼指令的地址)。

Java 虛擬機棧的局部變量表的空間單位是槽(Slot),其中 64 位長度的 double 和 long 類型會佔用兩個 Slot。局部變量表所需內存空間在編譯期完成分配,當進入一個方法時,該方法需要在幀中分配多大的局部變量是完全確定的,在方法運行期間不會改變局部變量表的大小。

Java虛擬機棧有兩種異常狀況:如果線程請求的棧的深度大於虛擬機所允許的深度,將拋出 StackOverflowError 異常;如果擴展時無法申請到足夠的內存,就會拋出 OutOfMemoryError 異常。

  • 3. 本地方法棧

本地方法棧(Native  Method  Stack):與虛擬機棧所發揮的作用是非常相似的,它們之間的區別只不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。

Java 虛擬機規範沒有對本地方法棧中方法使用的語言、使用的方式和數據結構做出強制規定,因此具體的虛擬機可以自由地實現它。比如:Sun  HotSpot 虛擬機直接把Java虛擬機棧和本地方法棧合二爲一。

與Java虛擬機棧一樣,本地方法棧也會拋出StackOverflowError和 OutOfMemoryError 異常。

  • 4. Java 堆

Java堆(Java  Heap):是被所有線程所共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是:存放對象實例,幾乎所有的對象實例都在這裏分配內存。

Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC”堆(Garbage  Collected  Heap)。從內存回收的角度看,由於現在收集器基本都採用分代收集算法,所以 Java 堆中還可以細分爲:新生代和老年代。從內存分配角度來看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩衝區(Thread  Local  Allocation  Buffer, TLAB)。不過無論如何劃分,都與存放的內容無關,無論哪個區域,存儲的都仍然是對象實例,進一步劃分的目的是爲了更好地回收內存,或者更快地分配內存。

Java 虛擬機規定,Java 堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可。在實現時,可以是固定大小的,也可以是可擴展的。如果在堆中沒有完成實例分配。並且堆也無法擴展時,將會拋出  OutOfMemoryError 異常。

  • 5. 方法區

方法區(Method  Area):與 Java 堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

雖然 Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),其目的應該就是與 Java 堆區分開來。

Java 虛擬機規範對方法區的限制非常寬鬆,除了和 Java 堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。

根據Java虛擬機規範規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

運行時常量池

運行時常量池(Runtime  Constant  Pool):是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一些信息是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。

Java 虛擬機對 Class 文件每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用於存儲哪種數據都必須符合規範上的要求才會被虛擬機認可、裝載和執行。

直接內存

直接內存(Direct  Memory):並不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。但是這部分內存也頻繁地使用,而且也可能導致 OutOfMemoryError 異常。

本地直接內存的分配不會受到 Java 堆大小的限制,但是,既然是內存,肯定還是會受到本機總內存大小以及處理器尋址空間的限制。如果各個內存區域總和大於物理內存限制,從而導致動態擴展時出現 OutOfMemoryError 異常。

3、堆和棧的區別是什麼?

堆和棧(虛擬機棧)是完全不同的兩塊內存區域,一個是線程獨享的,一個是線程共享的。二者之間最大的區別就是存儲的內容不同:堆中主要存放對象實例。棧(局部變量表)中主要存放各種基本數據類型、對象的引用。

從作用來說,棧是運行時的單位,而堆是存儲的單位。棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。在 Java 中一個線程就會相應有一個線程棧與之對應,因爲不同的線程執行邏輯有所不同,因此需要一個獨立的線程棧。而堆則是所有線程共享的。棧因爲是運行單位,因此裏面存儲的信息都是跟當前線程(或程序)相關信息的。包括局部變量、程序運行狀態、方法返回值等等;而堆只負責存儲對象信息。

4、堆中存什麼?棧中存什麼?

堆中存的是對象。棧中存的是基本數據類型和堆中對象的引用。一個對象的大小是不可估計的,或者說是可以動態變化的,但是在棧中,一個對象只對應了一個 4btye 的引用(堆棧分離的好處)。

爲什麼不把基本類型放堆中呢?

因爲基本數據類型佔用的空間一般是1~8個字節,需要空間比較少,而且因爲是基本類型,所以不會出現動態增長的情況,長度固定,因此棧中存儲就夠了。如果把它存在堆中是沒有什麼意義的。基本類型和對象的引用都是存放在棧中,而且都是幾個字節的一個數,因此在程序運行時,它們的處理方式是統一的。但是基本類型、對象引用和對象本身就有所區別了,因爲一個是棧中的數據一個是堆中的數據。最常見的一個問題就是,Java 中參數傳遞時的問題。

5、 爲什麼要把堆和棧區分出來呢?棧中不是也可以存儲數據嗎?

1. 從軟件設計的角度看,棧代表了處理邏輯,而堆代表了數據。這樣分開,使得處理邏輯更爲清晰。分而治之的思想。這種隔離、模塊化的思想在軟件設計的方方面面都有體現。

2. 堆與棧的分離,使得堆中的內容可以被多個棧共享(也可以理解爲多個線程訪問同一個對象)。這種共享的收益是很多的。一方面這種共享提供了一種有效的數據交互方式(如:共享內存),另一方面,堆中的共享常量和緩存可以被所有棧訪問,節省了空間。

3. 棧因爲運行時的需要,比如:保存系統運行的上下文,需要進行地址段的劃分。由於棧只能向上增長,因此就會限制住棧存儲內容的能力。而堆不同,堆中的對象是可以根據需要動態增長的,因此棧和堆的拆分,使得動態增長成爲可能,相應棧中只需記錄堆中的一個地址即可。

6、Java 中的參數傳遞時傳值呢?還是傳引用?

要說明這個問題,先要明確兩點:

1. 不要試圖與 C 進行類比,Java 中沒有指針的概念。

2. 程序運行永遠都是在棧中進行的,因而參數傳遞時,只存在傳遞基本類型和對象引用的問題。不會直接傳對象本身。

 Java 在方法調用傳遞參數時,因爲沒有指針,所以它都是進行傳值調用。但是傳引用的錯覺是如何造成的呢?在運行棧中,基本類型和引用的處理是一樣的,都是傳值。所以,如果是傳引用的方法調用,也同時可以理解爲“傳引用值”的傳值調用,即引用的處理跟基本類型是完全一樣的。但是當進入被調用方法時,被傳遞的這個引用的值,被程序解釋到堆中的對象,這個時候纔對應到真正的對象。如果此時進行修改,修改的是引用對應的對象,而不是引用本身,即:修改的是堆中的數據。所以這個修改是可以保持的了。

對象,從某種意義上說,是由基本類型組成的。可以把一個對象看作爲一棵樹,對象的屬性如果還是對象,則還是一顆樹(即非葉子節點),基本類型則爲樹的葉子節點。程序參數傳遞時,被傳遞的值本身都是不能進行修改的,但是,如果這個值是一個非葉子節點(即一個對象引用),則可以修改這個節點下面的所有內容。

7、Java 對象的大小是怎麼計算的?

基本數據的類型的大小是固定的。對於非基本類型的 Java 對象,其大小就值得商榷。在 Java 中,一個空 Object 對象的大小是 8 byte,這個大小隻是保存堆中一個沒有任何屬性的對象的大小。看下面語句:

Object ob = new Object();

這樣在程序中完成了一個 Java 對象的生命,但是它所佔的空間爲:4 byte + 8 byte。4 byte 是上面部分所說的 Java 棧中保存引用的所需要的空間。而那 8 byte 則是 Java 堆中對象的信息。因爲所有的 Java 非基本類型的對象都需要默認繼承 Object 對象,因此不論什麼樣的 Java 對象,其大小都必須是大於 8 byte。有了 Object 對象的大小,我們就可以計算其他對象的大小了。

Class MaNong { 
    int count;
    boolean flag;
    Object obj; 
}

MaNong 的大小爲:空對象大小(8 byte) + int 大小(4 byte) + Boolean 大小(1 byte) + 空 Object 引用的大小(4 byte) = 17byte。但是因爲 Java 在對對象內存分配時都是以 8 的整數倍來分,因此大於 17 byte 的最接近 8 的整數倍的是 24,因此此對象的大小爲 24 byte。

這裏需要注意一下基本類型的包裝類型的大小。因爲這種包裝類型已經成爲對象了,因此需要把它們作爲對象來看待。包裝類型的大小至少是12 byte(聲明一個空 Object 至少需要的空間),而且 12 byte 沒有包含任何有效信息,同時,因爲 Java 對象大小是 8 的整數倍,因此一個基本類型包裝類的大小至少是 16 byte。這個內存佔用是很恐怖的,它是使用基本類型的 N 倍(N > 2),有些類型的內存佔用更是誇張(隨便想下就知道了)。因此,可能的話應儘量少使用包裝類。在 JDK5 以後,因爲加入了自動類型裝換,因此,Java 虛擬機會在存儲方面進行相應的優化。

8、對象的訪問定位的兩種方式?

Java 程序通過棧上的引用數據來操作堆上的具體對象。目前主流的對象訪問方式有:句柄 和 直接指針。

  • 1. 使用句柄

如果使用句柄的話,那麼 Java 堆中將會劃分出一塊內存來作爲句柄池,引用中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。

  • 2. 直接指針

如果使用直接指針訪問,那麼 Java 堆對象的佈局中就必須考慮如何防止訪問類型數據的相關信息,reference 中存儲的直接就是對象的地址。

  • 3. 各自的優點

1. 使用句柄來訪問的最大好處是引用中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而引用本身不需要修改;

2. 使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。

9、判斷垃圾可以回收的方法有哪些?

垃圾收集器在對堆區和方法區進行回收前,首先要確定這些區域的對象哪些可以被回收,哪些暫時還不能回收,這就要用到判斷對象是否存活的算法。

  • 1. 引用計數法

  • 基本思想

引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每個對象實例都有一個引用計數。當一個對象被創建時,就將該對象實例分配給一個變量,該變量計數設置爲 1。當任何其它變量被賦值爲這個對象的引用時,計數加1(a = b,則 b 引用的對象實例的計數器加 1),但當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數器減 1。任何引用計數器爲 0 的對象實例可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減 1。

  • 優缺點

優點:引用計數收集器可以很快的執行,交織在程序運行中。對程序需要不被長時間打斷的實時環境比較有利。

缺點:無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能爲 0。

循環引用

public class Demo{
    public static void main(String[]   args){
        MyObject  object1 = new  MyObject();
        MyObject  object2 = new  MyObject();
        
        object1.object = object2;
        object2.object = object1;
        object1 = null;
        object2 = null;
    }
}
​
class   MyObject{
   MyObject    object;
}

這段代碼是用來驗證引用計數算法不能檢測出循環引用。最後面兩句將 object1 和 object2 賦值爲null,也就是說 object1 和 object2 指向的對象已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數器都不爲 0,那麼垃圾收集器就永遠不會回收它們。

  • 2. 可達性分析算法

可達性分析算法是從離散數學中的圖論引入的,程序把所有的引用關係看作一張圖,從一個節點 GC ROOT 開始,尋找對應的引用節點,找到這個節點以後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之後,剩餘的節點則被認爲是沒有被引用到的節點,即無用的節點,無用的節點將會被判定爲是可回收的對象。

 

在 Java 語言中,可作爲 GC Roots 的對象包括下面幾種: 

  1. 虛擬機棧中引用的對象(棧幀中的本地變量表);  

  2. 方法區中類靜態屬性引用的對象;  

  3. 方法區中常量引用的對象;  

  4. 本地方法棧中 JNI(Native方法)引用的對象。

10、垃圾回收是從哪裏開始的呢?

查找哪些對象是正在被當前系統使用的。上面分析的堆和棧的區別,其中棧是真正進行程序執行地方,所以要獲取哪些對象正在被使用,則需要從 Java 棧開始。同時,一個棧是與一個線程對應的,因此,如果有多個線程的話,則必須對這些線程對應的所有的棧進行檢查。

同時,除了棧外,還有系統運行時的寄存器等,也是存儲程序運行數據的。這樣,以棧或寄存器中的引用爲起點,我們可以找到堆中的對象,又從這些對象找到對堆中其他對象的引用,這種引用逐步擴展,最終以 null 引用或者基本類型結束,這樣就形成了一顆以 Java 棧中引用所對應的對象爲根節點的一顆對象樹。如果棧中有多個引用,則最終會形成多顆對象樹。在這些對象樹上的對象,都是當前系統運行所需要的對象,不能被垃圾回收。而其他剩餘對象,則可以視爲無法被引用到的對象,可以被當做垃圾進行回收。

11、被標記爲垃圾的對象一定會被回收嗎?

即使在可達性分析算法中不可達的對象,也並非是“非死不可”,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程。  

第一次標記:如果對象在進行可達性分析後發現沒有與 GC Roots 相連接的引用鏈,那它將會被第一次標記;  

第二次標記:第一次標記後接着會進行一次篩選,篩選的條件是此對象是否有必要執行 finalize() 方法。在 finalize() 方法中沒有重新與引用鏈建立關聯關係的,將被進行第二次標記。第二次標記成功的對象將真的會被回收,如果對象在 finalize() 方法中重新與引用鏈建立了關聯關係,那麼將會逃離本次回收,繼續存活。

12、談談對 Java 中引用的瞭解?

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象的引用鏈是否可達,判定對象是否存活都與“引用”有關。在Java語言中,將引用又分爲強引用、軟引用、弱引用、虛引用 4 種,這四種引用強度依次逐漸減弱。

  • 1. 強引用  

在程序代碼中普遍存在的,類似 Object obj = new Object() 這類引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

  • 2. 軟引用

用來描述一些還有用但並非必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收後還沒有足夠的內

  • 3. 弱引用

也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

  • 4. 虛引用

也叫幽靈引用或幻影引用,是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。它的作用是能在這個對象被收集器回收時收到一個系統通知。  

13、談談對內存泄漏的理解?

  • 內存泄露的基本概念

在 Java 中,內存泄漏就是存在一些不會再被使用確沒有被回收的對象,這些對象有下面兩個特點:

1. 這些對象是可達的,即在有向圖中,存在通路可以與其相連;

2. 這些對象是無用的,即程序以後不會再使用這些對象。

如果對象滿足這兩個條件,這些對象就可以判定爲 Java 中的內存泄漏,這些對象不會被 GC 所回收,然而它卻佔用內存。

14、內存泄露的根本原因是什麼?

長生命週期的對象持有短生命週期對象的引用就很可能發生內存泄漏,儘管短生命週期對象已經不再需要,但是因爲長生命週期持有它的引用而導致不能被回收,這就是 Java 中內存泄漏的發生場景。

15、舉幾個可能發生內存泄漏的情況?

1. 靜態集合類引起的內存泄漏;

2. 當集合裏面的對象屬性被修改後,再調用 remove() 方法時不起作用;

3. 監聽器:釋放對象的時候沒有刪除監聽器;

4. 各種連接:比如數據庫連接(dataSourse.getConnection()),網絡連接(socket) 和 IO 連接,除非其顯式的調用了其 close() 方法將其連接關閉,否則是不會自動被 GC 回收的;

5. 內部類:內部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導致一系列的後繼類對象沒有釋放;

6. 單例模式:單例對象在初始化後將在 JVM 的整個生命週期中存在(以靜態變量的方式),如果單例對象持有外部的引用,那麼這個對象將不能被 JVM 正常回收,導致內存泄漏。

16、儘量避免內存泄漏的方法?

1. 儘量不要使用 static 成員變量,減少生命週期;

2. 及時關閉資源;

3. 不用的對象,可以手動設置爲 null。

17、常用的垃圾收集算法有哪些?

  • 1. 標記-清除算法(Mark-Sweep)

標記-清除算法採用從根集合(GC Roots)進行掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未被標記的對象,進行回收。標記-清除算法不需要進行對象的移動,只需對不存活的對象進行處理,在存活對象比較多的情況下極爲高效,但由於標記-清除算法直接回收不存活的對象,因此會造成內存碎片。

  • 2. 複製算法(Copying)  

複製算法的提出是爲了克服句柄的開銷和解決內存碎片的問題。它開始時把堆分成 一個對象面和多個空閒面, 程序從對象面爲對象分配空間,當對象滿了,基於 copying 算法的垃圾收集就從根集合(GC Roots)中掃描活動對象,並將每個活動對象複製到空閒面(使得活動對象所佔的內存之間沒有空閒洞),這樣空閒面變成了對象面,原來的對象面變成了空閒面,程序會在新的對象面中分配內存。

  • 3. 標記-整理算法(Mark-compact)  

標記-整理算法採用標記-清除算法一樣的方式進行對象的標記,但在清除時不同,在回收不存活的對象佔用的空間後,會將所有的存活對象往左端空閒空間移動,並更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,因此成本更高,但是卻解決了內存碎片的問題。

  • 4. 分代收集算法

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

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

18、爲什麼要採用分代收集算法?

分代的垃圾回收策略,是基於這樣一個事實:不同的對象的生命週期是不一樣的。因此,不同生命週期的對象可以採取不同的收集方式,以便提高回收效率。

在 Java 程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如 Http 請求中的 Session 對象、線程、Socket 連接,這類對象跟業務直接掛鉤,因此生命週期比較長。但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命週期會比較短,比如:String 對象,由於其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。

在不進行對象存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因爲每次回收都需要遍歷所有存活對象,但實際上,對於生命週期長的對象而言,這種遍歷是沒有效果的,因爲可能進行了很多次遍歷,但是他們依舊存在。因此,分代垃圾回收採用分治的思想,進行代的劃分,把不同生命週期的對象放在不同代上,不同代上採用最適合它的垃圾回收方式進行回收。

19、分代收集下的年輕代和老年代應該採用什麼樣的垃圾回收算法?

  • 1. 年輕代(Young Generation)的回收算法 (主要以 Copying 爲主) 

1. 所有新生成的對象首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的對象。

2. 新生代內存按照 8:1:1 的比例分爲一個 eden 區和兩個 survivor(survivor0、 survivor1)區。大部分對象在 Eden 區中生成。回收時先將 Eden 區存活對象複製到一個 survivor0 區,然後清空 eden 區,當這個 survivor0 區也存放滿了時,則將 eden 區和 survivor0 區存活對象複製到另一個 survivor1 區,然後清空 eden 區 和這個 survivor0 區,此時 survivor0 區是空的,然後將survivor0 區和 survivor1 區交換,即保持 survivor1 區爲空, 如此往復。

3. 當 survivor1 區不足以存放 Eden 區 和 survivor0區 的存活對象時,就將存活對象直接存放到老年代。若是老年代也滿了就會觸發一次Full GC(Major GC),也就是新生代、老年代都進行回收。

4、新生代發生的 GC 也叫做 Minor GC,MinorGC 發生頻率比較高(不一定等 Eden 區滿了才觸發)。

  • 2. 年老代(Old Generation)的回收算法(主要以 Mark-Compact 爲主)

1. 在年輕代中經歷了 N 次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認爲年老代中存放的都是一些生命週期較長的對象。

2. 內存比新生代也大很多(大概比例是1 : 2),當老年代內存滿時觸發 Major GC 即 Full GC,Full GC 發生頻率比較低,老年代對象存活時間比較長,存活率標記高。

20、什麼是浮動垃圾?

由於在應用運行的同時進行垃圾回收,所以有些垃圾可能在垃圾回收進行完成時產生,這樣就造成了“Floating Garbage”,這些垃圾需要在下次垃圾回收週期時才能回收掉。所以,併發收集器一般需要20%的預留空間用於這些浮動垃圾。

21、什麼是內存碎片?如何解決?

由於不同 Java 對象存活時間是不一定的,因此,在程序運行一段時間以後,如果不進行內存整理,就會出現零散的內存碎片。碎片最直接的問題就是會導致無法分配大塊的內存空間,以及程序運行效率降低。所以,在上面提到的基本垃圾回收算法中,“複製”方式和“標記-整理”方式,都可以解決碎片的問題。

22、常用的垃圾收集器有哪些?

  • 1. Serial 收集器(複製算法)

新生代單線程收集器,標記和清理都是單線程,優點是簡單高效。是 client 級別默認的 GC 方式,可以通過 -XX:+UseSerialGC 來強制指定。

  • 2. Serial Old 收集器(標記-整理算法)

老年代單線程收集器,Serial 收集器的老年代版本。

  • 3. ParNew 收集器(停止-複製算法)

新生代收集器,可以認爲是 Serial 收集器的多線程版本,在多核 CPU 環境下有着比 Serial 更好的表現。

  • 4. Parallel Scavenge 收集器(停止-複製算法)

並行收集器,追求高吞吐量,高效利用 CPU。吞吐量一般爲 99%, 吞吐量= 用戶線程時間 / (用戶線程時間+GC線程時間)。適合後臺應用等對交互相應要求不高的場景。是 server 級別默認採用的GC方式,可用 -XX:+UseParallelGC 來強制指定,用 -XX:ParallelGCThreads=4 來指定線程數。

  • 5. Parallel Old 收集器(停止-複製算法)

Parallel Old 收集器的老年代版本,並行收集器,吞吐量優先。

  • 6. CMS(Concurrent Mark Sweep)收集器(標記-清除算法)

高併發、低停頓,追求最短 GC 回收停頓時間,cpu 佔用比較高,響應時間快,停頓時間短,多核 cpu 追求高響應時間的選擇。

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

CMS 使用的是標記-清除的算法實現的,所以在 GC 的時候會產生大量的內存碎片,當剩餘內存不能滿足程序運行要求時,系統將會出現 Concurrent Mode Failure,臨時 CMS 會採用 Serial Old 回收器進行垃圾清除,此時的性能將會被降低。

  • 7. G1 

G1 收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First的由來。

23、談談你對 CMS 垃圾收集器的理解?

CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量爲代價來獲得最短回收停頓時間的垃圾回收器。是使用標記清除算法實現的,整個過程分爲四步:

1. 初始標記:記錄下直接與 root 相連的對象,暫停所有的其他線程,速度很快;

2. 併發標記:同時開啓 GC 和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達對象。因爲用戶線程可能會不斷的更新引用域,所以 GC 線程無法保證可達性分析的實時性。所以這個算法裏會跟蹤記錄這些發生引用更新的地方。

3. 重新標記:重新標記階段就是爲了修正併發標記期間因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄。【這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短】;

4. 併發清除:開啓用戶線程,同時 GC 線程開始對爲標記的區域做清掃。

  • CMS 的優缺點:

主要優點:併發收集、低停頓;

主要缺點:對 CPU 資源敏感、無法處理浮動垃圾、它使用的回收算法“標記-清除”算法會導致收集結束時會有大量空間碎片產生。 

24、談談你對 G1 收集器的理解?

垃圾回收的瓶頸

傳統分代垃圾回收方式,已經在一定程度上把垃圾回收給應用帶來的負擔降到了最小,把應用的吞吐量推到了一個極限。但是他無法解決的一個問題,就是 Full GC 所帶來的應用暫停。在一些對實時性要求很高的應用場景下,GC 暫停所帶來的請求堆積和請求失敗是無法接受的。這類應用可能要求請求的返回時間在幾百甚至幾十毫秒以內,如果分代垃圾回收方式要達到這個指標,只能把最大堆的設置限制在一個相對較小範圍內,但是這樣有限制了應用本身的處理能力,同樣也是不可接受的。

分代垃圾回收方式確實也考慮了實時性要求而提供了併發回收器,支持最大暫停時間的設置,但是受限於分代垃圾回收的內存劃分模型,其效果也不是很理想。

G1 可謂博採衆家之長,力求到達一種完美。它吸取了增量收集優點,把整個堆劃分爲一個一個等大小的區域(region)。內存的回收和劃分都以region爲單位;同時,它也吸取了 CMS 的特點,把這個垃圾回收過程分爲幾個階段,分散一個垃圾回收過程;而且,G1 也認同分代垃圾回收的思想,認爲不同對象的生命週期不同,可以採取不同收集方式,因此,它也支持分代的垃圾回收。爲了達到對回收時間的可預計性,G1 在掃描了 region 以後,對其中的活躍對象的大小進行排序,首先會收集那些活躍對象小的 region,以便快速回收空間(要複製的活躍對象少了),因爲活躍對象小,裏面可以認爲多數都是垃圾,所以這種方式被稱爲 Garbage First(G1)的垃圾回收算法,即:垃圾優先的回收。

25、說下你對垃圾回收策略的理解/垃圾回收時機?

  • 1. Minor / Scavenge GC 

所有對象創建在新生代的 Eden 區,當 Eden 區滿後觸發新生代的 Minor GC,將 Eden 區和非空閒 Survivor 區存活的對象複製到另外一個空閒的 Survivor 區中。保證一個 Survivor 區是空的,新生代 Minor GC 就是在兩個 Survivor 區之間相互複製存活對象,直到 Survivor 區滿爲止。

Minor/Scavenge 這種方式的 GC 是在年輕代的 Eden 區進行,不會影響到年老代。因爲大部分對象都是從 Eden 區開始的,同時 Eden 區不會分配的很大,所以 Eden 區的 GC 會頻繁進行。因而,一般在這裏需要使用速度快、效率高的算法,使 Eden 去能儘快空閒出來。

  • 2. Full GC 

對整個堆進行整理,包括 Young、Tenured 和 Perm。Full GC 因爲需要對整個堆進行回收,所以比 Minor GC 要慢,因此應該儘可能減少 Full GC 的次數。在對 JVM 調優的過程中,很大一部分工作就是對於 Full GC 的調節。

Minor 有如下原因可能導致 Full GC:

1. 調用 System.gc(),會建議虛擬機執行 Full GC。只是建議虛擬機執行 Full GC,但是虛擬機不一定真正去執行。

2. 老年代空間不足,原因:老年代空間不足的常見場景爲大對象直接進入老年代、長期存活的對象進入老年代等。爲了避免以上原因引起的 Full GC,應當儘量不要創建過大的對象以及數組。除此之外,可以通過 -Xmn 虛擬機參數調大新生代的大小,讓對象儘量在新生代被回收掉,不進入老年代。還可以通過 -XX:MaxTenuringThreshold 調大對象進入老年代的年齡,讓對象在新生代多存活一段時間;

3. 空間分配擔保失敗:使用複製算法的 Minor GC 需要老年代的內存空間作擔保,如果擔保失敗會執行一次 Full GC;

4. JDK 1.7 及以前的永久代空間不足。在 JDK1.7 及以前,HotSpot 虛擬機中的方法區是用永久代實現的,永久代中存放的爲一些 Class 的信息、常量、靜態變量等數據。當系統中要加載的類、反射的類和調用的方法較多時,永久代可能會被佔滿,在未配置爲採用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,那麼虛擬機會拋出 java.lang.OutOfMemoryError。爲避免以上原因引起的 Full GC,可採用的方法爲增大永久代空間或轉爲使用 CMS GC。

5. Concurrent Mode Failure 執行 CMS GC 的過程中,同時有對象要放入老年代,而此時老年代空間不足(可能是 GC 過程中浮動垃圾過多導致暫時性的空間不足),便會報 Concurrent Mode Failure 錯誤,並觸發 Full GC。

26、談談你對內存分配的理解?大對象怎麼分配?空間分配擔保?

1. 對象優先在 Eden 區分配:大多數情況下,對象在新生代 Eden 區分配,當 Eden 區空間不夠時,發起 Minor GC。

2. 大對象直接進入老年代:大對象是指需要連續內存空間的對象,最典型的大對象是那種很長的字符串以及數組。經常出現大對象會提前觸發垃圾收集以獲取足夠的連續空間分配給大對象。-XX:PretenureSizeThreshold,大於此值的對象直接在老年代分配,避免在 Eden 區和 Survivor 區之間的大量內存複製。

3. 長期存活的對象將進入老年代:爲對象定義年齡計數器,對象在 Eden 出生並經過 Minor GC 依然存活,將移動到 Survivor 中,年齡就增加 1 歲,增加到一定年齡則移動到老年代中。-XX:MaxTenuringThreshold 用來定義年齡的閾值。

4、動態對象年齡判定:爲了更好的適應不同程序的內存情況,虛擬機不是永遠要求對象年齡必須達到了某個值才能進入老年代,如果 Survivor 空間中相同年齡所有對象大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無需達到要求的年齡。

5. 空間分配擔保

(1)在發生 Minor GC 之前,虛擬機先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果條件成立的話,那麼 Minor GC 可以確認是安全的;

(2)如果不成立的話,虛擬機會查看 HandlePromotionFailure 設置值是否允許擔保失敗,如果允許那麼就會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次 Minor GC;如果小於,或者 HandlePromotionFailure 設置不允許冒險,那麼就要進行一次 Full GC。

27、說下你用過的 JVM 監控工具?

1. jvisualvm:虛擬機監視和故障處理平臺

2. jps :查看當前 Java 進程

3. jstat:顯示虛擬機運行數據

4. jmap:內存監控

5. jhat:分析 heapdump 文件

6. jstack:線程快照

7. jinfo:虛擬機配置信息

28、如何利用監控工具調優

  • 1. 堆信息查看

1. 可查看堆空間大小分配(年輕代、年老代、持久代分配)

2. 提供即時的垃圾回收功能

3. 垃圾監控(長時間監控回收情況)

4. 查看堆內類、對象信息查看:數量、類型等

5. 對象引用情況查看

  • 有了堆信息查看方面的功能,我們一般可以順利解決以下問題:

1. 年老代年輕代大小劃分是否合理

2. 內存泄漏垃

3. 圾回收算法設置是否合理

  • 2. 線程監控

線程信息監控:系統線程數量

線程狀態監控:各個線程都處在什麼樣的狀態下

Dump 線程詳細信息:查看線程內部運行情況 

死鎖檢查

  • 3. 熱點分析

1. CPU 熱點:檢查系統哪些方法佔用的大量 CPU 時間;

2. 內存熱點:檢查哪些對象在系統中數量最大(一定時間內存活對象和銷燬對象一起統計)這兩個東西對於系統優化很有幫助。我們可以根據找到的熱點,有針對性的進行系統的瓶頸查找和進行系統優化,而不是漫無目的的進行所有代碼的優化。

  • 4. 快照

快照是系統運行到某一時刻的一個定格。在我們進行調優的時候,不可能用眼睛去跟蹤所有系統變化,依賴快照功能,我們就可以進行系統兩個不同運行時刻,對象(或類、線程等)的不同,以便快速找到問題。

舉例說,我要檢查系統進行垃圾回收以後,是否還有該收回的對象被遺漏下來的了。那麼,我可以在進行垃圾回收前後,分別進行一次堆情況的快照,然後對比兩次快照的對象情況。

  • 5. 內存泄露檢查

內存泄漏是比較常見的問題,而且解決方法也比較通用,這裏可以重點說一下,而線程、熱點方面的問題則是具體問題具體分析了。

內存泄漏一般可以理解爲系統資源(各方面的資源,堆、棧、線程等)在錯誤使用的情況下,導致使用完畢的資源無法回收(或沒有回收),從而導致新的資源分配請求無法完成,引起系統錯誤。內存泄漏對系統危害比較大,因爲它可以直接導致系統的崩潰。

29、JVM 的一些參數?

  • 1. 堆設置

-Xms:初始堆大小

-Xmx:最大堆大小

-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:設置持久代大小

  • 2. 收集器設置

-XX:+UseSerialGC:設置串行收集器

-XX:+UseParallelGC:設置並行收集器

-XX:+UseParalledlOldGC:設置並行年老代收集器

-XX:+UseConcMarkSweepGC:設置併發收集器

  • 3. 垃圾回收統計信息

-XX:+PrintGC:開啓打印 gc 信息

-XX:+PrintGCDetails:打印 gc 詳細信息

-XX:+PrintGCTimeStamps

-Xloggc:filename

  • 4. 並行收集器設置

-XX:ParallelGCThreads=n:設置並行收集器收集時使用的 CPU 數

-XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間

-XX:GCTimeRatio=n:設置垃圾回收時間佔程序運行時間的百分比

  • 5. 併發收集器設置

-XX:+CMSIncrementalMode:設置爲增量模式。適用於單 CPU 情況

-XX:ParallelGCThreads=n:設置併發收集器年輕代收集方式爲並行收集時,使用的 CPU 數。並行收集線程數

30、談談你對類文件結構的理解?有哪些部分組成?

Class 文件結構如下標所示:

Class 文件沒有任何分隔符,嚴格按照上面結構表中的順序排列。無論是順序還是數量,甚至於數據存儲的字節序這樣的細節,都是被嚴格限定的,哪個字節代表什麼含義,長度是多少,先後順序如何,都不允許改變。

1. 魔數(magic):每個 Class 文件的頭 4 個字節稱爲魔數(Magic  Number),它的唯一作用是確定這個文件是否爲一個能被虛擬機接受的Class 文件,即判斷這個文件是否符合 Class 文件規範。

2. 文件的版本:minor_version 和 major_version。

3. 常量池:constant_pool_count 和 constant_pool:常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic  References)。

4. 訪問標誌:access_flags:用於識別一些類或者接口層次的訪問信息。包括:這個 Class 是類還是接口、是否定義了 Public 類型、是否定義爲 abstract 類型、如果是類,是否被聲明爲了 final 等等。

5.類索引、父類索引與接口索引集合:this_class、super_class和interfaces。

6. 字段表集合:field_info、fields_count:字段表(field_info)用於描述接口或者類中聲明的變量;fields_count 字段數目:表示Class文件的類和實例變量總數。

7. 方法表集合:methods、methods_count

8. 屬性表集合:attributes、attributes_count

31、談談你對類加載機制的瞭解?

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

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用、卸載 7 個階段。其中驗證、準備、解析 3 個部分統稱爲連接,這7個階段發生的順序如下圖所示:

32、類加載各階段的作用分別是什麼?

  • 1. 加載

在加載階段,虛擬機需要完成以下三件事情:

1. 通過一個類的全限定名來獲取定義此類的二進制字節流;

2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構;

3. 在內存中生成一個代表這個類的 java.lang.Class 對象,作爲方法區這個類的各種數據的訪問接口。

  • 2. 驗證

主要是爲了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段大致上分爲 4 個階段的檢驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。

1. 文件格式校驗:驗證字節流是否符合 class 文件的規範,並且能被當前版本的虛擬機處理。只有通過這個階段的驗證後,字節流纔會進入內存的方法區進行存儲,所以後面的3個階段的全部是基於方法區的存儲結構進行的,不會再直接操作字節流;

2. 元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合 Java 語言規範的要求。目的是保證不存在不符合 Java 語言規範的元數據信息;

3. 字節碼驗證:該階段主要工作是進行數據流和控制流分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行爲;

4. 符號引用驗證:最後一個階段的校驗發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在連接的第三個階段——解析階段中發生。符號引用驗證的目的是確保解析動作能正常執行。

  • 3. 準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配**。這時候進行內存分配的僅包括類變量(被 static 修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在 Java 堆中。實例化不是類加載的一個過程,類加載發生在所有實例化操作之前,並且類加載只進行一次,實例化可以進行多次。

初始值是默認值 0 或 false 或 null。如果類變量是常量(final),那麼會按照表達式來進行初始化,而不是賦值爲 0。public static final int value = 123;

  • 4. 解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。

  • 5. 初始化

在準備階段,變量已經賦過一次系統要求的初始值了,而在初始化階段,則根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器 <clinit>() 方法的過程。

33、有哪些類加載器?分別有什麼作用?

1. 啓動類加載器(Bootstrap  ClassLoader):這個類加載器是由 C++ 語言實現的,是虛擬機自身的一部分。負責將存在 <JAVA_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的類庫加載到虛擬機內存中。啓動內加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給啓動類加載器,直接使用 null 即可;

2. 其他類加載器:由 Java 語言實現,獨立於虛擬機外部,並且全都繼承自抽象類 java.lang.ClassLoader。如擴展類加載器和應用程序類加載器:

(1)擴展類加載器(Extension  ClassLoader):這個類加載器由sun.misc.Launcher$ExtClassLoader 實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被 java.ext.dirs 系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。

(2)應用程序類加載器 (Application  ClassLoader):這個類加載器由 sun.misc.Launcher$AppClassLoder 實現。由於個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也稱之爲系統類加載器。它負責加載用戶路徑(ClassPath)所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

34、類與類加載器的關係?

類加載器雖然只用於實現類的加載動作,但它在 Java 程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在 Java 虛擬機中的唯一性,每個類加載器,都擁有一個獨立的類名稱空間。換句話說:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個 Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那麼這兩個類就必定不相等。

35、談談你對雙親委派模型的理解?工作過程?爲什麼要使用?

應用程序一般是由上訴的三種類加載器相互配合進行加載的,如果有必要,還可以加入自己定義的類加載器,它們的關係如下圖所示:

  • 雙親委派模型的工作過程:

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

  • 使用雙親委派模型的好處:

Java 類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如:類 java.lang.Object,它存放在 rt.jar 中,無論哪一個類加載器需要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,因此 Object 類在程序的各種類加載器環境中都是同一個類(使用的是同一個類加載器加載的)。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個 java.lang.Object 類,並放在程序的 ClassPath 中,那麼系統將會出現多個不同的 Object 類,Java 類型體系中最基礎的行爲也就無法保證,應用程序也將變得一片混亂。

  • 雙親委派模型的主要代碼實現:

實現雙親委派的代碼都集中在 java.lang.ClassLoader 的 loadClass() 方法中,邏輯清晰易懂:先檢查是否已經被加載過,若沒有加載則調用父加載器的 loadClass() 方法,若父加載器爲空則默認使用啓動類加載器作爲父類加載器。如果父類加載失敗,拋出 ClassNotFoundException 異常後,再調用自己的 findClass() 方法進行加載。

36、怎麼實現一個自定義的類加載器?需要注意什麼?

若要實現自定義類加載器,只需要繼承  java.lang.ClassLoader 類,並且重寫其 findClass() 方法即可。

37、怎麼打破雙親委派模型?

1. 自己寫一個類加載器;

2. 重寫 loadClass() 方法

3. 重寫 findClass() 方法

這裏最主要的是重寫 loadClass 方法,因爲雙親委派機制的實現都是通過這個方法實現的,先找父加載器進行加載,如果父加載器無法加載再由自己來進行加載,源碼裏會直接找到根加載器,重寫了這個方法以後就能自己定義加載的方式了。

38、有哪些實際場景是需要打破雙親委派模型的?

 JNDI 服務,它的代碼由啓動類加載器去加載,但 JNDI 的目的就是對資源進行集中管理和查找,它需要調用獨立廠商實現部部署在應用程序的 classpath 下的 JNDI 接口提供者(SPI, Service Provider Interface) 的代碼,但啓動類加載器不可能“認識”之些代碼,該怎麼辦? 

爲了解決這個困境,Java 設計團隊只好引入了一個不太優雅的設計:**線程上下文件類加載器(Thread Context ClassLoader)。這個類加載器可以通過 java.lang.Thread 類的 setContextClassLoader() 方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個;如果在應用程序的全局範圍內都沒有設置過,那麼這個類加載器默認就是應用程序類加載器。有了線程上下文類加載器,JNDI 服務使用這個線程上下文類加載器去加載所需要的 SPI 代碼,也就是父類加載器請求子類加載器去完成類加載動作,這種行爲實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型,但這也是無可奈何的事情。Java 中所有涉及 SPI 的加載動作基本上都採用這種方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。 

39、談談你對編譯期優化和運行期優化的理解?

  • 編譯期優化:

1. 解析與填充符號表的過程

2. 插入式註解處理器的註解處理過程

3. 分析與字節碼生成過程

  • 編譯優化:

1. 方法內聯

2. 公共子表達式消除

3. 數組範圍檢查消除

4. 逃逸分析

40、爲何 HotSpot 虛擬機要使用解釋器與編譯器並存的架構?

 

解釋器:程序可以迅速啓動和執行,消耗內存小 (類似人工,成本低,到後期效率低);

編譯器:隨着代碼頻繁執行會將代碼編譯成本地機器碼  (類似機器,成本高,到後期效率高)。

在整個虛擬機執行架構中,解釋器與編譯器經常配合工作,兩者各有優勢:當程序需要迅速啓動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行。在程序運行後,隨着時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之後,可以獲取更高的執行效率。當程序運行環境中內存資源限制較大(如部分嵌入式系統),可以使用解釋執行節約內存,反之可以使用編譯執行來提升效率。

解釋執行可以節約內存,而編譯執行可以提升效率。因此,在整個虛擬機執行架構中,解釋器與編譯器經常配合工作。

41、說下你對 Java 內存模型的理解?

處理器和內存不是同數量級,所以需要在中間建立中間層,也就是高速緩存,這會引出緩存一致性問題。在多處理器系統中,每個處理器都有自己的高速緩存,而它們又共享同一主內存(Main Memory),有可能操作同一位置引起各自緩存不一致,這時候需要約定協議在保證一致性。

 Java 內存模型(Java  Memory  Model,JMM):屏蔽掉了各種硬件和操作系統的內存訪問差異,以實現讓 Java 程序在各種平臺下都能達到一致性的內存訪問效果。

  • 主內存與工作內存

Java 內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。

Java 內存模型規定了所有的變量都存儲在主內存(Main Memory)中,每個線程有自己的工作線程(Working Memory),保存主內存副本拷貝和自己私有變量,不同線程不能訪問工作內存中的變量。線程間變量值的傳遞需要通過主內存來完成。

42、內存間的交互操作有哪些?需要滿足什麼規則?

關於主內存與工作內存之間的具體的交互協議,即:一個變量如何從主內存拷貝到工作內存、如何從工作內存同步主內存之類的實現細節,Java內存模型中定義一下八種操作來完成:

1. lock(鎖定):作用於主內存的變量。它把一個變量標誌爲一個線程獨佔的狀態;

2. unlock(解鎖):作用於主內存的變量,它把處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定;

3. read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用;

4. load(載入):作用於工作內存的變量,它把read操作從主內存中得到變量值放入工作內存的變量的副本中;

5. use(使用):作用於工作內存的變量, 它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作;

6. assign(賦值):作用於工作內存的變量。它把一個從執行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到需要給一個變量賦值的字節碼時執行這個操作;

7. store(存儲):作用於工作內存的變量。它把一個工作內存中一個變量的值傳遞到主內存中,以便隨後的write操作使用;

8. write(寫入):作用於主內存的變量。它把store操作從工作內存中得到的變量的值放入主內存的變量中。

如果要把一個變量從工作內存複製到工作內存,那就要按順序執行 read 和 load 操作,如果要把變量從工作內存同步回主內存,就要按順序執行 store 和 write 操作。

  • 上訴 8 種基本操作必須滿足的規則:

1. 不允許 read 和 load、store 和 write 操作之一單獨出現;

2. 不允許一個線程丟棄它的最近的 assign 操作,即變量在工作內存中改變之後必須把該變化同步回主內存;

3. 不允許一個線程無原因地(沒有發生過任何 assign 操作)把數據從線程的工作內存同步回主內存中;

4. 一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load 或 assign)的變量,換句話說就是對一個變量實施 use 和 store 操作之前,必須執行過了 assign 和 load 操作;

5. 一個變量在同一時刻只允許一條線程對其進行 lock 操作,但 lock 操作可以被同一線程重複執行多次,多次執行 lock 後,只有執行相同次數的 unlock,變量纔會被解鎖;

6. 如果對一個變量執行 lock 操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行 load 或 assign 操作初始化變量的值;

7. 如果一個變量事先沒有被 lock 操作鎖定,則不允許對它執行 unlock 操作,也不允許去 unlock 一個被其他線程鎖定主的變量;

8. 對一個變量執行 unlock 操作之前,必須先把此變量同步回主內存中(執行 store 和 write 操作)。


往期精選 :

 

2019秋招總結二:460道Java後端面試高頻題

2019秋招:460道Java後端面試高頻題答案版【模塊一:Java基礎】

2019秋招:460道Java後端面試高頻題答案版【模塊二:Java集合類】

2019秋招:460道Java後端面試高頻題答案版【模塊三:Java併發】

 

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