面試官對於JVM類加載機制的猛烈炮火,你能頂住嗎?

本文經受權轉自公衆號狸貓技術窩的專欄:《從零開始帶你成爲JVM實戰高手》java

做者:救火隊隊長linux

目錄:面試

  1. 前文回顧windows

  2. JVM在什麼狀況下會加載一個類?服務器

  3. 從實用角度出發,來看看驗證、準備和初始化的過程微信

  4. 核心階段:初始化工具

  5. 類加載器和雙親委派機制加密

  6. 昨日思考題的解答設計

  7. 今日思考題3d

一、前文回顧

我們今天先來回顧一下昨天講到的JVM總體的一個運行原理。

咱們首先從「.java」代碼文件,編譯成「.class」字節碼文件,而後類加載器把「.class」字節碼文件中的類給加載到JVM中,接着是JVM來執行咱們寫好的那些類中的代碼,總體是這麼個順序。

再看看下圖,感覺一下這個過程:

那麼今天,咱們就來仔細看看上圖中的「類加載」這個過程,看看JVM的類加載機制究竟是怎麼樣的?

搞清楚這個過程了,那麼之後在面試時,對面試官常問的JVM類加載機制,就能把一些核心概念說清楚了。


二、JVM在什麼狀況下會加載一個類?

其實類加載過程很是的瑣碎複雜,可是對於咱們平時從工做中實用的角度來講,主要是把握他的核心工做原理就能夠。

一個類從加載到使用,通常會經歷下面的這個過程:

加載 -> 驗證 -> 準備 -> 解析 -> 初始化 -> 使用 -> 卸載

因此首先要搞明白的第一個問題,就是JVM在執行咱們寫好的代碼的過程當中,通常在什麼狀況下會去加載一個類呢?

也就是說,啥時候會從「.class」字節碼文件中加載這個類到JVM內存裏來。

其實答案很是簡單,就是在你的代碼中用到這個類的時候

舉個簡單的例子,好比下面你有一個類(Kafka.class),裏面有一個「main()」方法做爲主入口。

那麼一旦你的JVM進程啓動以後,它必定會先把你的這個類(Kafka.cass)加載到內存裏,而後從「main()」方法的入口代碼開始執行。

咱們仍是堅持一步一圖,你們先看看下圖,感覺一下:

接着假設上面的代碼中,出現了以下的這麼一行代碼:

這時可能你們就想了,你的代碼中明顯須要使用「ReplicaManager」這個類去實例化一個對象,此時必須得把「ReplicaManager.class」字節碼文件中的這個類加載到內存裏來啊!是否是?

因此這個時候就會觸發JVM經過類加載器,從「ReplicaManager.class」字節碼文件中加載對應的類到內存裏來使用,這樣代碼才能跑起來。

咱們來看下面的圖:

上面就是給你們舉的一個例子,相信很是的通俗易懂。

簡單歸納一下:首先你的代碼中包含「main()」方法的主類必定會在JVM進程啓動以後被加載到內存,開始執行你的「main()」方法中的代碼

接着遇到你使用了別的類,好比「ReplicaManager」,此時就會從對應的「.class」字節碼文件加載對應的類到內存裏來。


三、從實用角度出發,來看看驗證、準備和初始化的過程

其實上面的類加載時機的問題,對於不少有經驗的同窗來講不是什麼問題。可是對於不少初學者來講,是一個很是重要的須要捋清的概念。

接下來就來簡單帶着你們,從實用的角度出發,過一下另外三個概念:

驗證、準備、初始化

其實對於這三個概念,沒太大的必要去深究裏面的細節,這裏的細節不少很繁瑣,對於大部分同窗而言,只要腦子裏有下面的幾個概念就能夠了:

(1)驗證階段

簡單來講,這一步就是根據Java虛擬機規範,來校驗你加載進來的「.class」文件中的內容,是否符合指定的規範。

這個相信很好理解,假如說,你的「.class」文件被人篡改了,裏面的字節碼壓根兒不符合規範,那麼JVM是無法去執行這個字節碼的!

因此把「.class」加載到內存裏以後,必須先驗證一下,校驗他必須徹底符合JVM規範,後續才能交給JVM來運行。

下面用一張圖,展現了這個過程:

(2)準備階段

這個階段其實也很好理解,我們都知道,咱們寫好的那些類,其實都有一些類變量,好比下面的這個「ReplicaManager」類:

