虛擬機把描述類的數據從Class文件加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制
類的生命周期
類從被加載到虛擬機內存中開始,直到卸載出內存為止,它的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和卸載, 而驗證,準備,解析部分又統稱為連接。
在整個生命周期中,類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之后開始,這是為了支持Java語言的運行時綁定(也成為動態綁定或晚期綁定)。另外注意這里的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或激活另一個階段。
加載
加載,是類加載(Class Loading)過程的第一個階段,主要任務是查找并加載類的二進制數據。在加載階段,虛擬機需要完成以下三件事情
- 通過一個類的全限定名來獲取定義此類的二進制字節流
- 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構.
- 在內存中生成一個代表這個類的
java.lang.Class
對象,作為在方法區中這個類的各種數據的訪問入口
加載過程需要注意如下兩點:
1、因為沒有指定Class文件要從何獲取,也就讓整個加載過程可以有了更加廣闊開放的舞臺。這個設計最終間接導致了jar,war等格式的文件出現,Applet技術,動態代理技術更是應運而生。除此之外,開發人員可以自由選擇類加載,甚至開發自己的類加載器去完成特殊“類”加載。
2、在加載階段就已經在內存中生成了java.lang.Class
對象,這個階段之后就可以通過該對象訪問方法區(永久代)上該類的相關信息。永久代上保存了被加載的類信息,常量,靜態常量,JIT編譯后的代碼等數據。
驗證(連接)
驗證的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全
驗證階段大致會完成如下4個階段的檢驗工作
- 文件格式驗證:驗證字節流是否符合Class文件格式的規范;
例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理范圍之內、常量池中的常量是否有不被支持的類型。
-
元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規范的要求;
例如:這個類是否有父類,除了java.lang.Object之外。類是否繼承了不允許被繼承的類(final類)
-
字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。
例如:保證任何時刻操作數棧的數據類型與指令代碼序列都能配合工作,保證跳轉指令不會跳轉到方法體以外的字節碼指令上。StackMapTable屬性優化,描述方法體中所有的基本塊開始時本地變量表和操作棧應有的狀態,在字節碼驗證期間只需要檢查StackMapTable屬性中的記錄是否合法即可不需要根據程序與退到狀態是否合法性。
符號引用驗證:確保解析動作能正確執行。
例如:符號引用中通過字符串描述的全限定名是否可以找到對應的類,符號引用中的類,字段等是否可以被當前類訪問
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反復驗證,那么可以考慮采用-Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
準備(連接)
準備階段是正式為類變量分配內存并設置類變量
(static變量)
初始值的階段,這些變量所使用的內存都將在方法區
中進行分配。
這個階段只為類的靜態變量分配內存并進行零值初始化,對于該階段有以下幾點需要注意:
- 這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在堆中。
- 這里所說的初始值“通常情況”下是數據類型的零值(所謂的零值不代表0,詳細零值見如下表格)。
- 如果類字段的字段屬性表中存在 ConstantValue屬性,即同時被final和static修飾,那么在準備階段變量value就會被初始化為ConstValue屬性所指定的值。
各種基本數據類型的初始化零值如下:
數據類型 | 零值 | 數據類型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.00f |
short (short) | 0 | double | 0.00d |
char | '\u0000' | reference | null |
byte (byte) | 0 |
案例分析
假設一個類變量的定義如下
public static int value = 1;
那變量value在準備階段過后的初始值為0而不是1。因為這時候尚未開始執行任何java方法和java代碼,而把value賦值為1的putstatic
指令是程序被編譯后,存放于類構造器<clinit>()
方法之中,而真正執行該類初始化方法的時間實在初始化階段。
假設上面的類變量value被定義如下
public static final int value = 1;
編譯時Javac
將會為value生成ConstantValue
屬性,在準備階段虛擬機就會根據 ConstantValue
的設置將value賦值為1。我們可以理解為static final
常量在編譯期就將其結果放入了調用它的類的常量池中
總結如下
對基本數據類型來說,對于類變量
(static)
和全局變量,如果不顯式地對其賦值而直接使用,則系統會為其賦予默認的零值,而對于局部變量來說,在使用前
必須顯式地為其賦值,否則編譯時不通過。對于
同時
被static
和final
修飾的常量,必須在聲明的時候就為其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在聲明時顯式地為其賦值,也可以在類實例初始化時顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統不會為其賦予默認零值。對于引用數據類型
reference
來說,如數組引用、對象引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會為其賦予默認的零值,即null。如果在數組初始化時沒有對數組中的各元素賦值,那么其中的元素將根據對應的數據類型而被賦予默認的零值。
解析(連接)
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。
直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
初始化
類初始化階段是類加載過程的最后一步。在準備階段,變量已經被賦予過一次‘零值’的初始值了,而在初始化階段,則是根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源。也可以說,初始化階段是執行類構造器<clinit>()方法的過程。
JVM 初始化類兩個方式
- 聲明類變量是指定初始值
- 使用靜態代碼塊為類變量指定初始值
JVM初始化步驟
- 假如這個類還沒有被加載和連接,則程序先加載并連接該類
- 假如該類的直接父類還沒有被初始化,則先初始化其直接父類
- 假如類中有初始化語句,則系統依次執行這些初始化語句
在JVM進行初始化時需要注意如下幾點
<clinit>()方法是由編譯器自動收集類中的所有
類變量的賦值動作
和靜態語句塊static{}
中的語句合并產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊之前的變量,定義在它之后的變量,在前面的靜態語句塊可以賦值,但是不能訪問。(很多筆試題經常出現)<clinit>()方法與實例構造器<init>()方法不同,它不需要顯示地調用父類構造器,虛擬機會保證在子類<init>()方法執行之前,父類的<clinit>()方法方法已經執行完畢。
由于父類的<clinit>()方法先執行,也就意味著父類中定義的靜態語句塊要優先于子類的變量賦值操作。
<clinit>()方法對于類或者接口來說并不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生產<clinit>()方法。
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法。但接口與類不同的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的<clinit>()方法。
虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有好事很長的操作,就可能造成多個線程阻塞,在實際應用中這種阻塞往往是隱藏的。
<clinit>()方法線程安全問題
其他線程初始化同一個<clinit>()時會被阻塞,但如果執行<clinit>()方法的那條線程退出<clinit>()方法后,其他線程喚醒之后不會再次進入<clinit>()方法。同一個類加載器下,一個類型只會初始化一次。
public class DeadLoopTest {
static class DeadLoopClass {
static {
// 如果不加上這個if語句,編譯器將提示“Initializer does not complete normally”并拒絕編譯
if (true) {
System.out.println(Thread.currentThread() + "init DeadLoopClass");
while (true) {
}
}
}
}
public static void main(String[] args) {
Runnable script = new Runnable() {
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + " run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
類初始化時機
虛擬機規定有且只有5種情況必須立即對類進行“初始化”
- 使用
new
關鍵字實例化對象 - 訪問某個類或接口的
靜態變量
,或者對該靜態變量賦值(被final修飾、已在編譯器把結果放入常量池的靜態字段除外) - 調用類的
靜態方法
- 反射(
java.lang.reflect
包下的反射類,動態代理) - 初始化子類時,先初始化其父類(父類還未初始化)
- Java虛擬機啟動時被標明為啟動類的類( Applaction),直接使用 java.exe命令來運行某個主類
結束生命周期
在如下幾種情況下,Java虛擬機將結束生命周期
- 執行了 System.exit()方法
- 程序正常執行結束
- 程序在執行過程中遇到了異常或錯誤而異常終止
- 由于操作系統出現錯誤而導致Java虛擬機進程終止
類加載器
什么是類加載
虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個工作的代碼模塊稱為“類加載器”。
類加載器
類加載器是一個用來加載類文件的類。Java源代碼通過javac編譯器編譯成類文件。然后JVM來執行類文件中的字節碼來執行程序。類加載器負責加載文件系統、網絡或其他來源的類文件。有三種默認使用的類加載器:Bootstrap類加載器、Extension類加載器和System類加載器(或者叫作Application類加載器)。每種類加載器都有設定好從哪里加載類。
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTest.class.getClassLoader();
System.out.println(loader.toString());
System.out.println(loader.getParent().toString());
System.out.println(loader.getParent().getParent());
}
打印結果如下:
sun.misc.Launcher$AppClassLoader@4e0e2f2a
sun.misc.Launcher$ExtClassLoader@2a139a55
null
- Bootstrap類加載器負責加載rt.jar中的JDK類文件,它是所有類加載器的父加載器。Bootstrap類加載器沒有任何父類加載器,如果你調用
String.class.getClassLoader()
,會返回null,任何基于此的代碼會拋出NullPointerException
異常。Bootstrap加載器被稱為初始類加載器。 - Extension將加載類的請求先委托給它的父加載器,也就是Bootstrap,如果沒有成功加載的話,再從
jre/lib/ext
目錄下或者java.ext.dirs
系統屬性定義的目錄下加載類。Extension加載器由sun.misc.Launcher$ExtClassLoader
實現。 - 第三種默認的加載器就是System類加載器(又叫作
Application
類加載器)了。它負責從classpath環境變量中加載某些應用相關的類,classpath環境變量通常由-classpath或-cp命令行選項來定義,或者是JAR中的Manifest的classpath屬性。Application類加載器是Extension類加載器的子加載器。通過sun.misc.Launcher$AppClassLoader實現。除了Bootstrap類加載器是大部分由C來寫的,其他的類加載器都是通過java.lang.ClassLoader來實現的。
注意:這里父類加載器并不是通過繼承關系來實現的,而是采用組合實現的。
JVM類加載機制
全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入
父類委托,先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類
緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統才會讀取該類對應的二進制數據,并將其轉換成Class對象,存入緩存區。這就是為什么修改了Class后,必須重啟JVM,程序的修改才會生效。
雙親委派模型
雙親委派的工作過程如下
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因為所有的加載請求最終都應該被傳遞到頂層的啟動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去加載。
雙親委派機制:
1、當 AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。
2、當 ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader```去完成。
3、如果 BootStrapClassLoader加載失敗(例如在 $JAVA_HOME/jre/lib里未查找到該class),會使用 ExtClassLoader來嘗試加載;
4、若ExtClassLoader也加載失敗,則會使用 AppClassLoader來加載,如果 AppClassLoader也加載失敗,則會報出異常 ClassNotFoundException。
類加載器的工作原理
Java類加載器基于三個機制:委托、可見性和單一性。委托機制是指將加載一個類的請求交給父類加載器,如果這個父類加載器不能夠找到或者加載這個類,那么再加載它。可見性的原理是子類的加載器可以看見所有的父類加載器加載的類,而父類加載器看不到子類加載器加載的類。單一性原理是指僅加載一個類一次,這是由委托機制確保子類加載器不會再次加載父類加載器加載過的類。
雙親委派的設計優點
java類隨著它的類加載器一起具備了帶有優先級的層次關系。
比如java.langObject
,它存放在\jre\lib\rt.jar
中,它是所有java類的父類,因此無論哪個類加載都要加載這個類,最終所有的加載請求都匯總到頂層的啟動類加載器中。因此Object
類會由啟動類加載器來加載,所以加載的都是同一個類,如果不使用雙親委派模型,由各個類加載器自行去加載的話,系統中就會出現不止一個Object
類,應用程序就會全亂了。
類的加載
常見類加載有三種方式:
- 命令行啟動應用時候由JVM初始化加載
- 通過Class.forName()方法動態加載
- 通過ClassLoader.loadClass()方法動態加載
Class.forName()和ClassLoader.loadClass()區別
- Class.forName():將類的.class文件加載到jvm中之外,還會對類進行解釋,執行類中的static塊;
- ClassLoader.loadClass():只干一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance才會去執行static塊。
- Class.forName(name,initialize,loader)帶參函數也可控制是否加載static塊。并且只有調用了newInstance()方法采用調用構造函數,創建類的對象 。
自定義類加載器
通常情況下,我們都是直接使用系統類加載器。但是,有的時候,我們也需要自定義類加載器。比如應用是通過網絡來傳輸 Java類的字節碼,為保證安全性,這些字節碼經過了加密處理,這時系統類加載器就無法對其進行加載,這樣則需要自定義類加載器來實現。自定義類加載器一般都是繼承自 ClassLoader類,從上面對 loadClass方法來分析來看,我們只需要重寫 findClass 方法即可。下面我們通過一個示例來演示自定義類加載器的流程:
package jvm.c7;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader {
public MyClassLoader(String root) {
this.root = root;
}
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
try {
String fileName = root + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
InputStream in = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = in.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
MyClassLoader myClassLoader = new MyClassLoader("C://Users/Administrator.USER-20170830RI/Documents/workspace-sts-3.8.3.RELEASE/jvm");
Class<?> testClass = null;
try {
testClass = myClassLoader.loadClass("jvm.c7.Test");
Object object = testClass. newInstance();
System.out.println(object.getClass().getClassLoader());
System.out.println(object.getClass().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
自定義類加載器的核心在于對字節碼文件的獲取,如果是加密的字節碼則需要在該類中對文件進行解密。由于這里只是演示,我并未對class文件進行加密,因此沒有解密的過程。這里有幾點需要注意:
1、這里傳遞的文件名需要是類的全限定性名稱,即 com.paddx.test.classloading.Test格式的,因為 defineClass 方法是按這種格式進行處理的。
2、最好不要重寫loadClass方法,因為這樣容易破壞雙親委托模式。
3、這類Test 類本身可以被 AppClassLoader類加載,因此我們不能把 com/paddx/test/classloading/Test.class放在類路徑下。否則,由于雙親委托機制的存在,會直接導致該類由 AppClassLoader加載,而不會通過我們自定義類加載器來加載。
破壞雙親委派模型
- 1、兼容JDK1.2之前的ClassLoader
- 2、引入線程上下文類加載器
- 3、代碼熱替換,模塊熱部署等要求。OSGi實現模塊化熱部署的關鍵則是它自定義的類加載機制的實現。
參考文件
《深入理解Java虛擬機》
《java類的加載機制》http://www.cnblogs.com/ityouknow/p/5603287.html