目錄javascript
@
前言
你是否真的理解java的類加載機制?點進文章的盆友不如先來作一道很是常見的面試題,若是你能作出來,可能你早已掌握並理解了java的類加載機制,若結果出乎你的意料,那就頗有必要來了解了解java的類加載機制了。代碼以下java
package com.jvm.classloader; class Father2{ public static String strFather="HelloJVM_Father"; static{ System.out.println("Father靜態代碼塊"); } } class Son2 extends Father2{ public static String strSon="HelloJVM_Son"; static{ System.out.println("Son靜態代碼塊"); } } public class InitativeUseTest2 { public static void main(String[] args) { System.out.println(Son2.strSon); } }
運行結果: Father靜態代碼塊 Son靜態代碼塊 HelloJVM_Son
嗯哼?其實上面程序並非關鍵,可能真的難不倒各位,不妨作下面一道面試題可好?若是下面這道面試題都作對了,那沒錯了,這篇文章你就不用看了,真的。程序員
package com.jvm.classloader; class YeYe{ static { System.out.println("YeYe靜態代碼塊"); } } class Father extends YeYe{ public static String strFather="HelloJVM_Father"; static{ System.out.println("Father靜態代碼塊"); } } class Son extends Father{ public static String strSon="HelloJVM_Son"; static{ System.out.println("Son靜態代碼塊"); } } public class InitiativeUse { public static void main(String[] args) { System.out.println(Son.strFather); } }
各位先用「畢生所學」來猜測一下運行的結果是啥...面試
注意了...
注意了...
注意了...數據庫
運行結果: YeYe靜態代碼塊 Father靜態代碼塊 HelloJVM_Father
是對是錯已經有個數了吧,我就不拆穿各位的當心思了...api
以上的面試題其實就是典型的java類的加載問題,若是你對Java加載機制不理解,那麼你可能就錯了上面兩道題目的。這篇文章將經過對Java類加載機制的講解,讓各位熟練理解java類的加載機制。數組
其實博主仍是想在給出一道題,畢竟各位都已經有了前面兩道題的基礎了,那麼請看代碼:緩存
package com.jvm.classloader; class YeYe{ static { System.out.println("YeYe靜態代碼塊"); } } class Father extends YeYe{ public final static String strFather="HelloJVM_Father"; static{ System.out.println("Father靜態代碼塊"); } } class Son extends Father{ public static String strSon="HelloJVM_Son"; static{ System.out.println("Son靜態代碼塊"); } } public class InitiativeUse { public static void main(String[] args) { System.out.println(Son.strFather); } }
注意了
注意了
注意了安全
運行結果:HelloJVM_Father
衝動的小白童鞋看到了運行結果,果斷的註銷了博客帳戶....網絡
JVM重要的一個領域:類加載
當程序主動使用某個類時,若是該類還未被加載到內存中,則JVM會經過加載、鏈接、初始化3個步驟來對該類進行初始化。若是沒有意外,JVM將會連續完成3個步驟,因此有時也把這個3個步驟統稱爲類加載或類初始化。
而類加載必然涉及類加載器,下面咱們先來了解一下類的加載。
類的加載(類初始化):
一、在java代碼中,類型的加載、鏈接、與初始化過程都是在程序運行期間完成的(類從磁盤加載到內存中經歷的三個階段)【緊緊記在內心】
二、提供了更大的靈活性,增長了更多的可能性
雖然上面的第一句話很是簡短,可是蘊含的知識量倒是巨大的!包含兩個重要的概念:
一、類型
定義的類、接口或者枚舉稱爲類型而不涉及對象,在類加載的過程當中,是一個建立對象以前的一些信息
二、程序運行期間
程序運行期間完成典型例子就是動態代理,其實不少語言都是在編譯期就完成了加載,也正由於這個特性給Java程序提供了更大的靈活性,增長了更多的可能性
一、類加載器並不須要等到某個類被 「首次主動使用」 時再加載它~關於首次主動使用這個重要概念下文將講解~
二、JVM規範容許類加載器在預料某個類將要被使用時就預先加載它
三、若是在預先加載的過程當中遇到了.class
文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError
錯誤)若是這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤。
首先給各位打個預防針:可能沒有了解過JVM的童鞋可能看的很蒙,感受全是理論的感受,不勉強一字一句的「死看」,只要達到一種概念印象就好!等到有必定理解認識以後再回頭看一遍就好不少了,畢竟學習是一種循進漸進的過程,記住沒有捷徑!
從上圖可知,類從被加載到虛擬機內存開始,到卸載出內存爲止,它的整個生命週期包括 7 個階段,而驗證、準備、解析 3 個階段統稱爲鏈接。
加載、驗證、準備、初始化和卸載這 5 個階段的順序是固定肯定的,類的加載過程必須按照這種順序開始(注意是「開始」,而不是「進行」),而解析階段則不必定:它在某些狀況下能夠在初始化後再開始,這是爲了支持 Java 語言的運行時綁定【也就是java的動態綁定/晚期綁定】。
在上面已經提到過,加載階段是類加載的第一個階段!類的加載過程就是從加載階段開始~
加載階段指的是將類的.class
文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,而後在堆區建立一個 java.lang.Class
對象(JVM規範並未說明Class對象位於哪裏,HotSpot虛擬機將其放在方法區中),用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的 Class
對象, Class對象封裝了類在方法區內的數據結構,而且向Java程序員提供了訪問方法區內的數據結構的接口。
Class對象是存放在堆區的,不是方法區,這點不少人容易犯錯。類的元數據纔是存在方法區的。【元數據並非類的Class對象。Class對象是加載的最終產品,類的方法代碼,變量名,方法名,訪問權限,返回值等等都是在方法區的】
JDK7建立Class實例存在堆中;由於JDK7中JavaObjectsInPerm參數值固定爲false。
JDK8移除了永久代,轉而使用元空間來實現方法區,建立的Class實例依舊在java heap(堆)中
編寫一個新的java類時,JVM就會幫咱們編譯成class對象,存放在同名的.class文件中。在運行時,當須要生成這個類的對象,JVM就會檢查此類是否已經裝載內存中。如果沒有裝載,則把.class文件裝入到內存中。如果裝載,則根據class文件生成實例對象。
怎麼理解Class對象與new出來的對象之間的關係呢?
new出來的對象以car
爲例。能夠把car
的Class
類當作具體的一我的,而new car
則是人物映像,具體的一我的(Class
)是惟一的,人物映像(new car
)是多個的。鏡子中的每一個人物映像都是根據具體的人映造出來的,也就是說每一個new
出來的對象都是以Class
類爲模板參照出來的!爲啥能夠參照捏?由於Class對象提供了訪問方法區內的數據結構的接口哇,上面說起過了喔!
算了參照下面這張圖理解吧,理解是其次,重點是話說這妹砸蠻好看的。
總結:
加載階段簡單來講就是:
.class文件(二進制數據)——>讀取到內存——>數據放進方法區——>堆中建立對應Class對象——>並提供訪問方法區的接口
相對於類加載的其餘階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動做)是可控性最強的階段,由於開發人員既可使用系統提供的類加載器來完成加載,也能夠自定義本身的類加載器來完成加載。
加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,並且在Java堆中也建立一個 java.lang.Class
類的對象,這樣即可以經過該對象訪問方法區中的這些數據。
加載.calss文件的方式:
類的加載由類加載器完成,類加載器一般由JVM提供,這些類加載器也是前面全部程序運行的基礎,JVM提供的這些類加載器一般被稱爲系統類加載器。除此以外,開發者能夠經過繼承ClassLoader基類來建立本身的類加載器。經過使用不一樣的類加載器,能夠從不一樣來源加載類的二進制數據,二進制數據一般有以下幾種來源:
(1)從本地系統中直接加載
(2)經過網絡下載.class文件
(3)從zip,jar等歸檔文件中加載.class文件
(4)從專用數據庫中提取.class文件
(5)將java源文件動態編譯爲.class文件
驗證:確保被加載的類的正確性。
關於驗證大可沒必要深刻可是瞭解類加載機制必需要知道有這麼個過程以及知道驗證就是爲了驗證確保Class文件的字節流中包含的信息符合當前虛擬機的要求便可。
因此下面關於驗證的內容做爲了解便可!
驗證是鏈接階段的第一階段,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段大體會完成4個階段的檢驗動做:
文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以 0xCAFEBABE
開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。
元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了 java.lang.Object
以外。
字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。
符號引用驗證:確保解析動做能正確執行。
驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用 -Xverifynone
參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
當完成字節碼文件的校驗以後,JVM 便會開始爲類變量分配內存並初始化。準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。
這裏須要注意兩個關鍵點,即內存分配的對象以及初始化的類型。
內存分配的對象:要明白首先要知道Java 中的變量有類變量以及類成員變量兩種類型,==類變量指的是被 static 修飾的變量==,而==其餘全部類型的變量都屬於類成員變量==。在準備階段,JVM 只會爲類變量分配內存,而不會爲類成員變量分配內存。類成員變量的內存分配須要等到初始化階段纔開始(初始化階段下面會講到)。
舉個例子:例以下面的代碼在準備階段,只會爲 LeiBianLiang
屬性分配內存,而不會爲 ChenYuanBL
屬性分配內存。
public static int LeiBianLiang = 666; public String ChenYuanBL = "jvm";
初始化的類型:在準備階段,JVM 會爲類變量分配內存,併爲其初始化(JVM 只會爲類變量分配內存,而不會爲類成員變量分配內存,類成員變量天然這個時候也不能被初始化)。==可是這裏的初始化指的是爲變量賦予 Java 語言中該數據類型的默認值,而不是用戶代碼裏初始化的值。==
例以下面的代碼在準備階段以後,LeiBianLiang
的值將是 0,而不是 666。
public static int LeiBianLiang = 666;
注意了!!!
注意了!!!
注意了!!!
但若是一個變量是常量(被 static final
修飾)的話,那麼在準備階段,屬性便會被賦予用戶但願的值。例以下面的代碼在準備階段以後,ChangLiang
的值將是 666,而再也不會是 0。
public static final int ChangLiang = 666;
之因此 static final
會直接被複制,而 static
變量會被賦予java語言類型的默認值。其實咱們稍微思考一下就能想明白了。
兩個語句的區別是一個有 final
關鍵字修飾,另一個沒有。而 final
關鍵字在 Java 中表明不可改變的意思,意思就是說 ChangLiang
的值一旦賦值就不會在改變了。既然一旦賦值就不會再改變,那麼就必須一開始就給其賦予用戶想要的值,所以被 final
修飾的類變量在準備階段就會被賦予想要的值。而沒有被 final
修飾的類變量,其可能在初始化階段或者運行階段發生變化,因此就沒有必要在準備階段對它賦予用戶想要的值。
若是還不是很清晰理解final和static關鍵字的話建議參閱下面博主整理好的文章,但願對你有所幫助!
java中的Static、final、Static final各類用法
當經過準備階段以後,進入解析階段。解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,能夠是任何字面量。
直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
==其實這個階段對於咱們來講也是幾乎透明的,瞭解一下就好==。
到了初始化階段,用戶定義的 Java 程序代碼才真正開始執行。
Java程序對類的使用方式可分爲兩種:主動使用與被動使用。通常來講只有當對類的==首次主動使用==的時候纔會致使類的初始化,因此主動使用又叫作類加載過程當中「初始化」開始的時機。那啥是主動使用呢?類的主動使用包括如下六種【超級重點】:
一、 建立類的實例,也就是new的方式
二、 訪問某個類或接口的靜態變量,或者對該靜態變量賦值(凡是被final修飾不不不其實更準確的說是在編譯器把結果放入常量池的靜態字段除外)
三、 調用類的靜態方法
四、 反射(如 Class.forName(「com.gx.yichun」))
五、 初始化某個類的子類,則其父類也會被初始化
六、 Java虛擬機啓動時被標明爲啓動類的類( JavaTest ),還有就是Main方法的類會 首先被初始化
最後注意一點對於靜態字段,只有直接定義這個字段的類纔會被初始化(執行靜態代碼塊),這句話在繼承、多態中最爲明顯!爲了方便理解下文會陸續經過例子講解
當 JVM 完成初始化階段以後,JVM 便開始從入口方法開始執行用戶的程序代碼。這個使用階段也只是瞭解一下就能夠了。
當用戶程序代碼執行完畢後,JVM 便開始銷燬建立的 Class 對象,最後負責運行的 JVM 也退出內存。這個卸載階段也只是瞭解一下就能夠了。
在以下幾種狀況下,Java虛擬機將結束生命週期
一、 執行了 System.exit()方法
二、 程序正常執行結束
三、 程序在執行過程當中遇到了異常或錯誤而異常終止
四、 因爲操做系統出現錯誤而致使Java虛擬機進程終止
接口加載過程與類加載過程稍有不一樣。
==當一個類在初始化時,要求其父類所有都已經初始化過了,可是一個接口在初始化時,並不要求其父接口所有都完成了初始化,當真正用到父接口的時候纔會初始化。==
package com.jvm.classloader; class Father2{ public static String strFather="HelloJVM_Father"; static{ System.out.println("Father靜態代碼塊"); } } class Son2 extends Father2{ public static String strSon="HelloJVM_Son"; static{ System.out.println("Son靜態代碼塊"); } } public class InitativeUseTest2 { public static void main(String[] args) { System.out.println(Son2.strSon); } } 運行結果: Father靜態代碼塊 Son靜態代碼塊 HelloJVM_Son
再回頭看這個題,這也太簡單了吧,因爲Son2.strSon
是調用了Son
類本身的靜態方法屬於主動使用,因此會初始化Son
類,又因爲繼承關係,類繼承原則是初始化一個子類,會先去初始化其父類,因此會先去初始化父類!
再看開篇的第二個題
package com.jvm.classloader; class YeYe{ static { System.out.println("YeYe靜態代碼塊"); } } class Father extends YeYe{ public static String strFather="HelloJVM_Father"; static{ System.out.println("Father靜態代碼塊"); } } class Son extends Father{ public static String strSon="HelloJVM_Son"; static{ System.out.println("Son靜態代碼塊"); } } public class InitiativeUse { public static void main(String[] args) { System.out.println(Son.strFather); } } 運行結果: YeYe靜態代碼塊 Father靜態代碼塊 HelloJVM_Father
這個題就稍微要注意一下,不過要是你看懂這篇文章,這個題也很簡單。這個題要注意什麼呢?要注意子類Son
類沒有被初始化,也就是Son
的靜態代碼塊沒有執行!發現了咩?那咱們來分析分析...
首先看到Son.strFather
,你會發現是子類Son
訪問父類Father
的靜態變量strFather
,這個時候就千萬要記住我在概括主動使用概念時特別提到過的一個注意點了:對於靜態字段,只有直接定義這個字段的類纔會被初始化(執行靜態代碼塊),這句話在繼承、多態中最爲明顯!
嗯哼,對吧,Son.strFather
中的靜態字段是屬於父類Father
的對吧,也就是說直接定義這個字段的類是父類Father
,因此在執行 System.out.println(Son.strFather);
這句代碼的時候會去初始化Father
類而不是子類Son
!是否是一會兒明白了?若是明白了就支持一下博主點個讚唄,謝謝~
再看開篇的第三個題
package com.jvm.classloader; class YeYe{ static { System.out.println("YeYe靜態代碼塊"); } } class Father extends YeYe{ public final static String strFather="HelloJVM_Father"; static{ System.out.println("Father靜態代碼塊"); } } class Son extends Father{ public static String strSon="HelloJVM_Son"; static{ System.out.println("Son靜態代碼塊"); } } public class InitiativeUse { public static void main(String[] args) { System.out.println(Son.strFather); } } 運行結果:HelloJVM_Father
這個題惟一的特色就在於final static
!是的Son.strFather
所對應的變量即是final static
修飾的,依舊是在本篇文章中概括的類的主動使用範疇第二點當中:訪問某個類或接口的靜態變量,或者對該靜態變量賦值(凡是被final修飾不不不其實更準確的說是在編譯器把結果放入常量池的靜態字段除外)
因此,這個題並不會初始化任何類,固然除了Main方法所在的類!因而僅僅執行了System.out.println(Son.strFather);
因此僅僅打印了Son.strFather
的字段結果HelloJVM_Father
,嗯哼,是否是又忽然明白了?若是明白了就再支持一下博主點個讚唄,謝謝~
實際上上面的題目並不能徹底說明本篇文章中概括的類的主動使用範疇第二點!這話怎麼說呢?怎麼理解呢?再來一個程序各位就更加明瞭了
package com.jvm.classloader; import sun.applet.Main; import java.util.Random; import java.util.UUID; class Test{ static { System.out.println("static 靜態代碼塊"); } // public static final String str= UUID.randomUUID().toString(); public static final double str=Math.random(); //編譯期不肯定 } public class FinalUUidTest { public static void main(String[] args) { System.out.println(Test.str); } }
請試想一下結果,會不會執行靜態代碼塊裏的內容呢?
重點來了
重點來了
重點來了
重點來了
運行結果
static 靜態代碼塊 0.7338688977344875
上面這個程序徹底說明本篇文章中概括的類的主動使用範疇第二點當中的這句話:凡是被final修飾不不不其實更準確的說是在編譯器把結果放入常量池的靜態字段除外!
分析:==其實final不是重點,重點是編譯器把結果放入常量池!當一個常量的值並不是編譯期能夠肯定的,那麼這個值就不會被放到調用類的常量池中,這時在程序運行時,會致使主動使用這個常量所在的類,因此這個類會被初始化==
到這裏,能理解完上面三個題已經很不錯了,可是要想更加好好的學習java,博主不得不給各位再來一頓燒腦盛宴,野心不大,只是單純的想巔覆各位對java代碼的認知,固然還望大佬輕拍哈哈哈,直接上代碼:
package com.jvm.classloader; public class ClassAndObjectLnitialize { public static void main(String[] args) { System.out.println("輸出的打印語句"); } public ClassAndObjectLnitialize(){ System.out.println("構造方法"); System.out.println("我是熊孩子個人智商=" + ZhiShang +",情商=" + QingShang); } { System.out.println("普通代碼塊"); } int ZhiShang = 250; static int QingShang = 666; static { System.out.println("靜態代碼塊"); } }
建議這個題不要花太多時間思考,不然看告終果你會發現本身想太多了,致使最後可能你看到結果想砸電腦哈哈哈
隔離運行結果專業跑龍套...
隔離運行結果專業跑龍套...
隔離運行結果專業跑龍套...
隔離運行結果專業跑龍套...
隔離運行結果專業跑龍套...
運行結果 靜態代碼塊 輸出的打印語句
怎麼樣,是否是沒有你想的那麼複雜呢?
下面咱們來簡單分析一下,首先根據上面說到的觸發初始化的(主動使用)的第六點:Java虛擬機啓動時被標明爲啓動類的類( JavaTest ),還有就是Main方法的類會首先被初始化
嗯哼?小白童鞋就有疑問了:不是說好有Main方法的類會被初始化的麼?那怎麼好多東西都沒有執行捏?
那麼類的初始化順序究竟是怎麼樣的呢?在咱們代碼中,咱們只知道有一個構造方法,但實際上Java代碼編譯成字節碼以後,最開始是沒有構造方法的概念的,只有==類初始化方法== 和 ==對象初始化方法== 。
這個時候咱們就不得不深刻理解了!那麼這兩個方法是怎麼來的呢?
類初始化方法:編譯器會按照其出現順序,收集:類變量(static變量)的賦值語句、靜態代碼塊,最終組成類初始化方法。==類初始化方法通常在類初始化的時候執行。==
因此,上面的這個例子,類初始化方法就會執行下面這段代碼了:
static int QingShang = 666; //類變量(static變量)的賦值語句 static //靜態代碼塊 { System.out.println("靜態代碼塊"); }
而不會執行普通賦值語句以及普通代碼塊了
對象初始化方法:編譯器會按照其出現順序,收集:成員變量的賦值語句、普通代碼塊,最後收集構造函數的代碼,最終組成對象初始化方法,值得特別注意的是,若是沒有監測或者收集到構造函數的代碼,則將不會執行對象初始化方法。==對象初始化方法通常在實例化類對象的時候執行。==
以上面這個例子,其對象初始化方法就是下面這段代碼了:
{ System.out.println("普通代碼塊"); //普通代碼塊 } int ZhiShang = 250; //成員變量的賦值語句 System.out.println("構造方法"); //最後收集構造函數的代碼 System.out.println("我是熊孩子個人智商=" + ZhiShang +",情商=" + QingShang);
明白了類初始化方法 和 對象初始化方法 以後,咱們再來看這個上面例子!是的!正如上面提到的:若是沒有監測或者收集到構造函數的代碼,則將不會執行對象初始化方法。上面的這個例子確實沒有執行對象初始化方法。忘了嗎?咱們根本就沒有對類ClassAndObjectLnitialize
進行實例化!只是單純的寫了一個輸出語句。
若是咱們給其實例化,驗證一下,代碼以下:
package com.jvm.classloader; public class ClassAndObjectLnitialize { public static void main(String[] args) { new ClassAndObjectLnitialize(); System.out.println("輸出的打印語句"); } public ClassAndObjectLnitialize(){ System.out.println("構造方法"); System.out.println("我是熊孩子個人智商=" + ZhiShang +",情商=" + QingShang); } { System.out.println("普通代碼塊"); } int ZhiShang = 250; static int QingShang = 666; static { System.out.println("靜態代碼塊"); } } 運行結果: 靜態代碼塊 普通代碼塊 構造方法 我是熊孩子個人智商=250,情商=666 輸出的打印語句
到這裏博主必需要聲明一點了!我爲何要用這些面試題做爲這篇文章的一部分?由於關於學習有必定的方法,你能夠設想一下,若是博主不涉及並分析這幾個面試題,你還有耐心看到這裏嗎?小白槓精童鞋說有。。。好的,就算有,大篇大篇的理論各位扣心自問,能掌握全部知識嗎?小白槓精童鞋說說能。。。額,就算能,那你能保證光記理論一個月不遺忘嗎?小白槓精童鞋說能夠。。。我特麼一老北京布鞋過去頭給你打歪(我這暴脾氣我天)。因此呢學習要帶着興趣、「目的」、「野心」!但願我這段話能對你有所幫助,哪怕是一點點...
我在上面提到過Java程序對類的使用方式可分爲兩種:主動使用與被動使用。通常來講只有當對類的首次主動使用的時候纔會致使類的初始化,其中首次關鍵字很重要,所以特意用一小結將其講解!
怎麼理解呢?老規矩看個題:
package com.jvm.classloader; class Father6{ public static int a = 1; static { System.out.println("父類粑粑靜態代碼塊"); } } class Son6{ public static int b = 2; static { System.out.println("子類熊孩子靜態代碼塊"); } } public class OverallTest { static { System.out.println("Main方法靜態代碼塊"); } public static void main(String[] args) { Father6 father6; System.out.println("======"); father6=new Father6(); System.out.println("======"); System.out.println(Father6.a); System.out.println("======"); System.out.println(Son6.b); } }
請試想一下運行結果
運行結果: Main方法靜態代碼塊 ====== 父類粑粑靜態代碼塊 ====== 1 ====== 子類熊孩子靜態代碼塊 2
分析:
首先根據主動使用歸納的第六點:Main方法的類會首先被初始化。 因此最早執行Main方法靜態代碼塊,而 Father6 father6;
只是聲明瞭一個引用不會執行什麼,當運行到father6=new Father6();
的時候,看到關鍵字new
而且將引用father6
指向了Father6
對象,說明主動使用了,因此父類Father6
將被初始化,所以打印了:父類粑粑靜態代碼塊 ,以後執行 System.out.println(Father6.a);
屬於訪問靜態變量因此也是主動使用,這個時候注意了,由於在上面執行father6=new Father6();
的時候父類已經主動使用而且初始化過一次了,此次再也不是首次主動使用了,因此Father6
不會在被初始化,天然它的靜態代碼塊就再也不執行了,因此直接打印靜態變量值1,然後面的System.out.println(Son6.b);
一樣,也是隻初始化本身,不會去初始化父類,只由於父類Father6
以及再也不是首次主動使用了!明白了沒?若是有疑問歡迎留言,絕對第一時間回覆!
喔o,終於到類加載器內容了!咱們以前講的類加載都是給類加載器作的一個伏筆,在這以前講的全部類被加載都是由類加載器來完成的,可見類加載器是多麼重要。因爲上面的面試題並不涉及類加載器的相關知識,因此到這裏再涉及涉及類加載器的知識!
類加載器負責加載全部的類,其爲全部被載入內存中的類生成一個java.lang.Class
實例對象。一旦一個類被加載入JVM中,同一個類就不會被再次載入了。正如一個對象有一個惟一的標識同樣,一個載入JVM的類也有一個惟一的標識。
關於惟一標識符:
在Java中,一個類用其全限定類名(包括包名和類名)做爲標識;
但在JVM中,一個類用其全限定類名和其類加載器做爲其惟一標識。
類加載器的任務是根據一個類的全限定名來讀取此類的二進制字節流到JVM中,而後轉換爲一個與目標類對應的java.lang.Class
對象實例,在虛擬機提供了3種類加載器,啓動(Bootstrap)類加載器、擴展(Extension)類加載器、系統(System)類加載器(也稱應用類加載器),以下:
站在Java開發人員的角度來看,類加載器能夠大體劃分爲如下三類:
啓動類加載器: BootstrapClassLoader
,啓動類加載器主要加載的是JVM自身須要的類,這個類加載使用C++語言實現的,是虛擬機自身的一部分,負責加載存放在 JDK\jre\lib
(JDK表明JDK的安裝目錄,下同)下,或被 -Xbootclasspath
參數指定的路徑中的,而且能被虛擬機識別的類庫(如rt.jar
,全部的java.開頭的類均被 BootstrapClassLoader
加載)。啓動類加載器是沒法被Java程序直接引用的。==總結一句話:啓動類加載器加載java運行過程當中的核心類庫JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放在JRE\classes裏的類,也就是JDK提供的類等常見的好比:Object、Stirng、List...==
擴展類加載器: ExtensionClassLoader,該加載器由 sun.misc.Launcher$ExtClassLoader實現,它負責加載 JDK\jre\lib\ext目錄中,或者由 java.ext.dirs系統變量指定的路徑中的全部類庫(如javax.開頭的類),開發者能夠直接使用擴展類加載器。
應用程序類加載器: ApplicationClassLoader,該類加載器由 sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者能夠直接使用該類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。==總結一句話:應用程序類加載器加載CLASSPATH變量指定路徑下的類 即指你自已在項目工程中編寫的類==
線程上下文類加載器:除了以上列舉的三種類加載器,還有一種比較特殊的類型就是線程上下文類加載器。相似Thread.currentThread().getContextClassLoader()
獲取線程上下文類加載器
在Java的平常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,咱們還能夠自定義類加載器,由於JVM自帶的類加載器(ClassLoader
)只是懂得從本地文件系統加載標準的java class文件,所以若是編寫了本身的ClassLoader
,即可以作到以下幾點:
一、在執行非置信代碼以前,自動驗證數字簽名。
二、動態地建立符合用戶特定須要的定製化構建類。
三、從特定的場所取得java class,例如數據庫中和網絡中。
須要注意的是,Java虛擬機對class文件採用的是按需加載的方式,也就是說當須要使用該類時纔會將它的class文件加載到內存生成class對象,並且加載某個類的class文件時,Java虛擬機默認採用的是雙親委派模式即把請求交由父類處理,它一種任務委派模式,下面將會詳細講到!
下面咱們看一個程序:
package com.jvm.classloaderQi; public class ClassloaderTest { public static void main(String[] args) { //獲取ClassloaderTest類的加載器 ClassLoader classLoader= ClassloaderTest.class.getClassLoader(); System.out.println(classLoader); System.out.println(classLoader.getParent()); //獲取ClassloaderTest類的父類加載器 System.out.println(classLoader.getParent().getParent()); } }
運行結果:
sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@1b6d3586 null
從上面的結果能夠看出,並無獲取到ExtClassLoader的父Loader,緣由是Bootstrap Loader(啓動類加載器)是用C++語言實現的(這裏僅限於Hotspot,也就是JDK1.5以後默認的虛擬機,有不少其餘的虛擬機是用Java語言實現的),找不到一個肯定的返回父Loader的方式,因而就返回null。至於$符號就是內部類的含義。
我以爲講類加載器,仍是頗有必要知道命名空間這個概念!實際上類加載器的一個必不可少的前提就是命名空間!
命名空間概念:
每一個類加載器都有本身的命名空間,命名空間由該加載器及全部父加載器所加載的類組成。
特別注意:在同一個命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類。
在不一樣的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類。
由子加載器加載的類能看見父加載器的類,由父親加載器加載的類不能看見子加載器加載的類
咱們已經知道每一個類只能被加載一次,其實這樣說是不夠準確的,怎樣纔算是準確的呢?那就涉及到命名空間的概念了!只有在相同的命名空間中,每一個類才只能被加載一次,反過來講就是一個類在不一樣的命名空間中是能夠被加載屢次的,而被加載屢次的Class對象是互相獨立的!
固然,直接把命名空間的概念直接拋給你們,若是沒有接觸過,100%是看不懂其中的含義的,我敢打包票,假一賠100萬。。。那麼博主就舉出寫例子讓各位深入體會一下!固然這些例子涉及自定義加載器的一些知識,建議先對自定義加載器有必定了解在看!
例子必知前提:
一、 本身在idea或者eclipse中建立的工程項目中只要編譯以後都會有對應的class文件成在classPath目錄中
二、 而這些目錄是由ApplicationClassLoader應用加載器加載
三、 我以後會將class文件放到系統桌面地址上,而這些系統地址由自定義加載器指定,因此由自定義加載器加載
事先編譯好,而後將項目工程中的兩個字節碼class文件【File1和File2】拷貝到系統桌面路徑上,編譯main方法就會出如今項目工程(ClassPath)下,注意如下例子狀況中系統桌面路徑的class文件一直都存在!
Main方法狀況:
一、建立一個自定義加載器classloader2
,並聲明桌面class文件路徑,接着加載File1
File1
的加載器三、newInstance
出File1
的實例
File1類的方法狀況:
File1
的構造方法中存在一行代碼:new File2
的new
實例代碼File2類的方法狀況:
File2
的加載器刪除File1
、File2
項目工程中的class文件,工程項目的兩個class文件都刪除(只存在系統桌面路徑下的class文件)
結果:File1
和File2
的加載器都是自定義加載器
只刪除File1
項目工程中的class文件
結果:File1
的加載器是自定義加載器,而執行到File2
實例的加載器是App應用加載器
只刪除File2
項目工程中的class文件
結果:File1
的加載器都是APP應用加載器,而執行到File2實例的時候報NoClassDefFoundError
異常
得出結論:加載一個類(File1
)的時候,這個類裏面調用了其餘的類(File2
)或者其餘類方法的初始化代碼,那麼這裏面的類也會試着從這個類的加載器開始向上委託加載,若是全都加載不了加載不了就報NoClassDefFoundError
異常。
固然這樣理解命名空間和類加載機制仍是遠遠不夠的!
File2類中發生改變狀況以下:
File1
的構造方法中存在一行new File2
的實例這沒變File2
的構造方法中,打印(訪問)File1
的class文件只刪除項目工程中File1
的class文件
結果:File1
的加載器都是自定義加載器,而執行到File2
實例的加載器是App應用加載器,當運行到File2
構造方法中的打印(訪問)File1
的class文件的時候報NoClassDefFoundError
異常
得出結論:父親加載器加載的類(File2
)不能看見子加載器加載的類(File1
)
File1方法發生改變狀況以下:
一、Main方法中newInstance
出File1
的實例,File1
的構造方法中存在一行new File2
的實例這都沒變
二、在File1
的構造方法中,打印File2
的class文件
只刪除File1
項目工程中的class文件
結果:File1
的加載器都是自定義加載器,而執行到File2
實例的加載器是App應用加載器,當運行到File1
構造方法中的打印File2
的class文件的時候沒問題
得出結論:由子加載器加載的類(File1)能看見父加載器的類(File2)
固然還要注意知道的一點的是:若是兩個加載器之間沒有直接或間接的父子關係,那麼它們各自加載類相互不可見。
固然整對上面的狀況仍是至關比較抽象,畢竟沒上代碼,若是有任何疑問,歡迎留言,宜春絕對第一時間回覆!
JVM的類加載機制主要有以下3種。
全盤負責:當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其餘Class也將由該類加載器負責載入,除非顯示使用另一個類加載器來載入
父類委託:先讓父類加載器試圖加載該類,只有在父類加載器沒法加載該類時才嘗試從本身的類路徑中加載該類,通俗講就是兒子們都他麼是懶豬,本身無論能不能作,就算能加載也先不幹,先給本身的父親作,一個一個往上拋,直到拋到啓動類加載器也就是最頂級父類,只有父親作不了的時候再沒辦法由下一個子類作,直到能某一個子類能作才作,以後的子類就直接返回,實力坑爹!
緩存機制:緩存機制將會保證全部加載過的Class都會被緩存,當程序中須要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統纔會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是爲何修改了Class後,必須重啓JVM,程序的修改纔會生效
雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即沒法完成該加載,子加載器纔會嘗試本身去加載該類。也就是實力坑爹!
雙親委派機制:
一、當AppClassLoader加載一個class時,它首先不會本身去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。
二、當 ExtClassLoader加載一個class時,它首先也不會本身去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。
三、若是 BootStrapClassLoader加載失敗(例如在 $JAVA_HOME/jre/lib裏未查找到該class),會使用
ExtClassLoader來嘗試加載;
四、若ExtClassLoader也加載失敗,則會使用 AppClassLoader來加載,若是
AppClassLoader也加載失敗,則會報出異常 ClassNotFoundException。
![]()
從代碼層面瞭解幾個Java中定義的類加載器及其雙親委派模式的實現,它們類圖關係以下:
![]()
從圖能夠看出頂層的類加載器是 抽象類ClassLoader類,其後 全部的類加載器都繼承自ClassLoader(不包括啓動類加載器),爲了更好理解雙親委派模型,ClassLoader源碼中的loadClass(String)方法該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2以後再也不建議用戶重寫但用戶能夠直接調用該方法, loadClass()方法是ClassLoader類本身實現的,該方法中的邏輯就是雙親委派模式的實現,loadClass(String name, boolean resolve)是一個重載方法,resolve參數表明是否生成class對象的同時進行解析相關操做。源碼分析以下::
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先從緩存查找該class對象,找到就不用從新加載 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 // 若是都沒有找到,則經過自定義實現的findClass去查找並加載 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; } }
既然存在這個雙親委派模型,那麼就必定有着存在的意義,其意義主要是:Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係,經過這種層級關能夠避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。其次是考慮到安全因素,java核心api中定義類型不會被隨意替換,假設經過網絡傳遞一個名爲java.lang.Integer的類,經過雙親委託模式傳遞到啓動類加載器,而啓動類加載器在覈心Java API發現這個名字的類,發現該類已被加載,並不會從新加載網絡傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣即可以防止核心API庫被隨意篡改。
雙親委派模型意義總結來說就是:
一、系統類防止內存中出現多份一樣的字節碼
二、保證Java程序安全穩定運行
ClassLoader類是一個抽象類,全部的類加載器都繼承自ClassLoader(不包括啓動類加載器),所以它顯得格外重要,分析ClassLoader抽象類也是很是重要的!
簡單小結一下ClassLoader抽象類中一些概念:
二進制概念(Binary name):格式以下
把二進制名字轉換成文件名字,而後在文件系統中磁盤上讀取其二進制文件(class文件),每個class對象都包含了定義了這個類的classload對象,class類都是由類加載器加載的只有數組類型是有JVM根據須要動態生成。
特別注意數組類型:
一、 數組類的類對象不是由類加載器建立的,而是根據Java運行時的須要自動建立的。
二、 數組類的類加載器getClassLoader()與它的元素類型的類加載器相同;若是元素類型是基本類型,則數組類沒有類加載器也就是null,而這個null不一樣於根類加載器返回的null,它是單純的null。
到這裏,下面就主要分析ClassLoader抽象類中幾個比較重要的方法。
該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2以後再也不建議用戶重寫但用戶能夠直接調用該方法,loadClass()方法是ClassLoader類本身實現的,該方法中的邏輯就是雙親委派模式的實現,其源碼以下,loadClass(String name, boolean resolve)是一個重載方法,resolve參數表明是否生成class對象的同時進行解析相關操做:
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先從緩存查找該class對象,找到就不用從新加載 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 // 若是都沒有找到,則經過自定義實現的findClass去查找並加載 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; } }
正如loadClass方法所展現的,當類加載請求到來時,先從緩存中查找該類對象,若是存在直接返回,若是不存在則交給該類加載去的父加載器去加載,假若沒有父加載則交給頂級啓動類加載器去加載,最後假若仍沒有找到,則使用findClass()方法去加載(關於findClass()稍後會進一步介紹)。從loadClass實現也能夠知道若是不想從新定義加載類的規則,也沒有複雜的邏輯,只想在運行時加載本身指定的類,那麼咱們能夠直接使用this.getClass().getClassLoder.loadClass("className"),這樣就能夠直接調用ClassLoader的loadClass方法獲取到class對象。
在JDK1.2以前,在自定義類加載時,總會去繼承ClassLoader類並重寫loadClass方法,從而實現自定義的類加載類,可是在JDK1.2以後已再也不建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中,從前面的分析可知,findClass()方法是在loadClass()方法中被調用的,當loadClass()方法中父加載器加載失敗後,則會調用本身的findClass()方法來完成類加載,這樣就能夠保證自定義的類加載器也符合雙親委託模式。須要注意的是ClassLoader類中並無實現findClass()方法的具體代碼邏輯,取而代之的是拋出ClassNotFoundException異常,同時應該知道的是findClass方法一般是和defineClass方法一塊兒使用的(稍後會分析),ClassLoader類中findClass()方法源碼以下:
//直接拋出異常 protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
defineClass()方法是用來將byte字節流解析成JVM可以識別的Class對象(ClassLoader中已實現該方法邏輯),經過這個方法不只可以經過class文件實例化class對象,也能夠經過其餘方式實例化class對象,如經過網絡接收一個類的字節碼,而後轉換爲byte字節流建立對應的Class對象,defineClass()方法一般與findClass()方法一塊兒使用,通常狀況下,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法並編寫加載規則,取得要加載類的字節碼後轉換成流,而後調用defineClass()方法生成類的Class對象,簡單例子以下:
protected Class<?> findClass(String name) throws ClassNotFoundException { // 獲取類的字節數組 byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { //使用defineClass生成class對象 return defineClass(name, classData, 0, classData.length); } }
須要注意的是,若是直接調用defineClass()方法生成類的Class對象,這個類的Class對象並無解析(也能夠理解爲連接階段,畢竟解析是連接的最後一步),其解析操做須要等待初始化階段進行。
使用該方法可使用類的Class對象建立完成也同時被解析。前面咱們說連接階段主要是對字節碼進行驗證,爲類變量分配內存並設置初始值同時將字節碼文件中的符號引用轉換爲直接引用。
以上上述4個方法是ClassLoader類中的比較重要的方法,也是咱們可能會常常用到的方法。接看SercureClassLoader擴展了 ClassLoader,新增了幾個與使用相關的代碼源(對代碼源的位置及其證書的驗證)和權限定義類驗證(主要指對class源碼的訪問權限)的方法,通常咱們不會直接跟這個類打交道,更可能是與它的子類URLClassLoader有所關聯,前面說過,ClassLoader是一個抽象類,不少方法是空的沒有實現,好比 findClass()、findResource()等。而URLClassLoader這個實現類爲這些方法提供了具體的實現,並新增了URLClassPath類協助取得Class字節碼流等功能,在編寫自定義類加載器時,若是沒有太過於複雜的需求,能夠直接繼承URLClassLoader類,這樣就能夠避免本身去編寫findClass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。
檢查完父類加載器以後loadClass會去默認調用findClass方法,父類(ClassLoader)中的findClass方法主要是拋出一個異常。
findClass根據二進制名字找到對應的class文件,返回值爲Class對象Class<?>
defineClass這個方法主要是將一個字節數組轉換成Class實例,會拋三個異常,但只是threws一個,由於其餘兩個是運行時異常。
loadClass方法是一個加載一個指定名字的class文件,調用findLoadedClass (String)檢查類是否已經加載…若是已經加裝就再也不加載而是直接返回第一次加載結果 因此一個類只會加載一次
自定義核心目的是擴展java虛擬機的動態加載類的機制,JVM默認狀況是使用雙親委託機制,雖然雙親委託機制很安全極高可是有些狀況咱們須要本身的一種方式加載,==好比應用是經過網絡來傳輸 Java類的字節碼,爲保證安全性,這些字節碼通過了加密處理,這時系統類加載器就沒法對其進行加載,這樣則須要自定義類加載器來實現==。所以自定義類加載器也是頗有必要的。
自定義類加載器通常都是繼承自 ClassLoader類,從上面對 loadClass方法來分析來看,咱們只須要重寫 findClass 方法便可。自定義加載器中點:重寫findClass,下面直接看自定義類加載器代碼的流程:
package com.yichun.classloader; import java.io.*; public class MyClassLoader extends ClassLoader { 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) { String fileName = root + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try { InputStream ins = new FileInputStream(fileName); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; int length = 0; while ((length = ins.read(buffer)) != -1) { baos.write(buffer, 0, length); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } public String getRoot() { return root; } public void setRoot(String root) { this.root = root; } public static void main(String[] args) { MyClassLoader classLoader = new MyClassLoader(); classLoader.setRoot("D:\\dirtemp"); Class<?> testClass = null; try { testClass = classLoader.loadClass("com.yichun.classloader.Demo1"); Object object = testClass.newInstance(); System.out.println(object.getClass().getClassLoader()); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
自定義類加載器的核心在於對字節碼文件的獲取,若是是加密的字節碼則須要在該類中對文件進行解密。上面代碼程序只是簡單Demo,並未對class文件進行加密,所以省略瞭解密的過程。這裏有幾點須要注意:
一、這裏傳遞的文件名須要是類的全限定性名稱,即com.yichun.test.classloading.Test格式的,由於
defineClass 方法是按這種格式進行處理的。
二、最好不要重寫loadClass方法,由於這樣容易破壞雙親委託模式。
三、這類Test 類自己能夠被 AppClassLoader類加載,所以咱們不能把com/yichun/test/classloading/Test.class放在類路徑下。不然,因爲雙親委託機制的存在,會直接致使該類由AppClassLoader加載,而不會經過咱們自定義類加載器來加載。
到這裏,相信你們已經對類的加載以及加載器有必定的瞭解了,那麼你知道嗎,其實加載類常見的有三種方式,以下:
一、靜態加載,也就是經過
new
關鍵字來建立實例對象。
二、動態加載,也就是經過
Class.forName()
方法動態加載(反射加載類型),而後調用類的newInstance()方法實例化對象。
三、動態加載,經過類加載器的
loadClass()
方法來加載類,而後調用類的newInstance()方法實例化對象
一、第一種和第二種方式使用的類加載器是相同的,都是當前類加載器。(this.getClass.getClassLoader)。而3由用戶指定類加載器。
二、若是須要在當前類路徑之外尋找類,則只能採用第3種方式。第3種方式加載的類與當前類分屬不一樣的命名空間。
三、第一種是靜態加載,而第2、三種是動態加載。
一、靜態加載的時候若是在運行環境中找不到要初始化的類,拋出的是NoClassDefFoundError
,它在JAVA的異常體系中是一個Error
二、動態態加載的時候若是在運行環境中找不到要初始化的類,拋出的是ClassNotFoundException
,它在JAVA的異常體系中是一個checked
異常
Class.forName()
是一種獲取Class
對象的方法,並且是靜態方法。
Class.forName()
是一個靜態方法,一樣能夠用來加載類,Class.forName()
返回與給定的字符串名稱相關聯類或接口的Class
對象。注意這是一種獲取Class
對象的方法
官方給出的API文檔以下
publicstatic Class<?> forName(String className) Returns the Class object associated withthe class or interface with the given string name. Invokingthis method is equivalent to: Class.forName(className,true, currentLoader) where currentLoader denotes the definingclass loader of the current class. For example, thefollowing code fragment returns the runtime Class descriptor for theclass named java.lang.Thread: Class t =Class.forName("java.lang.Thread") A call to forName("X") causes theclass named X to beinitialized. Parameters: className - the fully qualifiedname of the desired class. Returns: the Class object for the classwith the specified name.
能夠看出,Class.forName(className)
其實是調用Class.forName(className,true, this.getClass().getClassLoader())
。第二個參數,是指Class
被loading
後是否是必須被初始化。能夠看出,使用Class.forName(className)
加載類時則已初始化。因此Class.forName()
方法能夠簡單的理解爲:得到字符串參數中指定的類,並初始化該類。
首先,咱們必須先明確類加載機制的三個過程主要是:加載 --> 鏈接 --> 初始化。
Class.forName()
:將類的.class文件加載到jvm中以外,還會對類進行解釋,執行類中的static塊;
ClassLoader.loadClass()
:只幹一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance纔會去執行static塊。
Class.forName(name, initialize, loader)
:帶參函數也可控制是否加載static塊。而且只有調用了newInstance()方法採用調用構造函數,建立類的對象 。
這個時候,咱們再來看一個程序:
package com.jvm.classloader; class Demo{ static { System.out.println("static 靜態代碼塊"); } } public class ClassLoaderDemo { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader=ClassLoaderDemo.class.getClassLoader(); //一、使用ClassLoader.loadClass()來加載類,不會執行初始化塊 classLoader.loadClass("com.jvm.classloader.Demo"); //二、使用Class.forName()來加載類,默認會執行初始化塊 Class.forName("com.jvm.classloader.Demo"); //三、使用Class.forName()來加載類,並指定ClassLoader,初始化時不執行靜態塊 Class.forName("com.jvm.classloader.Demo",false,classLoader); } }
記得一個一個測試!我上面的程序是一次寫了三個的而且已經標明瞭標號一、二、3!!!各位再自個電腦上跑一遍,思路就很會清晰了!
類的加載、鏈接與初始化:
一、加載:查找並加載類的二進制數據到java虛擬機中
二、 鏈接:
驗證: 確保被加載的類的正確性
準備:爲類的靜態變量分配內存,並將其初始化爲默認值,可是到達初始化以前類變量都沒有初始化爲真正的初始值(若是是被final
修飾的類變量,則直接會被初始成用戶想要的值。)
解析:把類中的符號引用轉換爲直接引用,就是在類型的常量池中尋找類、接口、字段和方法的符號引用,把這些符號引用替換成直接引用的過程三、 初始化:爲類的靜態變量賦予正確的初始值
類從磁盤上加載到內存中要經歷五個階段:加載、鏈接、初始化、使用、卸載
Java程序對類的使用方式可分爲兩種
(1)主動使用
(2)被動使用
全部的Java虛擬機實現必須在每一個類或接口被Java程序「首次主動使用」時才能初始化他們
主動使用
(1)建立類的實例
(2)訪問某個類或接口的靜態變量 getstatic(助記符),或者對該靜態變量賦值 putstatic
(3)調用類的靜態方法 invokestatic
(4)反射(Class.forName(「com.test.Test」))
(5)初始化一個類的子類
(6)Java虛擬機啓動時被標明啓動類的類以及包含Main方法的類
(7)JDK1.7開始提供的動態語言支持(瞭解)
被動使用
除了上面七種狀況外,其餘使用java類的方式都被看作是對類的被動使用,都不會致使類的初始化
初始化入口方法。當進入類加載的初始化階段後,JVM 會尋找整個 main 方法入口,從而初始化 main 方法所在的整個類。當須要對一個類進行初始化時,會首先初始化類構造器(),以後初始化對象構造器()。
初始化類構造器:JVM 會按順序收集類變量的賦值語句、靜態代碼塊,最終組成類構造器由 JVM 執行。
初始化對象構造器:JVM 會按照收集成員變量的賦值語句、普通代碼塊,最後收集構造方法,將它們組成對象構造器,最終由 JVM 執行。值得特別注意的是,若是沒有監測或者收集到構造函數的代碼,則將不會執行對象初始化方法。對象初始化方法通常在實例化類對象的時候執行。
若是在初始化 main 方法所在類的時候遇到了其餘類的初始化,那麼就先加載對應的類,加載完成以後返回。如此反覆循環,最終返回 main 方法所在類。
若是本文對你有一點點幫助,那麼請點個讚唄,謝謝~
最後,如有不足或者不正之處,歡迎指正批評,感激涕零!若是有疑問歡迎留言,絕對第一時間回覆!
歡迎各位關注個人公衆號,一塊兒探討技術,嚮往技術,追求技術,說好了來了就是盆友喔...