天天看點

大廠面試系列-請簡單描述Java的類加載過程?

大廠面試系列-請簡單描述Java的類加載過程?

在之前的分享中我們介紹了雙親委派機制,這裡我們來分享關于Java類加載的生命周期等内容。

Java類加載的過程的産物

在之前的的分享中我們介紹過如何将Java源碼通過編譯之後轉換成位元組碼的Class檔案。并且類加載器将這些位元組碼的Class檔案加載到記憶體中,存放在方法區中。

根據會在JVM堆記憶體中該對象對應的java.lang.Class的對象,并且這個Class對象,指向了方法區中的Class檔案的封裝的資料結構。這一點可以這樣去了解,就是再JVM中會對每個Class檔案建立一個屬于自己的對象,而這個對象就是對應的Class檔案的抽象。如下圖所示,

而類加載過程的最終産物是是位于堆記憶體中的Class對象,Class對象封裝了對應的類在方法區中的資料結構,并且向開發者提供了方法區中的對應通路資料結構的接口。如下圖所示,圖檔來源網絡

大廠面試系列-請簡單描述Java的類加載過程?

類的生命周期

Java中對于每個對象來講都有屬于自己的生命周期,當然類也是有自己的生命周期的。類的生命周期包括了:加載、連接配接、初始化、使用、解除安裝等。在連接配接階段,有驗證、準備、解析三個子階段。如下圖所示,圖檔來源網絡

大廠面試系列-請簡單描述Java的類加載過程?

當然整個的過程都是從類加載開始的,但是在整個的過程中,各種階段也是互相直接互動混合進行,通常會在前一個階段進行的過程中就會觸發下一個過程的執行。下面我們就來詳細介紹一下這些過程中主要的工作原理。

類加載階段

根據上面的描述,類加載階段主要完成的工作就是将位元組碼.class檔案讀取到記憶體中。主要完成如下的幾項内容。

1、通過類的全限定名擷取到對應類的二進制位元組檔案,也就是通過編譯生成的.class檔案。

2、将這個位元組流所代表的靜态結構轉換為方法區中的運作時資料結構。

3、在JVM堆記憶體中生成一個代表這個類對應的java.lang.Class對象,并且指向了方法區中對應的存儲資料通路入口,也就是對應的引用。

完成這三個操作之後,整個的類加載的過程就算完成了,但是需要注意一點,在我們之前介紹關于類加載相關的内容的時候,介紹了關于自定義類加載器的操作,也就是為開發人員留下了拓展的空間。例如我們可以從壓縮包中讀取class檔案,也可以從網絡路徑中擷取class檔案,也可以通過自定義的實作方式來動态生成class檔案,例如我們的AOP技術等。

在加載階段完成之後,我們就可以通過在堆記憶體中對應的Class對象來通路到方法區中的一些對象封裝的資料結構了。

連接配接-驗證階段

如何確定一個類是被正确的加載,我們可以通過如下的四個方面來進行驗證。

  • 對于檔案格式的驗證

這個階段要驗證位元組流是否符合Class的檔案格式規範,并且是否可以被目前版本的虛拟機所處理。主要驗證是否是以0XCAFEBABE魔數開頭,以及版本号是否在目前虛拟機處理範圍,常量池中的常量是否存在不支援的情況,指向各種索引值的内容是否正常。并且還要驗證class相關的各種需要被驗證的附加資訊等的。

這個階段主要是要保證位元組流能夠正确的解析并且存儲到方法區中,當然驗證的内容遠不止這些,通過這個階段的驗證之後,位元組流就會進入到方法區中進行存儲。

  • 對于中繼資料的驗證

對于中繼資料的驗證其實就是對位元組碼描述的資訊進行語義分析、保證各種描述資訊都是符合Java的語言規範的。在這個階段主要驗證的有如下的一些資訊。驗證對應類是否存在父類、驗證繼承的父類是否是被final所修飾的、如果這個類不是抽象類,是否在在對應的子類或者父類中有未實作的方法。驗證類中字段、方法等是否都是符合Java語言規範封裝、繼承、多态等内容。

這個階段驗證完成的主要對類的中繼資料資訊的語義定義,保證不存在不符合Java語言規範的中繼資料資訊存在。

  • 對于位元組碼的驗證

這個過程是整個驗證階段最為複雜的過程,主要包括對于通過對資料流和控制流等内容的分析,确定程式定義的語義是符合邏輯的。

在上一階段完成對中繼資料資訊驗證之後,這個階段主要完成的就是對方法體進行校驗分析,保證被校驗的方法在運作過程中不會出現損害JVM安全的事情。例如保證通過指令跳轉等方式跳轉到方法體之外的位元組碼指令中,這也就是在Java中不支援GOTO操作的原因。因為這樣的操作可能會存在損害JVM的行為。

