每個類編譯后產生一個Class對象,存儲在.class文件中,JVM使用類加載器(Class Loader)來加載類的字節碼文件(.class),類加載器實質上是一條類加載器鏈,一般的,我們只會用到一個原生的類加載器AppClassLoader,它只加載Java API等可信類,通常只是在本地磁盤中加載,這些類一般就夠我們使用了。如果我們需要從遠程網絡或數據庫中下載.class字節碼文件,那就需要我們來掛載額外的類加載器。
一般來說,類加載器是按照樹形的層次結構組織的,每個加載器都有一個父類加載器。另外,每個類加載器都支持代理模式,即可以自己完成Java類的加載工作,也可以代理給其它類加載器。
ClassLoader中的幾個實現類
1、Bootstrap ClassLoader 這個是JVM加載自身工作需要的類,完全由JVM自己來控制,外部無法訪問到這個;
2、ExtClassLoader比較特殊的,服務的特定目標在System.getProperty("java.ext.dirs");
3、AppClassLoader,父類是ExtClassLoader,java中參數-classpath中的類都可以被這個類加載器加載;
4、URLClassLoader,一般這個類幫我們實現了大部分的工作,自定義可以繼承這個類,這樣僅僅在需要的地方做修改就行了;
類加載器的加載順序有兩種,一種是父類優先策略,一種是是自己優先策略,父類優先策略是比較一般的情況(如JDK采用的就是這種方式),在這種策略下,類在加載某個Java類之前,會嘗試代理給其父類加載器,只有當父類加載器找不到時,才嘗試子類加載器去加載,如果找到了,自己就不用加載。自己優先的策略與父類優先相反,它會首先嘗試自己加載,如果找到了就不用父類加載器去加載,只有找不到的時候才要父類加載器去加載,這種在web容器(如tomcat)中比較常見。
不管使用什么樣的類加載器,類都是在第一次被用到時,動態加載到JVM的。這句話有兩層含義:
- Java程序在運行時並不一定被完整加載,只有當發現該類還沒有加載時,才去本地或遠程查找類的.class文件並驗證和加載(賴加載);
- 當程序創建了第一個對類的靜態成員的引用(如類的靜態變量、靜態方法、構造方法——構造方法也是靜態的)時,才會加載該類。Java的這個特性叫做:動態加載。
JVM加載clas文件到內存的方式
1、顯示加載:不通過代碼里的ClassLoader調用,而是JVM來自動加載類到內存中的方式;
1.1、通過Class中的forName;
1.2、通過ClassLoader中的loadClass
1.3、通過ClasLoader中的findSystemClass
2、隱身加載:通過代碼中ClassLoader來加載的方式;
如何加載class文件
1)加載(Loading),由類加載器執行,查找字節碼,並創建一個Class對象(只是創建);
a)通過類的全名產生對應類的二進制數據流。(注意,如果沒找到對應類文件,只有在類實際使用時才拋出錯誤。)
b)分析並將這些二進制數據流轉換為方法區(JVM 的架構:方法區、堆,棧,本地方法棧,pc 寄存器)特定的數據結構(這些數據結構是實現有關的,不同 JVM 有不同實現)。這里處理了部分檢驗,比如類文件的魔數的驗證,檢查文件是否過長或者過短,確定是否有父類(除了 Obecjt 類)。
c)創建對應類的 java.lang.Class 實例(注意,有了對應的 Class 實例,並不意味着這個類已經完成了加載鏈鏈接!)。
2)鏈接(Linking),驗證字節碼,為靜態域分配存儲空間(只是分配,並不初始化該存儲空間),解析該類創建所需要的對其它類的應用;
a)驗證(verification)
鏈接的第三部解析會把類中成員方法、成員變量、類和接口的符號引用替換為直接引用,而在這之前,需要檢測被引用的類型正確性和接入屬性是否正確(就是 public ,private 的的問題),諸如檢查 final class 又沒有被繼承,檢查靜態變量的正確性等等。(注意到實際上有一部分驗證過程已經在加載的過程中執行了。)
b)准備(preparation)
對類的成員變量分配空間。雖然有初始值,但這個時候不會對他們進行初始化(因為這里不會執行任何 Java 代碼)。具體如下:所有原始類型的值都為 0。如 float: 0f, int: 0, boolean: 0(注意 boolean 底層實現大多使用 int),引用類型則為 null。值得注意的是,JVM 可能會在這個時期給一些有助於程序運行效率提高的數據結構分配空間。比如方發表(類似與 C++中的虛函數表,參見另一篇博文《Java:方法的虛分派和方法表》)。
c)解析(Resolution)
首先,為類、接口、方法、成員變量的符號引用定位直接引用(如果符號引用先到常量池中尋找符號,再找先應的類型,無疑會耗費更多時間),完成內存結構的布局。
然后,這一步是可選的。可以在符號引用第一次被使用時完成,即所謂的延遲解析(late resolution)。但對用戶而言,這一步永遠是延遲解析的,即使運行時會執行 early resolution,但程序不會顯示的在第一次判斷出錯誤時拋出錯誤,而會在對應的類第一次主動使用的時候拋出錯誤!
最后,這一步與之后的類初始化是不沖突的,並非一定要所有的解析結束以后才執行類的初始化。不同的 JVM 實現不同。詳情見另一篇博文《Java 類加載的延遲初始化》。
3)初始化(Initialization)。
動態加載類:
- public class BeanUtilsTest
- {
- public static void main(String[] args)
- throws Exception
- {
- Class clz = Class.forName("com.ai.redis.A");
- }
- }
- class A
- {
- public static int VALUE;
- static
- {
- System.out.println("run parent static code.");
- }
- }
輸出結果:打印run parent static code.
類.class:
- public class BeanUtilsTest
- {
- public static void main(String[] args)
- throws Exception
- {
- Class clz1 = A.class;
- }
- }
- class A
- {
- public static int VALUE;
- static
- {
- System.out.println("run parent static code.");
- }
- }
輸出結果:啥也沒有。
通過以上比較,下面這段代碼應該知道打印什么了吧。
- public class BeanUtilsTest
- {
- public static void main(String[] args)
- throws Exception
- {
- System.out.println(A.VALUE);
- }
- }
- class A
- {
- public static final int VALUE = 10;
- static
- {
- System.out.println("run parent static code.");
- }
- }
輸出結果:10
有人要問了,為什么不打印run parent static code.因為VALUE變量是在編譯時就已經確定的一個常量值跟類.class文件是一個道理,所以不打印。
注:編譯時常量必須滿足3個條件:static的,final的,常量。
- <pre class="html" name="code"> static int a;
- final int b;
- static final int c = Math.abs(10);
- static final int d;
- static
- {
- d = 5;
- }
PS:
為什么接口不能定義成員變量,而只能定義 final static 變量。
- 1.接口是不可實例化,它的所有元素都不必是實例(對象)層面的。static 滿足了這一點。
- 2.如果接口的變量能被修改,那么一旦一個子類實現了這個接口,並修改了接口中的非 final 變量,而該子類的子類再次修改這個非 final 的變量后,造成的結果就是雖然實現了相同的接口,但接口中的變量值是不一樣的。
綜上述,static final 更適合於接口。
參考:
1、《通過類字面常量解釋接口常量為什么只能定義為 static final,類加載過程—Thinking in java》
2、http://blog.csdn.net/biaobiaoqi/article/details/6909141