任何一個Class文件都對應着惟一一個類或接口的定義信息,但反過來講,類或接口並不必定都得定義在文件裏(類和接口也能夠用反射的方式經過類加載器直接生成)html
Class文件時一組以8位字節爲基礎單位的二進制流,各個數據都嚴格按照順序緊湊排列在Class文件中,沒有任何分隔符。java
Class文件格式採用一種相似C語言結構體的僞結構存儲數據,這種結構中只包含無符號數和表兩種類型。數據庫
表是由多個無符號整數或者其餘表構成的符合數據類型,都由"_info"結尾。數組
表用於描述有層次關係的複合結構的數據,整個Class文件實際上就是一張表。安全
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
編輯器用16進制打開類文件服務器
0 1 2 3 4 5 6 7 8 9 A B C D E F ca fe ba be 00 00 00 34 00 3f 0a 00 0a 00 2b 08 00 2c 09 00 0d 00 2d 06 40 59 00 00 00 00 00 00 09 00 0d 00 2e 09 00 2f 00 30 08 00 31 0a 00 32....
類文件第一個數據爲u4,咱們查看16進制文件前4個字符是cafebabe
,它用來肯定這個文件是否爲一個能被虛擬機接受的Class文件。網絡
u4後的兩個u2,即00 00 00 34用來表明jdk的主次版本。數據結構
常量池是Class文件結構中與其餘項目關聯最多的數據類型,也是佔用Class文件空間最大的數據,也是Class文件中第一個出現的表類型數據項目。多線程
存放類型包含:oracle
類從被加載到虛擬機內存開始,到卸載出內存爲止,它的整個生命週期包括如下 7 個階段:加載、驗證、準備、解析、初始化、使用、卸載
加載、驗證、準備、初始化和卸載這 5 個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始(注意是「開始」,而不是「進行」或「完成」),而解析階段則不必定:它在某些狀況下能夠在初始化後再開始,這是爲了支持 Java 語言的運行時綁定(多態)。
虛擬機規範嚴格規定了有且只有5鍾狀況必須當即對類進行初始化:
使用 new、getstatic、putstatic、或invokestatic這四條字節碼命令時,後三個命令分別表明對類的靜態變量進行操做,調用類的靜態方法。生成這四條指令最多見的場景爲:new一個對象的時候、讀取或者賦值給類的靜態變量的時候(被final修飾的除外,由於已經在編譯期把結果放入了常量池)、以及調用一個靜態方法的時候。
反射調用類的時候,若是類未被初始化須要進行初始化
當實例化某類時,其父類沒被初始化,須要初始化父類
當虛擬機啓動時,用戶指定的執行的主類(包含main方法的類),虛擬機會先初始化這個主類
當使用 JDK 1.7 的動態語言支持時,若是一個 java.lang.invoke.MethodHandle 實例最後的解析結果爲 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,而且這個方法句柄所對應的類還沒初始化,則須要先觸發其初始化。
這 5 種場景稱爲對一個類進行主動引用(有且只有這五種才能夠觸發類的初始化),除此以外,其它全部引用類的方式都不會觸發初始化,稱爲被動引用。
/** * 被動引用 Demo1: * 經過子類引用父類的靜態字段,不會致使子類初始化。 */ class SuperClass { static { System.out.println("SuperClass init!"); } public static int value = 123; } class SubClass extends SuperClass { static { System.out.println("SubClass init!"); } } public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); // SuperClass init! } }
對於靜態字段,只有直接定義這個字段的類纔會被初始化,所以經過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。
/** * 被動引用 Demo2: * 經過數組定義來引用類,不會觸發此類的初始化。 */ public class NotInitialization { public static void main(String[] args) { SuperClass[] superClasses = new SuperClass[10]; } }
new數組對象時並不會觸發SuperClass
類的初始化,而是在這段代碼裏觸發一個名爲Lorg.fenixsoft.classloading.SuperClass
的類初始化,他直接繼承自Object
類,由虛擬機來產生和觸發。
/** * 被動引用 Demo3: * 常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化。 */ class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLO_ZHIYIN = "Hello ZhiYin"; } public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.HELLO_ZHIYIN); } }
JVM在編譯期進行了傳播優化,將ConstClass類中的常量放入了NotInitialization的常量池中,事實上這個常量已經和ConstClass類沒有了聯繫,不會觸發初始化。
類加載過程包括 5 個階段:加載、驗證、準備、解析和初始化。
「加載」是「類加載」過程的第一步,在加載階段,虛擬機須要完成如下三件事情:
經過一個類的全限定名(com.zhiyin.TestClass)來獲取定義此類的二進制字節流
將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
在內存中(HostSpot在方法區)生成一個表明該類的java.lang.Class
對象,做爲方法區這個類的各類數據的訪問入口
對於 Class 文件,虛擬機沒有指明要從哪裏獲取、怎樣獲取。除了直接從編譯好的 .class 文件中讀取,還有如下幾種方式:
驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
文件格式驗證:第一階段是驗證字節流是否符合Class文件的規範
0xCAFEBABE
開頭元數據驗證:第二階段是對字節碼描述的信息進行語義分析,保證符合Java語言要求
字節碼驗證:本階段是驗證過程當中最複雜的一個階段,是對方法體進行語義分析,保證方法在運行時不會出現危害虛擬機的事件。
符號引用驗證:最後一個階段的驗證時發生在虛擬機將符號引用轉化爲直接引用的時候。這個動做在鏈接的第三階段——解析階段中發生,校驗如下內容:
符號引用驗證若是沒有經過,會拋出一個java.lang.IncompatibleClassChangeError
異常的子類,如常見的java.lang.NoSuchFieldError、java.lang.NoSuchMethodError
等
對於虛擬機的類加載機制而言,驗證是一個很重要的、但不是必須的(由於對程序運行期無影響)一個階段,若是運行的所有代碼(包括本身編寫的以及第三方包中的代碼)都已經被反覆使用和驗證過,那麼在實施階段就可使用-Xverify:none
來關閉大部分類的驗證過程,以縮短虛擬機類加載的時間
準備階段是正式爲類變量(被 static修飾的變量)分配內存並設置類變量初始值(一般爲零值,引用類型爲null)的階段,這些變量所使用的內存將在方法去區中進行分配。以下語句中:
public static int value = 666;
value
變量在準備階段以後初始值變爲0而不是666,變爲666的過程是在初始化階段進行。
上面說到一般狀況下是零值,特殊狀況爲該變量同時被final修飾,是常量。
public static final int value = 666;
編譯時value就會生成ConstantValue屬性(定義爲常量),在準備階段虛擬機就會依據ConstantValue的設置將value賦值爲666.
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
class二進制字節流中的引用關係都是符號引用沒有真正的意義,解析以後將會變成直接指向目標的指針。
類初始化階段是類加載過程的最後一步,是執行類構造器方法的過程。
類構造器方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static {} 塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的。
靜態語句塊中只能訪問定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊中能夠賦值,但不能訪問。以下方代碼所示:
public class Test { static { i = 0; // 給變量賦值能夠正常編譯經過 System.out.println(i); // 這句編譯器會提示「非法向前引用」 } static int i = 1; }
類構造器方法不須要顯式調用父類構造器,虛擬機會保證在子類的類構造器方法執行以前,父類的類構造器方法方法已經執行完畢。
因爲父類的類構造器方法方法先執行,意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做。以下方代碼所示:
static class Parent { public static int A = 1; static { A = 2; } } static class Sub extends Parent { public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); // 輸出 2 }
類構造器方法不是必需的,若是一個類沒有靜態語句塊,也沒有對類變量的賦值操做,那麼編譯器能夠不爲這個類生成它。
接口中不能使用靜態代碼塊,但接口也須要經過類構造器方法爲接口中定義的靜態成員變量顯式初始化。但接口與類不一樣,接口的類構造方法不須要先執行父類的類構造方法方法,只有當父接口中定義的變量使用時,父接口才會初始化。
虛擬機會保證一個類的類構造方法在多線程環境中被正確加鎖、同步。若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的類構造方法。
參考《深刻理解Java虛擬機》、Jvm官方規範