當然如果一個方法的運作通過了位元組碼驗證,也不一定能夠說它是安全的、通過程式邏輯去校驗是無法做到絕對的準确,但是通過校驗可以檢查出一些具有明顯安全隐患的代碼處理邏輯。可以在一定程度上保證安全性。

  • 對于符号引用的驗證

所謂驗證階段的最後一步操作,在JVM運作過程中需要将符号引用轉化成直接引用。這個轉化動作在連接配接的第三個階段完成,而這裡校驗的就是是否通過對應的符号引用能夠找到對應的直接引用操作。包括對于public、protected、private等關鍵字修飾的内容。以及如果無法正常解析的内容會抛出Error、然後程式中斷等操作。

當然對于類連接配接的驗證可以通過-Xverifynone 參數來進行驗證的配置。

因為在有些場景中,如果類驗證的過程太嚴格的話反而會導緻整個的類加載是一個耗時的操作。為了減少耗時操作。就可以通過該參數對類驗證的規則進行設定。

連接配接-準備階段

在準備階段,主要完成對于類的靜态變量配置設定記憶體,初始化預設值等一些操作。需要注意的是,這裡提到的關于記憶體配置設定的操作隻限于static 變量,并不包括對象的執行個體變量。因為在之前我們提到的Java記憶體配置設定的時候,提到過執行個體變量會随着Java對象的記憶體配置設定被配置設定到堆記憶體中。

而這裡所提到的預設值也是JVM預設指定的預設值,例如一些基礎資料類型對應的初始值,0 、false、null等等。這裡的預設值并不是我們在代碼編寫的時候所指定的那個預設值,這是需要注意的一點,當然也有例外,如下。

當然如果在代碼中有存在ConstatntValue屬性的字段,也就是同時被static和final關鍵字修飾的字段,那麼在準備階段整個屬性就會被初始化為其對應的初始值。

連接配接-解析階段

解析階段主要完成的工作是JVM将常量池中的符号引用轉換為直接引用的過程。解析動作主要是針對類或者接口、屬性字段、類方法、接口方法、方法類型、方法句柄和調用點等符号應用進行解析。

符号引用

什麼是符号引用?符号引用是用一組符号來表示所要引用的目标,它可以是任意形式的字面量,隻要能在使用的時候可以無歧義将目标定位即可。符号引用與虛拟機記憶體實作是沒有關系的,而所引用的目标也不一定都是已經被加載到記憶體中的資料。因為在各種虛拟機中記憶體布局可能是不一樣,但是他們所接收到的符号引用必須是一緻的,換句話來講,這也可以作為Java語言跨平台實作的底層原理之一。因為這些符号引用都是通過統一的字面量被明确定義在支援JVM規範的Class檔案中,是以将這些Class檔案用任意符合規範的JVM運作的時候都是可以正常的進行解析的。

直接引用

直接引用可以是直接指向目标對象的指針、偏移量或者是目标句柄等。直接引用是和虛拟機的記憶體實作有關聯的。同一個符号引用在不同的JVM上翻譯出來的内容一般是不一樣的。如果有直接引用,那麼其所引用的目标必須存在于記憶體中。

在JVM中并沒有對解析發生的時間進行明确的規定,隻要求了在執行anewarray、checkcast、getfield、getstatic、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic這16個用于操作符号引用之前,先對它們所使用的符号引用進行解析。

也就是說JVM可以根據需要來進行判斷是對哪個類加載之前就對其進行解析,還是說等到對應的符号引用要被解析的時候才會被解析。

在JVM中同一個符号引用被解析多次的情況是非常常見的,除了invokedynamic指令之外,JVM可以實作對第一次解析的結果進行緩存的操作。避免重複解析帶來的資源消耗。

初始化

作為類加載過程的最後一步操作,在之前的操作中,除了在加載階段可以通過使用者自定義類加載器進行擴充操作之外,其他的操作都是由JVM來完成的,而到了初始化階段才開始真正意義上的開始執行我們編寫的Java代碼。

根據之前的分析在準備階段,我們已經對屬性進行過一次指派操作了,并且我們提到準備階段的指派與初始化階段的指派操作是不一樣的。初始化階段的指派就是要将程式中的預設值進行指派。

在初始化階段需要執行類的構造方法。并且為類的靜态變量指派正确的值,同時也對類變量進行初始化操作。而對于整個初始化的過程,我們在後續分享關于構造方法的執行順序相關内容的時候會進行詳細介紹。

總結

經過初始化之後,所有的類對象就是我們需要在程式代碼中真正想要去使用的的對象,并且通過執行代碼中的邏輯也會得到我們想要的過程。使用完成之後,就通過垃圾回收将不用的對象進行收集并且對裝載到JVM中的類資訊進行解除安裝。這樣就完成了整個的Java類的生命周期。

繼續閱讀