@
目錄- 前言
- 類的生命周期
- 加載
- 驗證
- 準備
- 決議
- 初始化
- 案例一
- 案例二
- 案例三
- 案例四
- 類加載器
- 類加載器和雙親委派模型
- 破壞雙親委派模型
- 第一次
- SPI
- Tomcat
- OSGI
- 總結
前言
前面學習了虛擬機的記憶體結構、物件的分配和創建,但物件所對應的類是怎么加載到虛擬機中來的呢?加載程序中需要做些什么?什么是雙親委派機制以及為什么要打破雙親委派機制?
類的生命周期

類的生命周期包含了如上的7個階段,其中驗證、準備、決議統稱為連接 ,類的加載主要是前五個階段,每個階段基本上保持如上順序開始(僅僅是開始,實際上執行是交叉混合的),只有決議階段不一定,在初始化后也有可能才開始執行決議,這是為了支持動態語言,
加載
加載就是將位元組碼的二進制流轉化為方法區的運行時資料結構,并生成類所物件的Class物件,位元組碼二進制流可以是我們編譯后的class檔案,也可以從網路中獲取,或者運行時動態生成(動態代理)等等,
那什么時候會觸發類加載呢?這個在虛擬機規范中沒有明確定義,只是規定了何時需要執行初始化(稍后詳細分析),
驗證
這個階段很好理解,就是進行必要的校驗,確保加載到記憶體中的位元組碼是符合要求的,主要包含以下四個校驗步驟(了解即可):
- 檔案格式校驗:這個階段要校驗的東西非常多,主要的有下面這些(實際上遠遠不止)
- 是否以魔數0xCAFEBABE開頭,
- 主、次版本號是否在當前Java虛擬機接受范圍之內,
- 常量池的常量中是否有不被支持的常量型別(檢查常量tag標志),
- 指向常量的各種索引值中是否有指向不存在的常量或不符合型別的常量,
- CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的資料,
- Class檔案中各個部分及檔案本身是否有被洗掉的或附加的其他資訊,
- ,,,,,,
- 元資料校驗:對位元組碼描述資訊進行語意分析,
- 這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類),
- 這個類的父類是否繼承了不允許被繼承的類(被final修飾的類),
- 如果這個類不是抽象類,是否實作了其父類或介面之中要求實作的所有方法,
- 類中的欄位、方法是否與父類產生矛盾(例如覆寫了父類的final欄位,或者出現不符合規則的方法多載,例如方法引數都一致,但回傳值型別卻不同等),
- ,,,,,,
- 位元組碼校驗:確保程式沒有語法和邏輯錯誤,這是整個驗證階段最復雜的一個步驟,
- 保證任意時刻運算元堆疊的資料型別與指令代碼序列都能配合作業,例如不會出現類似于“在操作堆疊放置了一個 int 型別的資料,使用時卻按 long 型別來加載入本地變數表中”這樣的情況,
- 保證任何跳轉指令都不會跳轉到方法體以外的位元組碼指令上,
- 保證方法體中的型別轉換總是有效的,例如可以把-個子類物件賦值給父類資料型別,這是安全的,但是把父類物件賦值給子類資料型別,甚至把物件賦值給與它毫無繼承關系、完全不相干的一個資料型別,則是危險和不合法的,
- ,,,,,,
- 符號參考驗證:這個階段發生在符號參考轉為直接參考的時候,即實際上是在決議階段中進行的,
- 符號參考中通過字串描述的全限定名是否能找到對應的類,
- 在指定類中是否存在符合方法的欄位描述符及簡單名稱所描述的方法和欄位,
- 符號參考中的類、欄位、方法的可訪問性( private、 protected. public、
), - 是否可被當前類訪問,
- ,,,,,,
準備
該階段是為類變數(static)分配記憶體并設定零值,即類只要經過準備階段其中的靜態變數就是可使用的了,但此時類變數的值還不是我們想要的值,需要經過初始化階段才會將我們希望的值賦值給對應的靜態變數,
決議
決議就是將常量池中的符號參考替換為直接參考的程序,符號參考就是一個代號,比如我們的名字,而這里可以理解為就是類的完全限定名;直接參考則是對應的具體的人、物,這里就是指目標的記憶體地址,為什么需要符號參考呢?因為類在加載到記憶體之前還沒有分配記憶體地址,因此必然需要一個東西指代它,這個階段包含了類或介面的決議、欄位決議、類方法決議、介面方法決議,在決議的程序中可能會拋出以下例外:
- java.lang.NoSuchFieldError:找不到欄位
- java.lang.IllegalAccessError:不具有訪問權限
- java.lang.NoSuchMethodError:找不到方法
初始化
這是類加載程序中的最后一個步驟,主要是收集類的靜態變數的賦值動作和static塊中的陳述句合成<cinit>方法,通過該方法根據我們的意愿為靜態變數賦值以及執行static塊,該方法會被加鎖,確保多執行緒情況下只有一個執行緒能初始化成功,利用該特性可以實作單例模式,虛擬機規定了有且只有遇到以下情況時必須先確保對應類的初始化完成(加載、準備必然在此之前):
- 遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令時,能夠生成這四條指令的典型Java代碼場景有:
- 使用new關鍵字實體化物件的時候,
- 讀取或設定一個型別的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,
- 呼叫一個型別的靜態方法的時候,
- 反射呼叫類時,
- 當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化,
- 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類,
- 當使用JDK 7新加入的動態語言支持時,如果一個java.lang.invoke.MethodHandle實體最后的決議結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種型別的方法句柄,并且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化,
- 當一個介面中定義了JDK 8新加入的默認方法(被default關鍵字修飾的介面方法)時,如果有這個介面的實作類發生了初始化,那該介面要在其之前被初始化,
下面分析幾個案例代碼,讀者們可以先思考后再運行代碼看看和自己想的是否一樣,
案例一
先定義如下兩個類:
public class SuperClazz {
static {
System.out.println("SuperClass init!");
}
public static int value=https://www.cnblogs.com/yewy/p/123;
public static final String HELLOWORLD="hello world";
public static final int WHAT = value;
}
public class SubClaszz extends SuperClazz {
static{
System.out.println("SubClass init!");
}
}
然后進行下面的呼叫:
public class Initialization {
public static void main(String[]args){
Initialization initialization = new Initialization();
initialization.M1();
}
public void M1(){
System.out.println(SubClaszz.value);
}
}
第一個案例是通過子類去參考父類中的靜態變數,兩個類都會加載和初始化么?列印結果看看:
SuperClass init!
123
可以看到只有父類初始化了,那么父類必然是加載了的,問題就在于子類有沒有被加載呢?可以加上引數:-XX:+TraceClassLoading再執行(該引數的作用就是列印被加載了的類),可以看到子類是被加載了的,所以通過子類參考父類靜態變數,父子類都會被加載,但只有父類會進行初始化,
為什么呢?反編譯后可以看到生成了如下指令:
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #6 // Field ex7/init/SubClaszz.value:I
6: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
9: return
關鍵就是getstatic指令就會觸發類的初始化,但是為什么子類不會初始化呢?因為這個變數是來自于父類的,為了提高效率,所以虛擬機進行了優化,這種情況只需要初始化父類就行了,
案例二
呼叫下面的方法:
public void M2(){
SubClaszz[]sca = new SubClaszz[10];
}
執行后可以發現,使用陣列,不會觸發初始化,但父子類都會被加載,
案例三
public void M3(){
System.out.println(SuperClazz.HELLOWORLD);
}
參考常量不會觸發類的加載和初始化,因為常量在編譯后就已經存在當前class的常量池,
案例四
public void M4(){
System.out.println(SubClaszz.WHAT);
}
通過常量去參考其它的靜態變數會發生什么呢?這個和案例一結果是一樣的,
類加載器
類加載器和雙親委派模型
在我們平時開發中,確定一個類需要通過完全限定名,而不能簡單的通過名字,因為在不同的路徑下我們是可以定義同名的類的,那么在虛擬機中又是怎么區分類的呢?在虛擬機中需要類加載器+完全限定名一起來指定一個類的唯一性,即相同限定名的類若由兩個不同的類加載器加載,那虛擬機就不會把它們當做一個類,從這里我們可以看出類加載器一定是有多個的,那么不同的類加載器是怎么組織的?它們又分別需要加載哪些類呢?

