一、什么是 JVM
JVM(Java Virtual Machine)是一個可以執行 Java 字節碼文件(即 .class 文件)的虛擬機進程。當 Java 源文件能被成功編譯成 .class 文件,就能在不同平台上的不同版本的 JVM 運行,因為 JVM 能將相同的 .class 文件解釋稱不同平台的機器碼。正是因為 JVM 的存在,Java 被稱為與平台無關的語言。
一般而言,.java 文件經過編譯后會得到 .class 文件,而將這個文件加載到內存之前需要先通過類加載器,先簡單過一下圖:

二、類加載過程
類加載的過程為: 加載-->連接(驗證-->准備-->解析)-->初始化。下面介紹其中的幾個過程。
1、加載
這個過程主要是通過類的全限定名,例如 java.lang.String 這樣帶上包路徑的類名,獲取到字節碼文件;然后將這個字節碼文件代表的靜態存儲結構(可簡單理解為對象創建的模板)存在方法區,並在堆中生成一個代表此類的 Class 類型的對象,作為訪問方法區中“模板”的入口,往后創建對象的時候就按照這個模板創建。
舉個例子,有時候通過反射創建對象,像當初學 JDBC 時會通過 Class.getName("com.mysql.jdbc.Driver.class").newInstance() 創建對象,通過 Class 和相應的全限定類名獲取到方法區中的“模板”然后創建對象。

2、驗證
驗證過程主要確保被加載的類的正確性。首先要先驗證文件格式是否規范,如果只是通過 .class 后綴來辨別,那隨便把后綴名改一下就可以跑程序了,那豈不是很容易出事。來看看字節碼文件大概是長什么樣的:

注意看前綴 cafe babe(咖啡寶貝?)這只是驗證的其中一個點,還會驗證字節碼文件里是否包含主次版本號等驗證信息。
3、准備
這個階段主要是給類變量(靜態變量)分配方法區的內存並初始化。實例變量不是在這個階段分配內存,實例變量是隨着對象一起分配在堆中。另外,給靜態變量初始化為零值或空值,比如public static int n=5;這里並不是馬上給 n 這個變量賦值為 5,而是先將其賦值為 0,類似的,如果是引用數據類型,則默認為 null。還有一點需要注意的是,對於 final 類型的數據,必須在程序內給它賦值,系統不會自動初始化,例如 static String str = "hello" + “world”;String 是 final 類型的,在編譯階段就給它優化成 static String str = "helloworld” ,並且將 "helloworld" 放進了常量池。
4、初始化
這個階段就是將靜態變量賦值為初始值,還是 public static int n=5; 這回給 n 賦值為 5 了。
三、類加載器

啟動類加載器是由C/C++寫的,主要負責加載 jre\lib 目錄下的類;擴展類加載器主要負責加載 jre\lib\ext 目錄下的類;而應用程序類加載器主要負責加載我們自己編寫的類;當然還能自己寫類加載器,即自定義加載器。程序主要由前面三個類加載器相互配合加載的。
public class Main { public static void main(String[] args) { Main main = new Main(); System.out.println(main.getClass().getClassLoader()); System.out.println(main.getClass().getClassLoader().getParent()); System.out.println(main.getClass().getClassLoader().getParent().getParent()); } }
由於啟動類加載器是 C/C++ 語言寫的,所以輸出為 null
雙親委派機制
在類加載的過程中,存在着雙親委派機制,即當要加載一個類時,先由父類加載器加載,當父類加載器沒辦法加載時,才由下面的加載器加載,來看一個程序:
package java.lang; // 自定義的包 public class String { public static void main(String[] args) { System.out.println("這是自定義的java.lang.String類"); } }

由於 jre\lib\ext 中存在 java.lang.String 類,當加載該類的時候,根據全限定名進行查找,找到后由啟動類加載器加載,發現 String 類中不包含 main() 方法,因此程序出錯。
