本文經受權轉自公衆號狸貓技術窩的專欄:《從零開始帶你成爲JVM實戰高手》java
做者:救火隊隊長linux
目錄:面試
前文回顧windows
JVM在什麼狀況下會加載一個類?服務器
從實用角度出發,來看看驗證、準備和初始化的過程微信
核心階段:初始化工具
類加載器和雙親委派機制加密
昨日思考題的解答設計
今日思考題3d
我們今天先來回顧一下昨天講到的JVM總體的一個運行原理。
咱們首先從「.java」代碼文件,編譯成「.class」字節碼文件,而後類加載器把「.class」字節碼文件中的類給加載到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,暗號:返現