從虛擬角度看,只有兩種型別的類加載器:啟動類加載器(BootstrapClassLoader)和非啟動類加載器,前者是C++實作,屬于虛擬機的一部分,后者則是由Java實作的,獨立于虛擬機的外部,并且全部繼承自抽象類java.lang.ClassLoader,
但從Java本身來看,一直保持著三層類加載器、雙親委派的結構,當然除了Java本身提供的三層類加載器,我們還可以自定義實作類加載器,如上圖,上面三個就是原生的類加載器,每一個都是下一個類加載器的父加載器,注意這里都是采用組合而非繼承,當開始加載類時,首先交給父加載器加載,父加載器加載了子加載器就不用再加載了,而若是父加載器加載不了,就會交給子加載器加載,這就是雙親委派機制,這就好比作業中遇到了無法處理的事,你會去請示直接領導,直接領導處理不了,再找上層領導,然后上層領導覺得這是個小事,不用他親自動手,就讓你的直接領導去做,接著他又交給你去做等等,下面來看看每個類加載器的具體作用:
- BootstrapClassLoader:啟動類加載器,顧名思義,這個類加載器主要負責加載JDK lib包,以及-Xbootclasspath引數指定的目錄,并且虛擬機對檔案名進行了限定,也就是說即使我們自己寫個jar放入到上述目錄,也不會被加載,由于該類加載器是C++使用,所以我們的Java程式中無法直接參考,呼叫java.lang.ClassLoader.getClassLoader()方法時默認回傳的是null,
- ExtClassLoader:擴展類加載器,主要負責加載JDK lib/ext包,以及被系統變數java.ext.dirs指向的所有類別庫,這個類別庫可以存放我們自己寫的通用jar,
- AppClassLoader:應用程式類加載器,負責加載用戶classpath上的所有類,它是java.lang.ClassLoader.getSystemClassLoader()的回傳值,也是我們程式的默認類加載器(如果我們沒有自定義類加載器的話),
通過這三個類加載以及雙親委派機制,一個顯而易見的好處就是,不同的類隨它的類加載器天然具有了加載優先級,像Object、String等等這些核心類別庫自然就會在我們的應用程式類之前被加載,使得程式更安全,不會出現錯誤,Spring的父子容器也是這樣的一個設計,通過下面這段代碼可以看到每個類所對應的類加載器:
public class ClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader()); //啟動類加載器
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());//拓展類加載器
System.out.println(ClassLoader.class.getClassLoader());//應用程式類加載器
}
}
輸出:
null
sun.misc.Launcher$ExtClassLoader@4b67cf4d
sun.misc.Launcher$AppClassLoader@14dad5dc
破壞雙親委派模型
剛剛我舉了作業中的一個例子來說明雙親委派機制,但現實中我們不需要事事都去請示領導,同樣類加載器也不是完全遵循雙親委派機制,在必要的時候是可以打破這個規則的,下面列舉四個破壞的情況,在此之前我們需要先了解下雙親 委派的代碼實作原理,在java.lang.ClassLoader類中有一個loadClass以及findClass方法:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
從上面可以看到首先是呼叫parent去加載類,沒有加載到才呼叫自身的findClass方法去加載,也就是說用戶在實作自定義類加載器的時候需要覆寫的是fiindClass而不是loadClass,這樣才能滿足雙親委派模型,
下面具體來看看破壞雙親委派的幾個場景,
第一次
第一次破壞是在雙親委派模型出現之前, 因為該模型是在JDK1.2之后才引入的,那么在此之前,抽象類java.lang.ClassLoader就已經存在了,用戶自定義的類加載器都會去覆寫該類中的loadClass方法,所以雙親委派模型出現后,就無法避免用戶覆寫該方法,因此新增了findClass引導用戶去覆寫該方法實作自己的類加載邏輯,
SPI
第二次破壞是由于這個模型本身缺陷導致的,因為該模型保證了類的加載優先級,但是有些介面是Java定義在核心類別庫中,但具體的服務實作是由用戶提供的,這時候就不得不破壞該模型才能實作,典型的就是Java中的SPI機制(對SPI不了解的讀者可以翻閱我之前的文章或是其它資料,這里不進行闡述),J
DBC的驅動加載就是SPI實作的,所以直接看到java.sql.DriverManager類,該類中有一個靜態初始化塊:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
主要看ServiceLoader.load方法,這個就是通過SPI去加載我們引入java.sql.Driver實作類(比如引入mysql的驅動包就是com.mysql.cj.jdbc.Driver):
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
這個方法主要是從當前執行緒中獲取類加載器,然后通過這個類加載器去加載驅動實作類(這個叫執行緒背景關系類加載器,我們也可以使用這個技巧去打破雙親委派),那這里會獲取到哪一個類加載器呢?具體的設定是在sun.misc.Launcher類的構造器中:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
可以看到設定的就是AppClassLoader,你可能會有點疑惑,這個類加載器加載類的時候不也是先呼叫父類加載器加載么,怎么就打破雙親委派了呢?其實打破雙親委派指的就是類的層次結構,延伸意思就是類的加載優先級,這里本應該是在加載核心類別庫的時候卻提前將我們應用程式中的類別庫給加載到虛擬機中來了,
Tomcat

上圖是Tomcat類加載的類圖,前面三個不用說,CommonClassLoader、CatalinaClassLoader、SharedClassLoader、WebAppClassLoader、JspClassLoader則是Tomcat自己實作的類加載器,分別加載common包、server包、shared包、WebApp/WEB-INF/lib包以及JSP檔案,前面三個在tomcat 6之后已經合并到根目錄下的lib目錄下,而WebAppClassLoader則是每一個應用程式對應一個,JspClassLoader是每一個JSP檔案都會對應一個,并且這兩個類加載器都沒有父類加載器,這也就違背了雙親委派模型,
為什么每個應用程式需要單獨的WebAppClassLoader實體?因為每個應用程式需要彼此隔離,假如在兩個應用中定義了一樣的類(完全限定名),如果遵循雙親委派那就只會存在一份了,另外不同的應用還有可能依賴同一個類別庫的不同版本,這也需要隔離,所以每一個應用程式都會對應一個WebAppClassLoader,它們共享的類別庫可以讓SharedClassLoader加載,另外這些類加載加載的類對Tomcat本身來說也是隔離的(CatalinaClassLoader加載的),
為什么每個JSP檔案需要對應單獨的一個JspClassLoader實體?這是由于JSP是支持運行時修改的,修改后會丟棄掉之前編譯生成的class,并重新生成一個JspClassLoader實體去加載新的class,
以上就是Tomcat為什么要打破雙親委派模型的原因,
OSGI
OSGI是用于實作模塊熱部署,像Eclipse的插件系統就是利用OSGI實作的,這個技術非常復雜同時使用的也越來越少了,感興趣的讀者可自行查閱資料學習,這里不再進行闡述,
總結
類加載的程序讓我們了解到一個類是如何被加載到記憶體中,需要經過哪些階段;而類加載器和雙親委派模型則是告訴我們應該怎么去加載類、類的加載優先級是怎樣的,其中的設計思想我們也可以學習借鑒;最后需要深刻理解的是為什么需要打破雙親委派,在遇到相應的場景時應該怎么做,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/116760.html
標籤:Java
