了解Java虛拟機(2)之.class檔案加載過程
讀《深入了解Java虛拟機》-周志明 讀書筆記
虛拟機隻能執行.class檔案,在.class檔案加載過程中,生命周期包括:加載,驗證,準備,解析,初始化,使用,解除安裝
- 加載将.clss檔案加載進虛拟機,加載來源有
- 1.常見jar包;
- 2.網絡擷取(dubbo的RPC是典型,底層通過java的RMI方法,通過固定協 議,将遠端生成者接口,對象序列化,消費者調用);
- 3.運作時動态生成,典型代表反射技術;
- 4.從其他檔案,如jsp;
- 5.通過資料庫讀取,(沒見到過,書裡說中間件伺服器)
-
驗證
驗證是連接配接階段的第一步,并不是加載完了才驗證(這樣的太傻瓜式了,要是很多.class檔案,在最開始加載就有錯誤,等加載完了再驗證那就不用工作了),驗證和加載是相輔相成的。在.java編譯成.class檔案裡講過.class打頭就是個”cafebabe”的魔數,首先驗證這個,不是這個打頭的必然報錯,接着看看版本号,版本号是不是java沒有的(比方搞個java6.0,必然錯了),然後看常量類型啊,驗證相當于看這個.class檔案是否符合JVM規範,用過Eclipse的就知道,每次”ctrl+s”就能看到自己寫的代碼有沒有錯誤,因為eclipse就是有自己的CLassLoader,即時編譯,你一儲存就給你編譯了.class,打開檔案夾就能看到class目錄的檔案夾,然後通過去驗證編譯後的.class是不是符合JVM規範,然後錯了就各種紅叉叉,iead就沒有即時編譯,”ctrl+s”就找不到對應的class檔案夾,eclipse的驗證不是虛拟機的驗證,這隻是eclipse自己根據jVM規範驗證的,編譯後的.class檔案JVM在加載的過程中,還是要自己驗證的,不過虛拟機有個參數可以設定不驗證(-Xverify:none),這樣可以加快加載速度,當然前提是你相信.class符合規範,驗證是很重要的一件事,不然怎麼承受惡意的攻擊,使得代碼更安全,當然驗證也會使得加載時間長,在安全和時間上,還是選擇更安全些,畢竟java大都都是用來做企業級應用的,安全是最重要的。
-
準備
準備階段是正式給類變量(static修飾的變量)配置設定記憶體并設定類變量的初始值,執行個體變量(不是static修飾的類層面的變量)會在對象執行個體化時随着對象一起配置設定在java堆中,要注意的是初始值的含義,和final修飾的執行個體變量,初始值指資料類型的0值,下面a在這個時候初始值為0;final修飾的變量這個時候會給值,c此時的值就是為3;(感覺final修飾符有些破壞了面向對象的設計,也不知道這個設計的初衷)
public Class Test(){
private static int a = ;//類變量
private int b = ;//執行個體變量
private final int c = ;
}
-
解析
解析是虛拟機将常量池(常量池位于方法區)的符号引用替換成直接引用的過程,先解釋下符号引用,直接引用
符号引用:
public class Test {
public static int sa = new Random().nextInt();
}
public class TestClass {
//這裡就是符号引用
public static int sa = Test.sa;
public static int sb = ;
}
編譯下上上面的,再反編譯過來如下(javap -verbose TestClass.class):
D:\learn\myLearn\readBook\src\main\java\JVM>javap -verbose TestClass.class
Classfile /D:/learn/myLearn/readBook/src/main/java/JVM/TestClass.class
Last modified --; size bytes
MD5 checksum ed1ae561b16a0cfecbdf7518c947cb
Compiled from "TestClass.java"
public class JVM.TestClass
SourceFile: "TestClass.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref ## // java/lang/Object."<init>":()
# = Fieldref ## // JVM/Test.sa:I
# = Fieldref ## // JVM/TestClass.sa:I
# = Fieldref ## // JVM/TestClass.sb:I
# = Class # // JVM/TestClass
# = Class # // java/lang/Object
# = Utf8 sa
# = Utf8 I
# = Utf8 sb
# = Utf8 <init>
# = Utf8 ()V
# = Utf8 Code
# = Utf8 LineNumberTable
# = Utf8 <clinit>
# = Utf8 SourceFile
# = Utf8 TestClass.java
# = NameAndType #:# // "<init>":()V
# = Class # // JVM/Test
# = NameAndType #:# // sa:I
# = NameAndType #:# // sb:I
# = Utf8 JVM/TestClass
# = Utf8 java/lang/Object
# = Utf8 JVM/Test
{
public static int sa;
flags: ACC_PUBLIC, ACC_STATIC
public static int sb;
flags: ACC_PUBLIC, ACC_STATIC
public JVM.TestClass();
flags: ACC_PUBLIC
Code:
stack=, locals=, args_size=
: aload_0
: invokespecial # // Method java/lang/Object."
":()V
4: return
LineNumberTable:
line 8: 0
static {};
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #2 // Field JVM/Test.sa:I
3: putstatic #3 // Field sa:I
6: bipush 114
8: putstatic #4 // Field sb:I
11: return
LineNumberTable:
line 9: 0
line 11: 6
}
截部分看看
java/lang/Object."<init>":()
# = Fieldref ## // JVM/Test.sa:I
# = Fieldref ## // JVM/TestClass.sa:I
static {};
flags: ACC_STATIC
Code:
stack=, locals=, args_size=
: getstatic # // Field JVM/Test.sa:I
: putstatic # // Field sa:I
: bipush
: putstatic # // Field sb:I
最後satatic{}可以看到Test.java中sa(#3 = Fieldref #5.#19 // JVM/TestClass.sa:I)變量指向的TestClass(JVM/Test.sa:I 3: putstatic #3 // Field sa:I ),初始化就是将這種指向變成指向new Random().nextInt(),的具體記憶體位址,這樣才能真正的調用直接引用就是直接new 對象了,将句柄指向對象的指針位址;在解析的過程中,遇到引用其他的類,就去觸發對應類的加載,最後都是直接引用
- 初始化
初始化是類加載的最後一步,這個階段才是真正執行類中定義的代碼,在準備階段,變量已經指派過系統要求的值,初始化階段根據我們寫代碼的主觀習慣去初始化變量和其他資源。相當于程式真正運作起來。