類的加載classload和類對象的生成


在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中去。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM