1. 程式人生 > >不止面試—jvm類載入面試題詳解

不止面試—jvm類載入面試題詳解

面試題

帶著問題學習是最高效的,本次我們將嘗試回答以下問題:

  1. 什麼是類的載入?
  2. 哪些情況會觸發類的載入?
  3. 講一下JVM載入一個類的過程
  4. 什麼時候會為變數分配記憶體?
  5. JVM的類載入機制是什麼?
  6. 雙親委派機制可以打破嗎?為什麼

答案放在文章的最後,來不及看原理也可以直接跳到最後直接看答案。

深入原理

類的生命週期

類的生命週期相信大家已經耳熟能詳,就像下面這樣:

不過這東西總是背了就忘,忘了又背,就像馬什麼梅一樣,對吧?

其實理解之後,基本上就不會再忘了。

載入

載入主要做三件事:

  1. 找到類檔案(通過類的全限定名來獲取定義此類的二進位制位元組流)
  2. 放入方法區(將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構)
  3. 開個入口(生成一個代表此類的java.lang.Class物件,作為訪問方法區這些資料結構的入口)

總的來講,這一步就是通過類載入器把類讀入記憶體。需要注意的是,第三步雖然生成了物件,但並不在堆裡,而是在方法區裡。

連線

連線分為三步,一般面試都比較喜歡問準備這一步。

校驗

顧名思義,檢查Class檔案的位元組流中包含的資訊是否符合當前虛擬機器的要求。

準備

這一步中將為靜態變數和靜態常量分配記憶體,並賦值。

需要注意的是,靜態變數只會給預設值。比如下面這個:

public static int value = 123;

此時賦給value的值是0,不是123。

靜態常量(static final修飾的)則會直接賦值。比如下面這個:

public static final int value = 123;

此時賦給value的值是123。

解析

解析階段就是jvm將常量池的符號引用替換為直接引用。

恩......啥是常量池?啥是符號引用?啥是直接引用?

常量池我們放在jvm記憶體結構裡說。先來說下什麼是符號引用和直接引用。

符號引用和直接引用

假設有一個Worker類,包含了一個Car類的run()方法,像下面這樣:

class Worker{
    ......
    public void gotoWork(){
        car.run(); //這段程式碼在Worker類中的二進位制表示為符號引用        
    }
    ......
}

在解析階段之前,Worker類並不知道car.run()這個方法記憶體的什麼地方,於是只能用一個字串來表示這個方法。該字串包含了足夠的資訊,比如類的資訊,方法名,方法引數等,以供實際使用時可以找到相應的位置。

這個字串就被稱為符號引用。

在解析階段,jvm根據字串的內容找到記憶體區域中相應的地址,然後把符號引用替換成直接指向目標的指標、控制代碼、偏移量等,這之後就可以直接使用了。

這些直接指向目標的指標、控制代碼、偏移量就被成為直接引用。

初始化

類的初始化的主要工作是為靜態變數賦程式設定的初值。

還記得上面的靜態變數嗎:

public static int value = 123;

經過這一步,value的值終於是123了。

總結如下圖:

類初始化的條件

Java虛擬機器規範中嚴格規定了有且只有五種情況必須對類進行初始化:

  1. 使用new位元組碼指令建立類的例項,或者使用getstatic、putstatic讀取或設定一個靜態欄位的值(放入常量池中的常量除外),或者呼叫一個靜態方法的時候,對應類必須進行過初始化。
  2. 通過java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則要首先進行初始化。
  3. 當初始化一個類的時候,如果發現其父類沒有進行過初始化,則首先觸發父類初始化。
  4. 當虛擬機器啟動時,使用者需要指定一個主類(包含main()方法的類),虛擬機器會首先初始化這個類。
  5. 使用jdk1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、RE_invokeStatic的方法控制代碼,並且這個方法控制代碼對應的類沒有進行初始化,則需要先觸發其初始化。

除了以上這五種情況,其他任何情況都不會觸發類的初始化。

比如下面這幾種情況就不會觸發類初始化:

  1. 通過子類呼叫父類的靜態欄位。此時父類符合情況一,而子類不符合任何情況。所以只有父類被初始化。
  2. 通過陣列來引用類,不會觸發類的初始化。因為new的是陣列,而不是類。
  3. 呼叫類的靜態常量不會觸發類的初始化,因為靜態常量在編譯階段就會被存入呼叫類的常量池中,不會引用到定義常量的類。

類載入機制

類載入器

在上面咱們曾經說到,載入階段需要“通過一個類的全限定名來獲取描述此類的二進位制位元組流”。這件事情就是類載入器在做。

jvm自帶三種類載入器,分別是:

  1. 啟動類載入器。
  2. 擴充套件類載入器。
  3. 應用程式類載入器

他們的繼承關係如下圖:

雙親委派

雙親委派機制工作過程如下:

  1. 當前ClassLoader首先從自己已經載入的類中查詢是否此類已經載入,如果已經載入則直接返回原來已經載入的類。每個類載入器都有自己的載入快取,當一個類被載入了以後就會放入快取,等下次載入的時候就可以直接返回了。

  2.  當前classLoader的快取中沒有找到被載入的類的時候,委託父類載入器去載入,父類載入器採用同樣的策略,首先檢視自己的快取,然後委託父類的父類去載入,一直到bootstrp ClassLoader.

  3.  當所有的父類載入器都沒有載入的時候,再由當前的類載入器載入,並將其放入它自己的快取中,以便下次有載入請求的時候直接返回。

為啥要搞這麼複雜?自己處理不好嗎?

雙親委派的優點如下:

  1. 避免重複載入。當父親已經載入了該類的時候,就沒有必要子ClassLoader再載入一次。
  2. 為了安全。避免核心類,比如String被替換。

打破雙親委派

“雙親委派”機制只是Java推薦的機制,並不是強制的機制。

比如JDBC就打破了雙親委派機制。它通過Thread.currentThread().getContextClassLoader()得到執行緒上下文載入器來載入Driver實現類,從而打破了雙親委派機制。

至於為什麼,以後再說吧。

答案

現在,我們可以回答文章開頭提出的問題了。儘量在理解的基礎上回答,不需要死記硬背。

  1. 什麼是類的載入?

    JVM把通過類名獲得類的二進位制流之後,把類放入方法區,並建立入口物件的過程被稱為類的載入。經過載入,類就被放到記憶體裡了。

  2. 哪些情況會觸發類的初始化?

    類在5種情況下會被初始化:

    第一,假如這個類是入口類,他會被初始化。

    第二,使用new建立物件,或者呼叫類的靜態變數,類會被初始化。不過靜態常量不算。

    第三,通過反射獲取類,類會被初始化

    第四,如果子類被初始化,他的父類也會被初始化。

    第五,使用jdk1.7的動態語言支援時,呼叫到靜態控制代碼,也會被初始化。

  3. 講一下JVM載入一個類的過程

    同問題1。不過這裡也可以問下面試官是不是想問類的生命週期。如果是問類的生命週期,可以回答有”載入、連線、初始化、使用、解除安裝“五個階段,連線又可以分為”校驗、準備、解析“三個階段。

  4. 什麼時候會為變數分配記憶體?

    在準備階段為靜態變數分配記憶體。

  5. JVM的類載入機制是什麼?

    雙親委派機制,類載入器會先讓自己的父類來載入,父類無法載入的話,才會自己來載入。

  6. 雙親委派機制可以打破嗎?為什麼

    可以打破,比如JDBC使用執行緒上下文載入器打破了雙親委派機制。原因是JDBC只提供了介面,並沒有提供實現。這個問題可以再看下引用文獻的內容。

引用文獻

Java什麼時候會觸發類初始化及原理

Java 符號引用 與 直接引用

符號引用和直接引用

JVM中的直接引用和符號引用

面試題:JVM類載入機制詳解(一)JVM類載入過程

深入理解雙親委託機制

雙親委派模式破壞-J