假設你有這麼一個「ReplicaManager」類,他的「ReplicaManager.class」文件內容剛剛被加載到內存以後,會進行驗證,確認這個字節碼文件的內容是規範的。

接着,就會進行準備工做,這個準備工做,其實就是給這個「ReplicaManager」類分配必定的內存空間。

而後給他裏面的類變量(也就是static修飾的變量)分配內存空間,來一個默認的初始值。

好比上面的示例裏,就會給「flushInterval」這個類變量分配內容空間,給一個「0」這個初始值。

整個過程,以下圖所示:

(3)解析階段

這個階段乾的事兒,其實是把符號引用替換爲直接引用的過程,其實這個部分的內容很複雜,涉及到JVM的底層

可是注意,同窗們,就我本意而言,但願第一週的文章,絕對是淺顯易懂的,按部就班,要保證每一個同窗都能絕對看懂。

因此針對這個階段,如今不打算作過深的解讀,由於從實用角度而言,對不少同窗在工做中實踐JVM技術其實也用不到,因此這裏你們就暫時知道有這麼一個階段就能夠了。

一樣,我仍是給你們畫圖展現一下:

(4)三個階段的小結

其實這三個階段裏,最核心的你們務必關注的,就是「準備階段」

由於這個階段是給加載進來的類分配好了內存空間,類變量也分配好了內存空間,而且給了默認的初始值,這個概念,你們內心必定要有。


四、核心階段:初始化

以前說過,在準備階段時,就會把咱們的「ReplicaManager」類給分配好內存空間

另外他的一個類變量「flushInterval」也會給一個默認的初始值「0」,那麼接下來,在初始化階段,就會正式執行咱們的類初始化的代碼了。

那麼什麼是類初始化的代碼呢?咱們來看看下面這段代碼:

你們能夠看到,對於「flushInterval」這個類變量,咱們是打算經過Configuration.getInt("replica.flush.interval")這段代碼來獲取一個值,而且賦值給他的

可是在準備階段會執行這個賦值邏輯嗎?

NO!在準備階段,僅僅是給「flushInterval」類變量開闢一個內存空間,而後給個初始值「0」罷了。

那麼這段賦值的代碼何時執行呢?答案是在「初始化」階段來執行。

在這個階段,就會執行類的初始化代碼,好比上面的 Configuration.getInt("replica.flush.interval") 代碼就會在這裏執行,完成一個配置項的讀取,而後賦值給這個類變量「flushInterval」。

另外好比下圖的static靜態代碼塊,也會在這個階段來執行。

相似下面的代碼語義,能夠理解爲類初始化的時候,調用「loadReplicaFromDish()」方法從磁盤中加載數據副本,而且放在靜態變量「replicas」中:

那麼搞明白了類的初始化是什麼,就得來看看類的初始化的規則了。

何時會初始化一個類?

通常來講有如下一些時機:好比「new ReplicaManager()」來實例化類的對象了,此時就會觸發類的加載到初始化的全過程,把這個類準備好,而後再實例化一個對象出來;

或者是包含「main()」方法的主類,必須是立馬初始化的。

此外,這裏還有一個很是重要的規則,就是若是初始化一個類的時候,發現他的父類還沒初始化,那麼必須先初始化他的父類

好比下面的代碼:

若是你要「new ReplicaManager()」初始化這個類的實例,那麼會加載這個類,而後初始化這個類

可是初始化這個類以前,發現AbstractDataManager做爲父類還沒加載和初始化,那麼必須先加載這個父類,而且初始化這個父類。

這個規則,你們必須得牢記,再來一張圖,藉助圖片來進行理解:

五、類加載器和雙親委派機制

如今相信你們都搞明白了整個類加載從觸發時機到初始化的過程了,接着給你們說一下類加載器的概念。由於實現上述過程,那必須是依靠類加載器來實現的。

那麼Java裏有哪些類加載器呢?簡單來講有下面幾種:

(1)啓動類加載器

Bootstrap ClassLoader,他主要是負責加載咱們在機器上安裝的Java目錄下的核心類的

相信你們都知道,若是你要在一個機器上運行本身寫好的Java系統,不管是windows筆記本,仍是linux服務器,是否是都得裝一下JDK?

那麼在你的Java安裝目錄下,就有一個「lib」目錄,你們能夠本身去找找看,這裏就有Java最核心的一些類庫,支撐你的Java系統的運行。

因此一旦你的JVM啓動,那麼首先就會依託啓動類加載器,去加載你的Java安裝目錄下的「lib」目錄中的核心類庫。

