摘要:本文主要講了Java類加載的機制,這是學習反射的入門基礎。
JVM和類
當我們調用Java命令運行某個Java程序時,該命令將會啟動1條Java虛擬機進程,不管該Java程序有多么復雜,該程序啟動了多少個線程,它們都處于該Java虛擬機進程里。正如前面介紹的,同1個JVM的所有線程、所有變量都處于同1個進程里,它們都使用該JVM進程的內存區。當系統出現以下幾種情況時,JVM進程將被終止:
1、程序運行到最后正常結束。
2、程序運行到使用System.exit()或Runtime.getRuntime().exit()代碼結束程序。
3、程序履行進程中遇到未捕獲的異常或毛病而結束。
3、程序所在平臺強迫結束了JVM進程。
從上面的介紹可以看出,當Java程序運行結束時,JVM進程結束,該進程在內存中狀態將會丟失。
類的生命周期
類的加載/類初始化
當程序主動使用某個類時,如果該類還未被加載到內存中,系統會通過加載、連接、初始化3個步驟來對該類進行初始化,如果沒成心外,JVM將會連續完成這3個步驟,所以有時也把這3個步驟統稱為類加載或類初始化。
加載:查找并加載類的2進制數據
1、通過1個類的全限定名來獲得定義此類的2進制字節流。
2、將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
3、在java堆中生成1個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。
注意:將編譯后的java類文件(也就是.class文件)中的2進制數據讀入內存,并將其放在運行時數據區的方法區內,然后再堆區創建1個Java.lang.Class對象,用來封裝類在方法區的數據結構。即加載后終究得到的是Class對象,并且更加值得注意的是:該Java.lang.Class對象是單實例的,不管這個類創建了多少個對象,他的Class對象時唯1的!
連接:
1、驗證:確保被加載的類的正確性
2、準備:為類的靜態變量分配內存,并將其初始化為默許值
3、解析:把類中的符號援用轉換為直接援用。
初始化:為類的靜態變量賦予正確的初始值。
注意:連接和初始化階段,其實靜態變量經過了兩次賦值:第1次是靜態變量類型的默許值;第2次是我們真正賦給靜態變量的值。
我簡單畫了個圖,其進程以下:
類加載指的是將類的class文件讀入內存,并為之創建1個java.lang.Class對象,也就是說當程序中使用任何類時,系統都會為之建立1個java.lang.Class對象。事實上,每一個類是1批具有相同特點的對象的抽象(或說概念),而系統中所有的類,它們實際上也是對象,它們都是java.lang.Class的實例。
加載由類加載器完成,類加載器通常由JVM提供,這些類加載器也是我們前面所有程序運行的基礎,JVM提供的這些類加載器通常被稱為系統類加載器。除此以外,開發者可以通過繼承ClassLoader基類來創建自己的類加載器。
通過使用不同的類加載器,可以從不同來源加載類的2進制數據,通常有以下幾種來源:
1、從本地文件系統來加載class文件,這是絕大部份示例程序的類加載方式。
2、從JAR包中加載class文件,這類方式也是很常見的,前面介紹JDBC編程時用到的數據庫驅動類就是放在JAR文件中,JVM可以從JAR文件中直接加載該class文件。
3、通過網絡加載class文件。
4、把1個Java源文件動態編譯、并履行加載。
類加載器通常不必等到“首次使用”該類時才加載該類,Java虛擬機規范允許系統預先加載某些類。
Java程序對類的使用方式
主動使用
1、創建類的實例
2、方法某個類或接口的靜態變量,或對該靜態變量賦值
3、調用類的靜態方法
4、反射(如 Class.forName(“com.itzhai.Test”))
5、初始化1個類的子類
6、Java虛擬機啟動時被標明為啟動類的類(Main Class)
被動使用
除以上6中方式,其他對類的使用都是被動使用,都不會致使類的初始化。類的初始化時機正是java程序對類的首次主動使用,
所有的Java虛擬機實現必須在每一個類或接口被Java程序“首次主動使用”時才初始化它們。
對象初始化
在類被裝載、連接和初始化,這個類就隨時都可能使用了。對象實例化和初始化是就是對象生命的起始階段的活動,在這里我們主要討論對象的初始化工作的相干特點。
Java 編譯器在編譯每一個類時都會為該類最少生成1個實例初始化方法--即"()" 方法。此方法與源代碼中的每一個構造方法相對應,如果類沒有明確地聲明任何構造方法,編譯器則為該類生成1個默許的無參構造方法,這個默許的構造器僅僅調用父類的無參構造器,與此同時也會生成1個與默許構造方法對應的 "()" 方法.
通常來講,() 方法內包括的代碼內容大概為:調用另外一個() 方法;對實例變量初始化;與其對應的構造方法內的代碼。
如果構造方法是明確地從調用同1個類中的另外一個構造方法開始,那它對應的() 方法體內包括的內容為:1個對本類的() 方法的調用;對利用構造方法內的所有字節碼。
如果構造方法不是通過調用本身類的其它構造方法開始,并且該對象不是 Object 對象,那() 法內則包括的內容為:1個對父類() 方法的調用;對實例變量初始化方法的字節碼;最后是對應構造子的方法體字節碼。
如果這個類是 Object,那末它的() 方法則不包括對父類() 方法的調用。
1、相同點:
通過這幾種方式,得到的都是Java.lang.Class對象(這個是上面講到的類在加載時取得的終究產物)
例如:
結果:
2、區分:
下面用1個實例來講說它們的區分
以下新建1個類
然后開始使用:
如果,將Class.forName()去掉:
以下:
結果又變成:
所以,可以得出以下結論:
1)Class cl=Cat.class; JVM將使用類Cat的類裝載器,將類A裝入內存(條件是:類A還沒有裝入內存),不對類A做類的初始化工作.返回類A的Class的對象
2)Class cl=對象援用o.getClass();返回援用o運行時真正所指的對象(由于:兒子對象的援用可能會賦給父對象的援用變量中)所屬的類的Class的對象 ,如果還沒裝載過,會進行裝載。
3)Class.forName("類名"); 裝入類A,并做類的初始化(條件是:類A還沒有裝入內存)
從JVM的角度看,我們使用關鍵字new創建1個類的時候,這個類可以沒有被加載。但是使用Class對象的newInstance()方法的時候,就必須保證:
1、這個類已加載;
2、這個類已連接了。而完成上面兩個步驟的正是Class的靜態方法forName()所完成的,這個靜態方法調用了啟動類加載器,即加載 java API的那個加載器。
現在可以看出,Class對象的newInstance()(這類用法和Java中的工廠模式有著異曲同工之妙)實際上是把new這個方式分解為兩步,即首先調用Class加載方法加載某個類,然后實例化。這樣分步的好處是不言而喻的。我們可以在調用class的靜態加載方法forName時取得更好的靈活性,提供給了1種降耦的手段。
Class.forName().newInstance()和通過new得到對象的區分
1、使用newInstance可以解耦。使用newInstance的條件是,類已加載并且這個類已連接,這是正是class的靜態方法forName()完成的工作。newInstance實際上是把new 這個方式分解為兩步,即,首先調用class的加載方法加載某個類,然后實例化。
2、newInstance: 弱類型。低效力。只能調用無參構造。 new: 強類型。相對高效。能調用任何public構造。
3、newInstance()是實現IOC、反射、面對接口編程和依賴顛倒等技術方法的必定選擇,new只能實現具體類的實例化,不合適于接口編程。
4、 newInstance() 1般用于動態加載類。
5、Class.forName(“”).newInstance()返回的是object 。
6、newInstance( )是1個方法,而new是1個關鍵字;
注:1般在通用框架里面用的就是class.forName來加載類,然后再通過反射來調用其中的方法,比方Tomcat源碼里面,這樣就避免了new關鍵字的耦合度,還有讓不同的類加載器來加載不同的類,方便提高類之間的安全性和隔離性.