JAVA類的加載、連接與初始化
類的聲明周期總共分為5個步驟1、加載2、連接3、初始化4、使用5、卸載
當java程序需要某個類的時候,java虛擬機會確保這個類已經被加載、連接和初始化,而連接這個類的過程分為3個步驟
1、 加載:查詢並加載這個類的二進制數據
類的加載是指把.class文件中的二進制數據讀入到內從中,把他放在運行時的數據區的方法區內,后在堆區創建一個Class的對象,用來封裝類在方法區內的數據結構
java虛擬機可以從多種來源加載類的二進制數據,
a) 從本地文件系統中加載類的.class文件,常用的方式
b) 通過網絡下載.class文件
c) 從ZIP、JAR或其他類型提取.class文件
d) 從一個專有數據庫中提取.class文件
e) 把一個Java源文件動態編譯為.class文件
類的加載最終產品時位於運行時數據區的堆區的Class對象,Class對象封裝了類在方法區的數據結構,並向java程序提供類訪問該類在方法區數據結構的接口

類的加載是由類的加載器完成的,類的加載器分為兩種:
a) java虛擬機自帶的加載器,包括啟動類加載器,擴展類加載器,和系統類家在西
b) 用戶自定義加載器:ClassLoader類的子類,用戶可以通過實現該類來定制類的加載方式
類的加載器並不需要等到某個類被首次使用時初始化,java虛擬機規范允許類加載器在預料某個類將要被使用時優先加載它,如果在預先加載的過程沒有找到.class文件或者存在錯誤,類加載器必須等到程序首次主動使用該類時才拋出LinkageError異常,如果這個類一直沒有被程序主動使用,則類加載器不會拋出異常
2、 連接:包括驗證階段、准備階段、和解析二進制數據階段
a) 驗證階段:驗證類的正確性,如類的語法
類的驗證目的是確定java類二進制數據的正確性,也因為java虛擬機不知道.class文件是如何被創建的,有可能是正常創建,也有可能是黑客特質破壞虛擬機的所以要有驗證環節,提高程序的健壯性
類的驗證包括:
01、 類文件的結構檢查:確保類文件遵循java類的固定格式
02、 語義檢查:確保類本身符合java語法規定,如驗證final修飾的類是否有子類、final修飾的方法是否有重寫
03、 字節碼驗證:確保字節碼流可以被java虛擬機正確的執行,它是由操作碼的單字節指令組成的序列,每一個操作碼都跟着一個或多個操作數,java虛擬機會驗證該操作數是否合法
04、 二進制兼容驗證:確保類與類之間引用的協調性;如A類中a方法引用B類中b方法,虛擬機會驗證A類時會檢查方法區內是否有B類的b方法,如果不存在或不兼容時,會拋出NoSuchMethodError異常
b) 准備階段:為類的靜態變量分配內存空間,並將其賦予默認值,注意是初始值 如staticint類型初始值為0
c) 解析階段:將類中的符號引用轉換為直接引用,主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符類符號引用進行。如在A類中a方法中引用了B類中的b方法,將b方法放入A類中
3、 初始化:為類的靜態變量賦予正確的初始值;如static inta=50;這時50才賦給a這個靜態變量,所有java虛擬機在每隔類或接口被java程序首次主動使用時才初始化它們,並且初始化階段時執行類構造器<clinit>()方法的過程,<clinit>()方法是由編譯器自動收集類中的所有類變量賦值動作與靜態代碼塊中的語句合並產生的,編譯器收集順序是由語句在源文件出現的順序所決定的;如靜態代碼塊只能訪問定義在靜態代碼塊之前的變量,而在之后定義的變量可以賦值,但不可以訪問,如:
publicclass Test{
static{
i=1
System.out.println(i)//該行代碼會編譯錯誤:Cannot reference a field before it is defined
}
static int i;
}
上面說到<clinit>()方法,<init>()方法這兩個方法是class文件的兩種編譯產生方法
它們的區別
<clinit>方法是在虛擬機裝在一個類的時候調用<clinit>方法。而<init>方法則是在一個類實例化的時候調用
<clinit>方法與<init>方法的不同是<clinit>不需要顯示的調用父類構造器,虛擬機會在保證子類<init>方法執行之前父類的<clinit>方法已經執行完畢,着也就意味着在父類中定義的靜態語句會優先於子類變量以及靜態代碼塊
而<clinit>方法對於類或接口不是必須的,如:一個類或者接口沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器可以不為該類生產<clinit>方法,因此,一個父接口不會因為子接口或實現類的初始化而初始化,只有當程序首次使用父接口或特定接口中的靜態變量時才會導致初始化;
虛擬機會保證一個類<clinit>方法在多線程環境中被正確的加鎖、同步,如果由多個線程同時去初始化一個類,那么只有一個類回去執行<clinit>操作,其他線程會阻塞等待,直到該類<clinit>執行完畢,而如果一個類的<clinit>方法要做的事情很多,就可能造成多個線程阻塞,在實際應用中這種阻塞是被隱藏的
測試代碼:
public class Text{
static class DeadLoopClass
{
static
{
if(true)
{
System.out.println(Thread.currentThread()+"init DeadLoopClass");
while(true)
{
}
}
}
}
public static void main(String[] args) {
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread()+" start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread()+" run over");
}
};
Thread a=new Thread(runnable);
Thread b=new Thread(runnable);
a.start();
b.start();
}
}
運行結果:(即一條線程在死循環以模擬長時間操作,另一條線程在阻塞等待)
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-1,5,main]init DeadLoopClass
需要注意的是:雖然其他線程會被阻塞,但如果執行了<clinit>方法那條線程退出<clinit>方法之后,其他線程喚醒之后也不會在進入<clinit>方法,在同一個類加載器下,一個類型只會被初始化一次
測試代碼:(將靜態內部類代碼換為)
static class DeadLoopClass{
static
{
System.out.println(Thread.currentThread() + "init DeadLoopClass");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
執行結果:
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main]init DeadLoopClass
Thread[Thread-0,5,main] run over
Thread[Thread-1,5,main] run over
虛擬機嚴格規定了有5中情況必須要對類進行初始化
1、 當遇到new,getstatic,putstatic,invokestatic這種失調字節碼指定時,如果類沒有進行初始化則先初始化,生成這4條指令的常見java場景:使用new關鍵字創建對象時、讀取或者設置一些類的靜態字段(被final修飾或已經在編譯器常量池的靜態字段除外,但final類型的靜態常量,如果在編譯是不能計算出常量的取值,則會看作對類的主動使用,會初始化該類)或靜態方法的時候,以及要調用靜態方法的時候
2、 當使用java.lang.reflect包進行反射調用的時候如果類沒有初始化,則先初始化
3、 當初始化一個類的時候發現父類沒有進行初始化,這時要先初始化父類
4、 虛擬機啟動時,用戶要執行的主類(如包含main方法的類),如果沒有初始化要先進行初始化
5、 當使用jdk1.7動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。
當定義數組的引用類不會觸發此類的初始化階段
測試代碼
public static void main(String[] args) {
DeadLoopClass[] loopClasses=new DeadLoopClass[10];
}
結果什么都沒有
常量在編譯階段會存入類的常量池中,本質想沒有直接引用定義常量的這個類,所以不會觸發該類的初始化
測試代碼:
public class Text{
final static String mm="abc";
static{
System.out.println("執行了靜態代碼塊");
}
}
public class Test01 {
public static void main(String[] args) {
System.out.println(Text.mm);
}
}
注意:main方法不可以寫在Text類中因為含有main方法的類會被初始化
初始化大體步驟
1、 假如這個類還滅有被加載和連接,那就先加載和連接
2、 假如類中存在直接的父類,或者間接父類,並卻該父類沒有被初始化,則先初始化父類
3、 類中的初始化語句從上到下執行
類的加載器
java虛擬機自帶了以下幾種加載器
根(bootstrap)加載器:該加載器沒有父類加載器。他負責加載虛擬機核心類庫,如java.lang.*等。從下面例子可以看出java.lang.Object就是由根類加載器加載的,根加載器是從系統屬性sun.boot.class.path所指定的目錄加載類庫。根加載器的實現依賴於底層的操作系統,屬於虛擬機實現的一部分。
擴展(Extension)類加載器:它的父加載器是根加載器。他從java.ext.dirs系統屬性所指定的目錄中加載類庫,或者從JDK的安裝目錄的jre\lib\ext子目錄(擴展目錄)下加載類庫,如果把用戶JAR文件放在該目錄下,會自動由擴展類加載,擴展類加載器是純java類,是java.lang.ClassLoader類的子類
系統(System)類加載器:也成應用加載器,它的父類是擴展類加載器,它的環境變量classpath或者系統屬性java.class.path所指定的目錄的加載類,它是用戶自定義的類加載器的默認父加載器。系統加載器是純java類,是java.lang.ClassLoader類的子類
用戶自定義類加載器:實現java提供的系統加載器ClassLoader抽象類
類加載器關系圖:

測試Object的類加載器是根加載器:
public class Text{
public static void main(String[] args) {
Class c;
ClassLoader cl,cll;
cl=ClassLoader.getSystemClassLoader();//獲取系統加載器
System.out.println(cl);
while(cl!=null){
cll=cl;
cl=cl.getParent();
System.out.println(cll+"這個類的父類加載器是"+cl);
}
try {
c = Class.forName("java.lang.Object");
cll=c.getClassLoader();//獲取Object的類加載器
System.out.println("Object的類加載器是"+cll);
c = Class.forName("test.Text");
cll=c.getClassLoader();//獲取當前類的加載器
System.out.println("Text的類加載器是"+cll);
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印結果:
sun.misc.Launcher$AppClassLoader@6fd7bd04
sun.misc.Launcher$AppClassLoader@6fd7bd04這個類的父類
加載器是sun.misc.Launcher$ExtClassLoader@3cba8af9
sun.misc.Launcher$ExtClassLoader@3cba8af9這個類的父類加載器是null
Object的類加載器是null
Text的類加載器是sun.misc.Launcher$AppClassLoader@6fd7bd04
第一行獲取了系統加載器
第二行是系統加載器的父加載器是擴展加載器,
第三行是系統加載器的父加載器是null(根加載器,根加載器是用null表示,這是為了保護虛擬機的安全,防止黑客利用根加載器,加載非法的類,從而去破壞虛擬機)
第四行Text的類加載器是系統加載器ClassLoader類加載
類加載器的父親委托(Parent Delegation)機制
在父親委托機制種,每個加載器都按照父子關系形成樹形結構,出了類的跟加載器以外,其他加載器都有且只有一個父類加載器,
例:
loader2繼承了loader1,loader1繼承了ClassLoader這時loader2去加載一個類
Class c=loader2.loadClass(“Text”)
執行過程是loader2回去自己的命名空間查找該類是否被加載,如被加載直接返回該類的Class引用
如果沒有加載則loader2會請求loader1代加載,loader1在請求ClassLoader,ClassLoder在請求擴展加載器,擴展加載器在請求根加載器,如果父類加載器不能加載則子類加載器加載,一次類推,有一級可以加載則返回Class引用,如果所有加載器都不能加載則拋出ClassNotFoundException
如有一個類能夠成功加載Text類,那么這個類加載器被稱為定義加載器,能夠返回Class對象引用的類加載器和定義類加載器都稱為初始類加載器,
假設loader1成功加載了Text類那么loader1是定義類加載器,而loader2是Text類的引用加載器,所以loader1,loader2都是Text的初始類加載器
注意:加載器之間的父子關系實際上是加載器對象之間的包裝關系,而不是類之間的繼承關系,一對父子加載器可能是同一個加載器類的兩個實例,也可能不是。在子加載器對象中包裝了一個父加載器對象,如:
public class MyClassLoader extends ClassLoader{
private ClassLoader loader;
public MyClassLoader(){}
public Test01(ClassLoader loader) {
super(loader);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
}
ClassLoaderloader1=new MyClassLoader();
ClassLoader loader2=new MyClassLoader(loader1);
父親委托機制的優點是提高軟件體統的安全性。因為在刺激之下,用戶自定義的加載器不可能加載父類加載器的可靠類,從而防止不可靠的代碼去代替父加載器去加載可靠的代碼,例如java.lang.Object類只能由根類加載器加載,其他用戶任何自定義的類加載器都不不可能加載含有惡意代碼的Object類
命名空間
每個類加載器都有自己的命名空間,命名空間是由該加載器以所有父加載器所加載的類組成。在同一個命名空間中不會出現類的完全限定名一樣的兩個類,但在不同的命名空間中有可能會出現兩個完全限定名一樣的類
當同一個.class文件被一個用戶自定義loader1加載器加載和用戶自定義loader3加載器加載這時方法區會生成兩個該class類,也就是說在loader1和loader3各自的命名空間中都存在Sample和Dog類

不同的類加載器的命名空間存在以下關系:
1、 同意命名空間內的類是相互可見的
2、 子加載器的命名空間包含所有附加在其的命名空間,因此子加載器加載的類能看見父加載器加載的類,如系統加載器加載的類可以看見根加載器加載的類。
3、 由父加載器加載的類對子加載器加載的類是不可見的
4、 兩個加載器之間沒有直接或間接父子關系,則這兩個加載器加載的類相互不可見
所謂A類可見B類是在A類中可以引用B類的名字,如:
class A{
B b=new B();
}
兩個不同的命名空間內的類是項目不可見的,但可以通過java的反射機制來訪問對象的實例與方法。
運行時包
由同一個類加載器加載屬於同包的類組成可運行時包,決定兩個類是不是一運行時包要看它們的包名是否相同,還要看加載器是否相同。只有屬於同意運行時包的類才能訪問默認權限修飾符的類和類的成員,這樣避免了用戶自定義的類去冒充核心類庫的類,如自定義了一個java.lang.Spy,並由用戶自定義的類加載器加載,由於java.lang.Spy和核心類庫不是一個加載器加載的,它們屬於不同運行時包,所以java.lang.Spy不能訪問核心類的java.lang包下面的默認權限修飾符的成員
URLClassLoader類
在JDK的java.net包中,提供了一個功能強大的URLClassLoader類,它不僅能從本地文件中加載類,還可以從網上下載類。java程序可直接用URLClassLoader類作為用戶自定義的類加載器
URLClassLoader類的構造方法:
public URLClassLoader(URL[] urls);//urls是存放URL的數組
public URLClassLoader(URL[] urls, ClassLoader parent)//parent是指定父加載器
URLClassLoader類的默認父加載器是系統加載器
簡單運用:
URLurl=new URL(“www.XXXX.com/java/classes/”);
URLClassLoader loader=new URLClassLoader(new URL[]{url});
Class<?> clazz=loader.loadClass(“XXX”);
clazz.newInstance();
4、 類的卸載
由java虛擬機自帶的類加載器加載的類,在虛擬機的整個聲明周期中,始終不會卸載,如根加載器、擴展加載器、系統加載器,虛擬機本身會始終引用這些類加載器,而類加載器會始終引用它們所加載的Class對象,因此這些Class對象始終是可觸及的;
一個類的何時被卸載的是當該類的Class對象不再被引用時,該類在方法區內的數據也會被卸載
由用戶自定義的類加載器所加載的類是可以被卸載的,在類加載器的內部視線中,是用java集合來存放所有加載類的引用,
運行代碼:


當將引用變量變為null的時候,此時Sample對象結束聲明周期,和類加載器MyClassLoader也結束生命周期這時,Sample類在方法區的二進制數據就被卸載
執行結果:

objClass對象引用的哈希碼改變了說明objClass變量兩次引用了不同的Class對象

