在Java中最重要的可以說就是類的加載了。不論我們編寫的功能多么復雜,或是多么簡單,永遠逃離不開的,就是將這個類從class文件加載到JVM中來。
類的加載過程
首先我們要了解一下類的加載過程,包括:加載、連接(驗證、准備、解析)、初始化、使用、卸載。
加載:將根據類的全限定名找到對應的Class文件,將它加載進JVM中,並生成Class對象保存在堆中。
連接:
驗證:檢查加載進來的類信息是否滿足我們JVM的規范。
准備:對類中的靜態變量分配內存空間,並賦予原始值。對常量直接賦予指定的值。
解析:將類中的符號引用轉變為直接引用。
初始化:為類中的靜態變量賦值,執行靜態代碼塊。
下面我們用一個類來驗證一下:
public class Main7 { private final int z = 6; private final static int k = 1; private static int i = 5; private int j = 2; static { i = 10; } { i = 11; j=3; } public static void main(String[] args) { } }
如上,我們定義一個Main7類,並對類中的每一步都打上斷點:
然后點擊debug運行:
第一步:程序最先進入到第9行代碼,此時查看我們最下面的靜態成員中,k由於是final static,被直接賦予了我們給它指定的值1。而i由於只是一個static,它被先賦予了默認值0。至於我們其他的兩個變量z和j,此時是沒有被初始化的。
注意:第八行代碼在我們的程序運行中並沒有被debug進入斷點,但是實際上它是最先和i一起被初始化的。即說明,加載和連接兩個步驟,是無法被我們的debug進入的。我們這里能進入斷點的,也僅僅只是初始化步驟。(第九行之所以能進入是因為我們對i賦予了i=5,如果我們只定義static int i,則這行也不會被進入debug)
第二步:執行靜態代碼塊。
第三步:結束。
由於我們的main方法中並沒有內容,因此我們不會創建任何自定義類的對象。Main7中的static變量與static代碼塊之所以會被初始化,也是因為這是作為main方法所在的類,會被加載進JVM。
總結:由上面的結果可以總結如下幾點:
1.類只會在第一次被調用時候進行加載。這個調用包括Main方法所在的類、調用類中的靜態成員變量、執行類的靜態方法、通過反射創建對象、new一個對象、子類被初始化。
2.類的加載不會初始化非static變量,也不會執行非static代碼塊。
類加載器
JDK默認給我們提供了三個類加載器:
BootStrap ClassLoad:最頂級的類加載器,使用C++編寫,由JVM啟動,默認加載%JAVA_HOME%/lib下的jar包和類。
Extension ClassLoad:擴展加載器,由BootStrap ClassLoad啟動,父類加載器是BootStrap ClassLoad,默認加載%JAVA_HOME%/lib/ext下的jar包和類。
Application ClassLoad:應用加載器,由BootStrap ClassLoad啟動,父類加載器是Extension ClassLoad。默認加載classpath下的jar包和類。
關系圖如下:
我們查看一個自定義類的加載類:
public static void main(String[] args) { ClassLoader cl = Main7.class.getClassLoader(); while (cl != null) { System.out.println(cl.toString()); cl = cl.getParent(); } }
打印結果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@77459877
可以看到,上面只打印出來了兩個ClassLoader對象,一個是AppClassLoader,這是自定義類的加載器,另一個是ExtClassLoader,這是自定義類加載器的父類加載器。
而我們再次通過getParent()方法獲取ExtClassLoader對象的父類加載器時候,返回的結果等於null,因此跳出了循環。
我們再嘗試一個由BootStrap ClassLoader加載的類String,查看它的類加載類
public static void main(String[] args) { ClassLoader cl = String.class.getClassLoader(); System.out.println(cl == null ? "cl is null" : cl.toString()); }
打印結果:
cl is null
雖然BootStrap ClassLoader是ExtClassLoader的父類加載器,但是由於它是C++編寫,因此在Java代碼中,並沒有任何的體現,如果一個類的類加載器是null,那么它就是由BootStrap ClassLoader啟動。
下面我們來檢驗一下上面的說法,首先利用類加載器的雙親委派機制來確認。
雙親委派機制:類加載器加載一個類時,會先交由它的父類加載器加載,如果父類加載器的加載范圍中有全限定名相同的類文件,則由父類加載器加載這個類,子類加載器不再加載。
意即:自定義加載器加載一個類Aclass,首先交給父類加載器AppClassLoader,AppClassLoader再交由它的父類加載器ExClassLoader,ExtClassLoader再交由它的父類加載器BootStrapClassLoader,BootStrapClassLoader沒有父類加載器,因此檢查自己的加載范圍%JAVA_HOME%/lib下有沒有這個類,沒有則再由ExtClassLoader檢查它的加載范圍%JAVA_HOME%/lib/ext下有沒有這個類,沒有則再有AppClassLoader檢查classpath下有沒有這個類,沒有則再交由自定義類加載器去它的路徑下加載。其中一旦有一個類加載器找到這個類的class文件,就會由這個類加載器進行加載,它的子類加載器而不會在進行處理。
下面我們來查看一下類加載器(ClassLoader)的加載方法:
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
它這里接收一個全限定的二進制類名,然后調用loadClass(name,false)方法
// 這個protected可以看到,是允許我們通過自定義類加載器來重寫這個方法。 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { //這里加鎖,每一次只會有一個類被加載進來。而不會同時加載多個類。 // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); //首先檢查Class對象中是否已經包含了這個類。即這個類是否已經被加載過了。 if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //首先判斷父類加載器不等於null,也就是說父類加載器不是BootStrap ClassLoader c = parent.loadClass(name, false); //交由父類加載器加載。雙親委派機制 } else { c = findBootstrapClassOrNull(name); //否則的話,從BootStrapClassLoader中查找該類 } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { //父類加載器中沒有對應的class文件 // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); //調用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; } }
從上面的代碼里面可以看到,ClassLoader中明確指定了,當parent == null時,會調用findBootstrapClassOrNull(String)方法從BootStrapClassLoader中加載相關的類。
而且從ClassLoader的源碼中我們也可以看到,如果我們要自定義自己的類加載器,只要繼承ClassLoader,並重寫loadclass(String)或findClass(String)方法即可。
下面我們來查看一下ClassLoader自帶的findClass(String)方法。
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
可見,ClassLoader在這里采用了模版方法模式,將詳細的加載類文件的方法交由它的子類去實現。
我們定義一個自定義類加載器
public class MyClassLoader extends ClassLoader { // 加載器名稱 private String name; // 加載器的加載路徑 private String path; public MyClassLoader(String name, String path) { super(); // 采用默認的父類加載器,即為調用它的類的類加載器。一般是appClassLoader this.name = name; this.path = path; } public MyClassLoader(ClassLoader classLoader, String name, String path) { super(classLoader); //指定父類加載器 this.name = name; this.path = path; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = readClassFile(name); //根據名稱找到文件,並轉為字節數組 return super.defineClass(name, bytes, 0, bytes.length); //將字節數組轉為Class對象。 } private byte[] readClassFile(String name) { byte[] bytes = null; InputStream is = null; String filename = path + "/" + name.replaceAll("\\.", "/") + ".class"; File file = new File(filename); ByteArrayOutputStream os = new ByteArrayOutputStream(); try { is = new FileInputStream(file); int tmp = 0; while ((tmp = is.read()) != -1) { os.write(tmp); } bytes = os.toByteArray(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if (os != null) { try { os.close(); } catch (IOException e) { e.printStackTrace(); } } } return bytes; } }
我們定義一個用來被加載的測試類,將它編譯后的class文件放在D://tmp目錄下
public class Main5 { Main5() { System.out.println("Main5:" + this.getClass().getClassLoader().toString()); } }
客戶端調用代碼
public class Main11 { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { MyClassLoader mcl = new MyClassLoader("yxf", "d://tmp"); Class c = mcl.loadClass("Main5"); c.newInstance(); } }
輸出結果:
Main5:main11.MyClassLoader@5b2133b1
可見打印出來的類加載器正是我們自定義的MyClassLoader。
假如我們在classpath下同樣也存放一個Main5的class對象。
再次運行我們的客戶端調用代碼,輸出結果:
Main5:sun.misc.Launcher$AppClassLoader@18b4aac2
這一次由於雙親委派機制,我們Main5類被加載時使用的是AppClassLoader。
我們修改一下客戶端代碼:
public class Main11 { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { MyClassLoader mcl = new MyClassLoader(null, "yxf", "d://tmp"); //指定了父類加載器null,即BootStrapClassLoader Class c = mcl.loadClass("Main5"); c.newInstance(); } }
再次運行,輸出結果:
Main55:main11.MyClassLoader@5b2133b1
這一次是由於我們給自定義的類加載器指定了它的父類加載器BootStrapClassLoader,因此,即使我們在classpath下存放了一個Main5.class,也不會調用到AppClassLoader中去。