大白話談JVM的類加載機制
前言
我們很多小夥伴平時都是做JAVA開發的,那麼作為一名合格的工程師,你是否有仔細的思考過JVM的運行原理呢。
如果懂得了JVM的運行原理和內存模型,像是一些JVM調優、垃圾回收機制等等的問題我們才能有一個更清晰的概念。
為了走進JVM,深入了解底層,王子打算寫一個JVM的專題,留下自己對JVM探索的足跡,同時也希望能幫到小夥伴們更好的理解JVM。
那我們開始吧。
JAVA代碼的運行流程
首先我們就來聊一聊JAVA代碼是怎麼運行起來的,這部分比較基礎相信大家都知道,就當成是個複習吧。
我們編寫的代碼都是在java文件中編寫的,然後會編譯成class位元組碼文件。
當我們使用到哪個類的時候就會通過類加載器把class位元組碼文件中的類加載到jvm內存中,然後就是在jvm內存中運行我們的代碼了。
整體的運行流程就是這樣,相信小夥伴們都很清楚這些,但是有關類加載器是如何把類加載到jvm內存中的,小夥伴們有考慮過嗎?
今天我們主要就是聊這一部分。
JVM什麼時候加載類
其實說到類加載的底層機制,這是一個很複雜的過程,但是對於我們平時的工作來講,只要懂得它的核心原理就可以了。
一個類的加載過程會經歷如下的幾個過程:
加載、驗證、準備、解析、初始化、使用、卸載
首先我們就先弄明白一個問題,jvm是什麼時候去加載類的呢?
其實答案很簡單,就是我們什麼時候使用到了這個類,它就去class位元組碼文件中去加載這個類。
而作為程序的入口,具有main方法的類,肯定是最開始的時候就加載到jvm中了。
對於加載類的時間點問題,其實就是這麼簡單。
類加載器和雙親委派機制
既然我們知道了類加載的時間點,那麼jvm是通過什麼方式對類進行加載的呢?就是類加載器。
那接下來我們就來聊聊jvm的類加載器。
jvm的類加載器總體上可以分成4層,我們一起看一下。
1.啟動類加載器
首先就是jvm啟動的第一道關口,啟動類加載器Bootstrap ClassLoader,它主要是加載java的核心類。
相信大家都知道,無論是什麼環節下運行java程序,都是要安裝jvm虛擬機環境的,而在這個環境的目錄中是有一個lib文件夾的,這個文件下就是java最核心的類庫,支撐着java系統的運行。
所以一旦jvm啟動,那麼首先就會通過啟動類加載器去加載lib文件夾下的核心類庫。
2.擴展類加載器
然後我們就到了第二層,擴展類加載器Extension ClassLoader,這個類加載器其實與啟動類加載器是類似的。
在我們的jvm虛擬機環境目錄下,是有一個lib/ext的文件夾的,這裏面的類就是java運行環境的一些擴展類,這些擴展類就是在jvm啟動後,通過擴展類加載器進行加載的。
3.應用程序類加載器
加載完核心類庫和擴展類,這時候就到了第三層,應用程序類加載器Application ClassLoader,這個類加載器你就可以理解成是加載我們寫好的java代碼的就可以了。
4.自定義類加載器
前面的三層就是基本的類加載器了,然後第四層是自定義類加載器,根據一些特殊的需求來自己定義類加載器加載我們的類。
整體上類加載器就是這麼的4層結構。很多小夥伴可能都聽說過雙親委派機制,那麼什麼是雙親委派機制呢,王子就和大家用最接地氣的語言描述一下。
其實很好理解,就是當我們的類加載器要加載一個類的時候,它首先會委派給它的父親去加載,但是如果它的父親沒找到就會把這個事交給他的孩子自己去完成了。
這就是雙親委派機制。
舉個例子,假如我們的應用程序類加載器要加載一個類A,那麼首先它會先回家找它老爸擴展類加載器,問問「老爸,你那有這個類A嗎?」
然後擴展類加載器接到這個請求之後,同樣也懶得處理,再去找它爺爺啟動類加載器。
它爺爺找了一圈沒找到類A,很生氣,就對擴展類加載器說,「我這沒有,你自己找去!」
然後擴展類加載器就灰溜溜的自己找了一圈,同樣也沒找到,這時候就找到應用類加載器了,說:「我這哪有你這個類A,這明明是你自己應該乾的活,愛上哪找上哪找去,我不管了」。
這時候應用類加載器就只能自己去處理了,找了一圈發現找到了類A,就把它加載到jvm內存中了。
相信大家看了這個例子應該很容易理解了吧。
所以假設我們自己創建了一個類java.lang.String,它是不會被應用類加載器加載到內存中的,因為父類中可以找到這個類,就直接給加載到內存中了。
聊聊驗證、準備、解析、初始化階段
聊完了加載,我們再來看看驗證、準備、解析、初始化這幾個階段jvm都做了什麼。
1.驗證階段
這一步其實很容易理解,就是jvm根據java規範,來校驗你加載進來的class文件中的內容是否符合規範,如果不符合規範jvm是無法正常運行的。
所以在加載後,首先就是驗證階段。
2.準備階段
假設我們有一個類A,剛剛加載並通過了驗證,那麼就會進行準備工作。
這個準備工作其實就是給類A分配一定的內存空間,然後給裏面的靜態變量(static修飾的變量)也分配內存空間,並賦初始值。
3.解析階段
這個階段乾的事實際上是把符號引用替換為直接引用,這一過程網上有很多資料,還是比較複雜的,如果感興趣小夥伴們可以自己查閱一下資料。
實際工作中也很少會接觸這部分的內容,所以我們知道有這麼個階段就可以了。
4.初始化階段
在準備階段,我們把類A的內存已經分配完了,那麼初始化階段要做些什麼事呢?我們先看一下類A的代碼
public class A { private static String i=System.getProperty("i"); }
準備階段我們只是給變量i分配了內存空間,並賦值了初始值,但是後邊的System.getProperty(“i”)是不會執行的。
沒錯,這部分代碼就是在初始化階段執行的,另外靜態代碼塊也會在這一階段執行。
舉個例子,比如我們新建一個對象new A(),此時就會觸發從加載到初始化的全過程,把這個類準備好並創建一個實例對象。
此外這裡有一個規則,如果類A繼承了類B,那麼在初始化類A的時候,如果發現類B還沒有初始化,會先初始化類B。
擴展
到這裡關於JVM的類加載機制其實就已經說完了,王子再給大家擴展一個小知識點。
小夥伴們想過沒有,Tomcat也是用java開發的,那麼它的類加載機制是什麼樣的呢,為什麼就能支持jsp呢?
其實它就是利用了自定義類加載器這一機制,自己自定義了很多類加載器,整體的結構如下:
Tomcat自定義了這麼多的類加載器,用來加載它自己的核心類庫,並且Tomcat是打破了雙親委派機制的,感興趣的小夥伴可以自己去查資料了解一下,王子就不在本篇文章長篇大論來聊Tomcat了。
總結
今天我們聊的內容還是jvm中比較基礎的部分,以後的文章我們再慢慢深入,去探索jvm的底層原理,如果對JVM感興趣的小夥伴可以關注王子的後續文章哦,我們可以一步一個腳印的逐步分解JVM,去了解JVM的垃圾回收機制、性能調優等等實用性問題,讓你面對JVM的面試或者生產實踐也可以遊刃有餘。
那我們下次見。
往期文章推薦: