解析Java虛擬機(jī)中類的初始化及加載器的父委托機(jī)制
類的初始化
在初始化階段,Java虛擬機(jī)執(zhí)行類的初始化語句,為類的靜態(tài)變量賦予初始值。
在程序中,靜態(tài)變量的初始化有兩種途徑:
1.在靜態(tài)變量的聲明處進(jìn)行初始化;
2.在靜態(tài)代碼塊中進(jìn)行初始化。
沒有經(jīng)過顯式初始化的靜態(tài)變量將原有的值。
一個比較奇怪的例子:
package com.mengdd.classloader; class Singleton { // private static Singleton mInstance = new Singleton();// 位置1 // 位置1輸出: // counter1: 1 // counter2: 0 public static int counter1; public static int counter2 = 0; private static Singleton mInstance = new Singleton();// 位置2 // 位置2輸出: // counter1: 1 // counter2: 1 private Singleton() { counter1++; counter2++; } public static Singleton getInstantce() { return mInstance; } } public class Test1 { public static void main(String[] args) { Singleton singleton = Singleton.getInstantce(); System.out.println("counter1: " + Singleton.counter1); System.out.println("counter2: " + Singleton.counter2); } }
可見將生成對象的語句放在兩個位置,輸出是不一樣的(相應(yīng)位置的輸出已在程序注釋中標(biāo)明)。
這是因為初始化語句是按照順序來執(zhí)行的。
靜態(tài)變量的聲明語句,以及靜態(tài)代碼塊都被看做類的初始化語句,Java虛擬機(jī)會按照初始化語句在類文件中的先后順序來依次執(zhí)行它們。
類的初始化步驟
1.假如這個類還沒有被加載和連接,那就先進(jìn)行加載和連接。
2.假如類存在直接的父類,并且這個父類還沒有被初始化,那就先初始化直接的父類。
3.假如類中存在初始化語句,那就依次執(zhí)行這些初始化語句。
類的初始化時機(jī)
Java程序?qū)︻惖氖褂梅绞娇梢苑譃閮煞N:
1.主動使用
2.被動使用
所有的Java虛擬機(jī)實現(xiàn)必須在每個類或接口被Java程序首次主動使用時才初始化它們。
主動使用的六種情況:
1.創(chuàng)建類的實例。
new Test();
2.訪問某個類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值。
int b = Test.a; Test.a = b;
3.調(diào)用類的靜態(tài)方法
Test.doSomething();
4.反射
Class.forName(“com.mengdd.Test”);
5.初始化一個類的子類
class Parent{ } class Child extends Parent{ public static int a = 3; } Child.a = 4;
6.Java虛擬機(jī)啟動時被標(biāo)明為啟動類的類
java com.mengdd.Test
除了以上六種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導(dǎo)致類的初始化。
接口的特殊性
當(dāng)Java虛擬機(jī)初始化一個類時,要求它的所有父類都已經(jīng)被初始化,但是這條規(guī)則并不適用于接口。
在初始化一個類時,并不會先初始化它所實現(xiàn)的接口。
在初始化一個接口時,并不會先初始化它的父接口。
因此,一個父接口并不會因為它的子接口或者實現(xiàn)類的初始化而初始化,只有當(dāng)程序首次使用特定接口的靜態(tài)變量時,才會導(dǎo)致該接口的初始化。
final類型的靜態(tài)變量
final類型的靜態(tài)變量是編譯時常量還是變量,會影響初始化語句塊的執(zhí)行。
如果一個靜態(tài)變量的值是一個編譯時的常量,就不會對類型進(jìn)行初始化(類的static塊不執(zhí)行);
如果一個靜態(tài)變量的值是一個非編譯時的常量,即只有運行時會有確定的初始化值,則就會對這個類型進(jìn)行初始化(類的static塊執(zhí)行)。
例子代碼:
package com.mengdd.classloader; import java.util.Random; class FinalTest1 { public static final int x = 6 / 3; // 編譯時期已經(jīng)可知其值為2,是常量 // 類型不需要進(jìn)行初始化 static { System.out.println("static block in FinalTest1"); // 此段語句不會被執(zhí)行,即無輸出 } } class FinalTest2 { public static final int x = new Random().nextInt(100);// 只有運行時才能得到值 static { System.out.println("static block in FinalTest2"); // 會進(jìn)行類的初始化,即靜態(tài)語句塊會執(zhí)行,有輸出 } } public class InitTest { public static void main(String[] args) { System.out.println("FinalTest1: " + FinalTest1.x); System.out.println("FinalTest2: " + FinalTest2.x); } }
主動使用的歸屬明確性
只有當(dāng)程序訪問的靜態(tài)變量或靜態(tài)方法確實在當(dāng)前類或當(dāng)前接口中定義時,才可以認(rèn)為是對類或接口的主動使用。
package com.mengdd.classloader; class Parent { static int a = 3; static { System.out.println("Parent static block"); } static void doSomething() { System.out.println("do something"); } } class Child extends Parent { static { System.out.println("Child static block"); } } public class ParentTest { public static void main(String[] args) { System.out.println("Child.a: " + Child.a); Child.doSomething(); // Child類的靜態(tài)代碼塊沒有執(zhí)行,說明Child類沒有初始化 // 這是因為主動使用的變量和方法都是定義在Parent類中的 } }
ClassLoader類
調(diào)用ClassLoader類的loadClass()方法加載一個類,并不是對類的主動使用,不會導(dǎo)致類的初始化。
package com.mengdd.classloader; class CL { static { System.out.println("static block in CL"); } } public class ClassLoaderInitTest { public static void main(String[] args) throws Exception { ClassLoader loader = ClassLoader.getSystemClassLoader(); Class<?> clazz = loader.loadClass("com.mengdd.classloader.CL"); // loadClass方法加載一個類,并不是對類的主動使用,不會導(dǎo)致類的初始化 System.out.println("----------------"); clazz = Class.forName("com.mengdd.classloader.CL"); } }
類加載器的父委托機(jī)制
類加載器
類加載器用來把類加載到Java虛擬機(jī)中。
類加載器的類型
有兩種類型的類加載器:
1.JVM自帶的加載器:
根類加載器(Bootstrap)
擴(kuò)展類加載器(Extension)
系統(tǒng)類加載器(System)
2.用戶自定義的類加載器:
java.lang.ClassLoader的子類,用戶可以定制類的加載方式。
JVM自帶的加載器
Java虛擬機(jī)自帶了以下幾種加載器。
1.根(Bootstrap)類加載器:
該加載器沒有父加載器。
它負(fù)責(zé)加載虛擬機(jī)的核心類庫,如java.lang.*等。
根類加載器從系統(tǒng)屬性sun.boot.class.path所指定的目錄中加載類庫。
根類加載器的實現(xiàn)依賴于底層操作系統(tǒng),屬于虛擬機(jī)的實現(xiàn)的一部分,它并沒有繼承java.lang.ClassLoader類,它是用C++寫的。
2.擴(kuò)展(Extension)類加載器:
它的父加載器為根類加載器。
它從java.ext.dirs系統(tǒng)屬性所指定的目錄中加載類庫,或者從JDK的安裝目錄的jre\lib\ext子目錄(擴(kuò)展目錄)下加載類庫,如果把用戶創(chuàng)建的JAR文件放在這個目錄下,也會自動由擴(kuò)展類加載器加載。
擴(kuò)展類加載器是純Java類,是java.lang.ClassLoader類的子類。
3.系統(tǒng)(System)類加載器:
也稱為應(yīng)用類加載器,它的父加載器為擴(kuò)展類加載器。
它從環(huán)境變量classpath或者系統(tǒng)屬性java.class.path所指定的目錄中加載類,它是用戶自定義的類加載器的默認(rèn)父加載器。
系統(tǒng)類加載器是純Java類,是java.lang.ClassLoader類的子類。
注意:這里的父加載器概念并不是指類的繼承關(guān)系,子加載器不一定繼承了父加載器(其實是組合的關(guān)系)。
用戶自定義類加載器
除了以上虛擬機(jī)自帶的類加載器以外,用戶還可以定制自己的類加載器(User-defined Class Loader)。
Java提供了抽象類java.lang.ClassLoader,所有用戶自定義的類加載器都應(yīng)該繼承ClassLoader類。
類加載的父委托機(jī)制
從JDK 1.2版本開始,類的加載過程采用父親委托機(jī)制,這種機(jī)制能更好地保證Java平臺的安全。
在父委托機(jī)制中,除了Java虛擬機(jī)自帶的根類加載器以外,其余的類加載器都有且只有一個父加載器,各個加載器按照父子關(guān)系形成了樹形結(jié)構(gòu)。
當(dāng)Java程序請求加載器loader1加載Sample類時,loader1首先委托自己的父加載器去加載Sample類,若父加載器能加載,則由父加載器完成加載任務(wù),否則才由loader1本身加載Sample類。
說明具體過程的一個例子:
loader2首先從自己的命名空間中查找Sample類是否已經(jīng)被加載,如果已經(jīng)加載,就直接返回代表Sample類的Class對象的引用。
如果Sample類還沒有被加載,loader2首先請求loader1代為加載,loader1再請求系統(tǒng)類加載器代為加載,系統(tǒng)類加載器再請求擴(kuò)展類加載器代為加載,擴(kuò)展類加載器再請求根類加載器代為加載。
若根類加載器和擴(kuò)展類加載器都不能加載,則系統(tǒng)類加載器嘗試加載,若能加載成功,則將Sample類所對應(yīng)的Class對象的引用返回給loader1,loader1再返回給loader2,從而成功將Sample類加載進(jìn)虛擬機(jī)。
若系統(tǒng)加載器不能加載Sample類,則loader1嘗試加載Sample類,若loader1也不能成功加載,則loader2嘗試加載。
若所有的父加載器及l(fā)oader2本身都不能加載,則拋出ClassNotFoundException異常。
總結(jié)下來就是:
每個加載器都優(yōu)先嘗試用父類加載,若父類不能加載則自己嘗試加載;若成功則返回Class對象給子類,若失敗則告訴子類讓子類自己加載。所有都失敗則拋出異常。
定義類加載器和初始類加載器
若有一個類加載器能成功加載Sample類,那么這個類加載器被稱為定義類加載器。
所有能成功返回Class對象的引用的類加載器(包括定義類加載器,即包括定義類加載器和它下面的所有子加載器)都被稱為初始類加載器。
假設(shè)loader1實際加載了Sample類,則loader1為Sample類的定義類加載器,loader2和loader1為Sample類的初始類加載器。
父子關(guān)系
需要指出的是,加載器之間的父子關(guān)系實際上指的是加載器對象之間的包裝關(guān)系,而不是類之間的繼承關(guān)系。
一對父子加載器可能是同一個加載器類的兩個實例,也可能不是。
在子加載器對象中包裝了一個父加載器對象。
例如loader1和loader2都是MyClassLoader類的實例,并且loader2包裝了loader1,loader1是loader2的父加載器。
當(dāng)生成一個自定義的類加載器實例時,如果沒有指定它的父加載器(ClassLoader構(gòu)造方法無參數(shù)),那么系統(tǒng)類加載器就將成為該類加載器的父加載器。
父委托機(jī)制優(yōu)點
父親委托機(jī)制的優(yōu)點是能夠提高軟件系統(tǒng)的安全性。
因為在此機(jī)制下,用戶自定義的類加載器不可能加載應(yīng)該由父加載器加載的可靠類,從而防止不可靠甚至惡意的代碼代替由父加載器加載的可靠代碼。
例如,java.lang.Object類總是由根類加載器加載,其他任何用戶自定義的類加載器都不可能加載含有惡意代碼的java.lang.Object類。
命名空間
每個類加載器都有自己的命名空間,命名空間由該加載器及所有父加載器所加載的類組成。
在同一個命名空間中,不會出現(xiàn)類的完整名字(包括類的包名)相同的兩個類。
在不同的命名空間中,有可能會出現(xiàn)類的完整名字(包括類的包名)相同的兩個類。
運行時包
由同一類加載器加載的屬于相同包的類組成了運行時包。
決定兩個類是不是屬于同一個運行時包,不僅要看它們的包名是否相同,還要看定義類加載器是否相同。
只有屬于同一運行時包的類才能互相訪問包可見(即默認(rèn)訪問級別)的類和類成員。
這樣的限制能避免用戶自定義的類冒充核心類庫的類,去訪問核心類庫的包可見成員。
假設(shè)用戶自己定義了一個類java.lang.Spy,并由用戶自定義的類加載器加載,由于java.lang.Spy和核心類庫java.lang.*由不同的類加載器加載,它們屬于不同的運行時包,所以java.lang.Spy不能訪問核心類庫java.lang包中的包可見成員。
相關(guān)文章
Java正則校驗密碼至少包含字母數(shù)字特殊符號中的2種實例代碼
正則表達(dá)式驗證密碼功能在項目中經(jīng)常被使用到,但是很多朋友還是不大會使用密碼正則表達(dá)式進(jìn)行驗證,下面這篇文章主要給大家介紹了關(guān)于Java正則校驗密碼至少包含字母數(shù)字特殊符號中2種的相關(guān)資料,需要的朋友可以參考下2022-08-08Spring框架+jdbcTemplate實現(xiàn)增刪改查功能
這篇文章主要介紹了Spring框架+jdbcTemplate實現(xiàn)增刪改查功能,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09Mybatis特殊字符轉(zhuǎn)義查詢實現(xiàn)
本文主要介紹了Mybatis特殊字符轉(zhuǎn)義查詢實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02在SpringBoot中注入RedisTemplate實例異常的解決方案
這篇文章主要介紹了在SpringBoot中注入RedisTemplate實例異常的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01IDEA快速部署Spring?Boot?項目到Docker的實現(xiàn)方法
本文主要介紹了IDEA快速部署Spring?Boot?項目到Docker的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07使用Spring開啟@Async異步方式(javaconfig配置)
這篇文章主要介紹了使用Spring開啟@Async異步方式(javaconfig配置),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08