类文件结构详解及类加载过程解析
在Java中,JVM可以理解的代碼被稱之為字節碼,它不面向任何的處理器,只面向JVM虛擬機,所以對于字節碼來說它屏蔽了任何和處理器指令相關的實現,可以做到一次編譯到處運行的能力,為什么它可以有這種能力呢,本質上是因為虛擬機其實替我們的代碼做了很多底層兼容適配的工作,所以不同操作系統的平臺上的JVM其實是完全不同的;
Java的字節碼其實還有另外一個好處,那就是可以同時兼顧編譯執行和解釋執行的優點,對于Java程序來說,先通過Java的編譯工具可以做到將源代碼編譯成機器碼,這個層面上看好像確實和編譯執行的代碼很相似,但是它編譯出來的字節碼卻又不是最終的機器碼,真正運行的時候,還需要jvm來進行解釋執行,所以說jvm同時具備了編譯執行和解釋執行的特點,不能說是優點;
好了,接下來我們看一下一個編譯好的類文件到底是什么樣子;
看下圖:
一個編譯好的類文件(xxxx.class)由以下幾個部分組成:
魔法數
class文件版本號
常量池
訪問標志
當前類、父類、接口、索引集合
字段表集合
方法表集合
屬性表集合
下面分別介紹一下類文件的這些屬性都是什么用途:
魔法數
魔法數是用來鑒別一個字節碼文件是否損壞的,是固定的0xcafebabe,也就是說如果一旦發現字節碼文件不是以cafebabe開頭,那么就說明這個字節碼文件已經損壞,也就沒辦法走后續的流程了;
Class文件版本號
這里的版本號,指的是編譯所用的Java編譯器的大小版本號,第五六個字節是次版本號,第七八字節是主版本號!高版本的Java虛擬機可以執行低版本的字節碼文件,但是低版本的虛擬機是無法執行高版本的編譯器編譯的Class文件的
常量池
緊接著是常量池,常量池的數量是constant_poo_count - 1
常量池的作用是存放字面量和符號引用,字面量接近于Java語言層面的常量概念,如文本字符串,聲明為final的常量值等,我的理解就是常量屬性聲明時等號后面的那部分,存在于常量池中稱之為字面量;
符號引用屬于編譯原理方面的概念,包括:類和接口的全限定名,字段的名稱和描述符,方法的名稱和描述符;也就是代碼中等號前面的部分;
訪問標志
常量池后面是訪問標志,這個標志用于標志類或者接口的訪問信息,比如說這個class是類還是接口,是public還是abstract等等,如果是類的話是否聲明為final等等信息
當前類、父類、接口、索引集合
類索引用于確定這個類的全限定名,父類索引用于確定類的父類的全限定名;
接口索引描述了這個類的所有實現的接口的集合,implement按照從左往右的順序排列
字段表集合(Fields)
用于描述接口或者類中聲明的變量,字段包括類變量和實例變量,但不包括在方法內部聲明的局部變量;
access_flag指的是字段的作用域(public、private、protocted等等,是實例變量還是類變量,被static修飾)
name_index 對常量池的引用,表示的字段的名稱
descriptor_index:對常量池的引用,表示字段和方法的描述符
attributes_count:一個字段的額外屬性,存放額外屬性的個數
attributes:存放具體的額外屬性的內容
方法表集合(Methods)
方法表的內容看起來和字段表是類似的就不再贅述了
類加載的完整過程
接下來我們來講類加載的完整過程,我們先看一個整體的類加載的過程圖:
就是說一個類的完整加載過程要經過加載 、連接、初始化三個過程,而連接這個過程又包含:驗證、準備、解析三個過程;下面我們就一一來介紹一下這幾個過程分別都做了哪些事
加載
1.通過類的全限定名獲取定義 此類的二進制字節流
2.將字節流所代表的靜態存儲結構轉化為方法區運行時的動態存儲結構;
3.在Java堆中生成一個代表該類的class對象,作為方法區中這些數據的訪問入口
注意,虛擬機規范中上述這3點并不具體,因此對于虛擬機實現來說非常靈活,比如“通過類的全限定名獲取定義此類的二進制字節流”并沒有指明是從哪里獲取,怎樣獲取;比如比較常見的就是從zip包中讀取(日后出現的Jar、Ear、War等格式 的基礎)以及其他文件生成(比如從JSP文件中)等等
一個非數據組類的加載階段是可控性最強的階段,這一步我們可以自定義類加載器去控制字節流的獲取方式(重寫類加載器的loadClass()方法)。數組類型不通過類加載器創建,而是由虛擬機直接創建;
加載完了之后就進入了連接階段,連接階段共分成三部分:驗證、準備、解析
驗證
(圖源來自JavaGuide公眾號文章,推薦)
驗證這部分主要是進行字節碼的相關驗證,比如文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證,所謂的字節碼驗證就是驗證是否以cafebabe開頭,主次版本號是否在當前java虛擬機能處理的范圍內,常量池中是否有不被支持的常量等等
而元數據驗證主要是進行一個語義驗證的階段,主要是對一些基本的語法信息進行驗證,比如是否有父類,是否繼承了不允許被繼承的父類,訪問權限又是否可見等,需要保證基本的語義正確;
字節碼驗證進行的是更高階的能力驗證,加入剛剛的元數據驗證是判斷語義是否正確的話,那么字節碼驗證主要是進行整體邏輯上的驗證,即通過數據流和控制流分析確定程序語義是合理合法的。任意時刻操作數棧和指令代碼序列都能配合工作;
最后是符號引用的驗證,即確保解析動作能夠正確執行,這句話圖中解釋的比較簡單,我們以字節碼的文件格式中的類方法表為例,一個Java類想要調用相關的方法,必須去方法表中根據符號引用來找到它具體對應的內存的就偏移量地址,所以這一步其實就是在驗證符號引用是否存在于相關的碼表中,方便后續步驟的時候可以直接進行轉化;
準備
準備階段的主要作用是正式的對類變量進行內存空間的分配,注意這里只有類變量會進行內存空間的分配,而不包括實例變量,何為類變量,就是被static修飾符修飾的變量稱之為類變量,除了為類變量分配內存空間以外還會對類變量進行賦初值的操作,加入我們有一個變量是 public static int count = 755,那么在這個階段其實只是做到了為count變量準備內存空間,并將初始0值賦給這個count變量而已,不過有一個特殊的點就是如果變量被final修飾,那么在準備階段其就已經會將給定值賦給類變量了,同樣是上面的例子,加入 public static final count = 755,此時,在這個階段count就會被賦值為755 而不會再發生改變了;
那么這些類變量是在內存的哪塊區域開辟的空間呢,在JDK7之前,也就是用永久代來實現方法區的時候,是存放在永久代中的,但是JDK7及之后,類變量隨著Class對象一起放到了Java堆中
(JDK7之后,HotSpot虛擬機已經把原本存放在永久代中的字符串常量池、靜態變量等移動到了堆中)
解析
何為解析,其實就是將常量池內的符號引用替換為直接引用的過程,解析動作主要針對的對象是類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符等
符號引用是一組符號來描述目標,直接引用就是直接指向目標的指針、相對偏移量或者一個間接定位到目標的句柄,在程序運行的過程中,只有符號引用是遠遠不夠的,系統需要明確知道這個方法所在的位置,前面我們說到的Java類的方法表,當Java想要調用一個類的方法的時候,只要知道了這個類中方法在方法表中的偏移量就可以直接調用該方法(知道了這個方法的具體實現存儲在了哪塊內存區域中)
初始化
這個階段才是真正開始調用我們代碼中的初始化方法的過程(我理解應該就是構造函數),是類加載的最后一步,這一步JVM才開始真正的執行類中定義的Java程序代碼;
對于(<clinit>)方法的調用,虛擬機會確保其在多線程環境下調用的完全性,也就是說會通過加鎖的方式來保證線程安全,不知道各位還記不記得,單例模式中有一種靜態子類加載的方式,其實本質上就是利用了類加載過程會加鎖的這個條件來使得實例的初始化階段是線程安全的,也就說不會存在兩個或者兩個以上的類可以同時初始化我們的單例實例;
至于具體哪些初始化階段需要對類進行初始化這邊省略(可以看深入理解JVM虛擬機)
extra:卸載
額外補充一個類的卸載;
方法區其實也是一個GC回收的對象,因為它是線程間共享的一塊內存資源,不過在方法區進行內存回收效率太低了,因為對常量池和類的回收本身沒什么收益,但是從中我們可以得到的信息就是類也是可以被回收的,可是滿足什么條件的時候類可以被回收呢?
類的所有實例都已經被回收了,也就是說在虛擬機堆中找不到任何一個類的實例;
類的類加載器已經被回收了;
類的Class對象已經被回收了(存儲于堆中),也就是說你無法通過任何反射的途徑來訪問到類的方法和屬性;
即使是滿足了上述的三個條件,也并不能夠保證類一定會被回收,也就是說其實類的回收條件是非常非常非常苛刻的;
同時需要注意的是,JVM自帶的類加載器加載的類在JVM的生命周期中是不可能被回收的,也就是說BootstrapClassLoader(啟動類加載器),extClassLoader(擴展類加載器),applicationClassLoader(應用程序類加載器)這三種類加載器加載的類不可以被回收,而我們自己實現的類加載器加載的類在滿足了上述條件的時候是有可能發生類的回收的;
本文參考:
《深入理解JVM虛擬機》
《JavaGuide類加載過程》
總結
以上是生活随笔為你收集整理的类文件结构详解及类加载过程解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ubuntu12.04 Jdk1.7 T
- 下一篇: MusicXML 3.0 (6) - 符