(2)擴展類加載器

Extension ClassLoader,這個類加載器其實也是相似的,就是你的Java安裝目錄下,有一個「lib\ext」目錄

這裏面有一些類,就是須要使用這個類加載器來加載的,支撐你的系統的運行。

那麼你的JVM一旦啓動,是否是也得從Java安裝目錄下,加載這個「lib\ext」目錄中的類?

(3)應用程序類加載器

Application ClassLoader,這類加載器就負責去加載「ClassPath」環境變量所指定的路徑中的類

其實你大體就理解爲去加載你寫好的Java代碼吧,這個類加載器就負責加載你寫好的那些類到內存裏。

(4)自定義類加載器

除了上面那幾種以外,還能夠自定義類加載器,去根據你本身的需求加載你的類。

(5)雙親委派機制

JVM的類加載器是有親子層級結構的,就是說啓動類加載器是最上層的,擴展類加載器在第二層,第三層是應用程序類加載器,最後一層是自定義類加載器。

你們看下圖:

而後,基於這個親子層級結構,就有一個雙親委派的機制

什麼意思呢?

就是假設你的應用程序類加載器須要加載一個類,他首先會委派給本身的父類加載器去加載,最終傳導到頂層的類加載器去加載

可是若是父類加載器在本身負責加載的範圍內,沒找到這個類,那麼就會下推加載權利給本身的子類加載器。

聽完了上面一大堆繞口令,是否是很迷茫?彆着急,我們用一個例子來講明一下。

好比你的JVM如今須要加載「ReplicaManager」類,此時應用程序類加載器會問問本身的爸爸,也就是擴展類加載器,你能加載到這個類嗎?

而後擴展類加載器直接問本身的爸爸,啓動類加載器,你能加載到這個類嗎?

啓動類加載器心想,我在Java安裝目錄下,沒找到這個類啊,本身找去!

而後,就下推加載權利給擴展類加載器這個兒子,結果擴展類加載器找了半天,也沒找到本身負責的目錄中有這個類。

這時他很生氣,說:明明就是你應用程序加載器本身負責的,你本身找去。

而後應用程序類加載器在本身負責的範圍內,好比就是你寫好的那個系統打包成的jar包吧,一會兒發現,就在這裏!而後就本身把這個類加載到內存裏去了。

這就是所謂的雙親委派模型:先找父親去加載,不行的話再由兒子來加載。

這樣的話,能夠避免多層級的加載器結構重複加載某些類。

最後,給你們來一張圖,感覺一下類加載器的雙親委派模型。

六、昨日思考題的解答

好!今天的文章看完了,相信你們就能大體推測出昨日的思考題的答案了。

我昨天的問題是:如何對「.class」文件處理保證不被人拿到之後反編譯獲取公司源代碼?

其實認真看完今天的文章,就很簡單了。首先你編譯時,就能夠採用一些小工具對字節碼加密,或者作混淆等處理

如今有不少第三方公司,都是專門作商業級的字節碼文件加密的,因此能夠付費購買他們的產品。

而後在類加載的時候,對加密的類,考慮採用自定義的類加載器來解密文件便可,這樣就能夠保證你的源代碼不被人竊取。

七、今日思考題

今天再給你們留一個思考題,相信每一個作Java的同窗,都知道如今通常用Java開發的Web系統,除非是基於Java寫中間件,通常都是採用Tomcat之類的Web容器來部署的。

那麼你們想一想,Tomcat自己就是用Java寫的,他本身就是一個JVM。

咱們寫好的那些系統程序,說白了,就是一堆編譯好的.class文件放入一個war包,而後在Tomcat中來運行的。

那麼,Tomcat的類加載機制應該怎麼設計,才能把咱們動態部署進去的war包中的類,加載到Tomcat自身運行的JVM中,而後去執行那些咱們寫好的代碼呢?

你們先思考,明天文末會給你們進行梳理並給出答案。

End

推薦一個專欄:

《從零開始帶你成爲JVM實戰高手》

做者是我多年好友,之前團隊的左膀右臂

一塊兒經歷過各類大型複雜系統上線的血雨腥風

現任阿里資深技術專家,對JVM有豐富的生產實踐經驗

專欄目錄參見文末,能夠掃下方海報進行試讀

經過上面海報購買,再返你24元

領取方式:加微信號:Giotto1245,暗號:返現

相關文章
相關標籤/搜索