Java基礎:類加載機制

  以前的《java基礎:內存模型》當中,咱們大致瞭解了在java當中,不一樣類型的信息,都存放於java當中哪一個部位當中,那麼有了對於堆、棧、方法區、的基本理解之後,今天咱們來好好剖析一下,java當中的類加載機制(其實就是在美團的二面的時候,被面試官問的懵逼了,特意來總結一下,省得下次再那麼丟人 T-T)。html

  咱們都知道,在java語言當中,猴子們寫的程序,都會首先被編譯器編譯成爲.class文件(又稱字節碼文件),而這個.class文件(字節碼文件)中描述了類的各類信息,字節碼文件格式主要分爲兩部分:常量池和方法字節碼。那麼java的編譯器生成了這些.class文件以後,又是怎麼將它們加載到虛擬機當中的呢?接下來咱們就好好討論一下這個事情。java

  參考鏈接:http://www.cnblogs.com/xrq730/p/4844915.html  (感受這個博主寫的很適合greenHand看,因此就參考着本身總結了一份)程序員

類的生命週期:面試

  首先咱們來看看,在java當中一個類的完整的生命週期,主要包括瞭如下七個部分:1.加載、2.驗證、3.準備、4.解析、5.初始化、6.使用、7.卸載。在這7個階段當中,前5個階段加起來,就是類加載的全數組

過程,如圖所示。而驗證、準備、解析,三個階段又能夠被稱爲鏈接階段。除此以外,類加載過程中的五個階段,除了解析階段,其餘都是順序開始的,但不是順序執行的,也就是說在過程中是能夠並行的,好比在驗證開始後,還未結束,可能就會開始準備階段。而解析階段不必定在這個順序當中的緣由是由於,它在某些狀況下能夠初始化階段以後在開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定)。網絡

 

注意:這裏出現了一個新概念,叫綁定,簡單解釋了一下什麼叫綁定吧,在java當中的綁定定義爲:指的是把一個方法的調用與方法所在的類(方法主體)關聯起來,主要分爲了靜態綁定和動態綁定。多線程

靜態綁定:即在程序執行方法以前就已經被綁定,簡單來講再編譯期就進行綁定,在java當中被final、static、private修飾的方法,以及構造方法都是屬於靜態綁定,即編譯期綁定。eclipse

動態綁定:又稱運行時綁定,在運行時根據具體對象的類型進行綁定,在java當中,幾乎除了知足靜態綁定的方法以外,全部方法都是動態綁定的(java當中運行時多態的重要實現根據)ide

 

1.加載函數

  在java的類加載的過程中的加載,通常分爲兩種:第一種,預加載,指的是虛擬機啓動的時候,加載JDK路徑下的   lib/rt.jar   下的.class文件,在這個jar包當中包含着基礎的java.lang.*、java,util.*等基礎包,他們隨着虛擬機一塊兒被加載。第二種,運行時加載,指虛擬機在須要用到某一個類的時候,會先去內存當中查看有沒有這個類對應的.class文件,若是沒有會按照類的全限定名來加載這個類。而在咱們的文章當中,主要討論第二種運行時加載。

注意:這裏提到了一個全限定名,指的是包含着這個類所在的包的名稱,即好比   bjtu.wellhold.test.testclass 這樣的名稱,有包含絕對路徑的含義。

其實在加載階段,主要作了三件事情:

1.獲取.class文件的二進制字節流。

2.將類信息,靜態變量,方法字節碼,常量等這些.class文件中的內容放入到方法區當中(在《java基礎:內存模型》當中已經講解過)

3.在堆當中生成一個表明這個.class文件的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。(HotSpot虛擬機比較特殊,這個Class類放在了方法區當中)

 而程序員來講,最可控的地方就在於第一件事,因爲虛擬機並無規定二進制字節流要從哪而來,因此再這個部分,二進制字節流的來源能夠來自如下源:

1)從jar、war格式等來,

2)從網絡當中來,如Applet

3)運行時計算獲得,如動態代理技術。

4)由其餘文件生成,如JSP。

 

2.驗證

  因爲在加載階段的過程中,並無嚴格規定二進制字節流須要經過Java源碼編譯而來,因此驗證階段的主要目的是在加載階段獲取獲得的二進制字節流中包含的信息符合當前虛擬機的要求,而且不會致使虛擬機收到危害。主要分爲了如下幾種形式的驗證:

1.文件格式驗證:提一點,也許在安裝某個開源中間件的時候,須要JDK多少版本以上,這是由於在文件格式驗證的過程中,有一個部分就是對.class文件的版本號,高版本的JDK能夠向下兼容之前版本的.class文件,可是低版本的JDK則不能運行高版本的.class文件,即便文件格式爲發生任何變化,虛擬機也會拒絕執行。

2.元數據驗證。

3.字節碼驗證。

4.符號引用驗證。

注意:驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

 

3.準備

  這個階段當中,會正式的爲類當中那些被 static修飾的變量分配內存,而且設置其初始值,而這些變量都會存在方法區當中。

注意:

1)這個時候分配內存的,都是靜態變量,即被Static修飾的變量,而非實例變量。

2)這個階段賦初始值的變量指的是那些不被final修飾的static變量,好比"public static int value = 123;",value在準備階段事後是0而不是123,給value賦值爲123的動做將在初始化階段才進行;好比"public static final int value = 123;"就不同了,在準備階段,虛擬機就會給value賦值爲123。

 

4.解析

  解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。主要針對   類或接口的全限定名、字段的名稱和描述符、方法的名稱和描述符       的符號引用進行。

注意:這裏提到了一個符號引用和直接引用的概念,簡單解釋一下。

1.符號引用:符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時可以無歧義的定位到目標便可。符號引用與虛擬機的內存佈局無關,引用的目標並不必定加載到內存中。好比org.simple.People類引用了org.simple.Language類,在編譯時People類並不知道Language類的實際內存地址,所以只能使用符號org.simple.Language來表示Language類的地址。

2.直接引用:1)直接指向目標的指針,好比Class對象、static變量,static方法,都是指向方法區的指針。2)相對偏移量(從對象的映像開始算起到這個實例變量位置的偏移量。實例方法的直接引用多是方法表的偏移量)。3)一個能間接定位到目標的句柄

 

5.初始化

  初始化是類加載過程中的最後一步,在這個過程中才真正執行類中定義的java程序代碼(或者說字節碼),其實簡單來講,初始化階段作的事就是給static變量賦予用戶指定的值以及執行靜態代碼塊。它是一個執行類構造器<clinit>()方法的過程。

 這裏簡單說明下<clinit>()方法的執行規則:

 一、<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句中能夠賦值,可是不能訪問。
二、<clinit>()方法與實例構造器<init>()方法(類的構造函數)不一樣,它不須要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以,在虛擬機中第一個被執行的<clinit>()方法的類確定是java.lang.Object。
三、<clinit>()方法對於類或接口來講並非必須的,若是一個類中沒有靜態語句塊,也沒有對類變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。
四、接口中不能使用靜態語句塊,但仍然有類變量(final static)初始化的賦值操做,所以接口與類同樣會生成<clinit>()方法。可是接口魚類不一樣的是:執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法,只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。
五、虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖和同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有耗時很長的操做,那就可能形成多個線程阻塞,在實際應用中這種阻塞每每是很隱蔽的。
 

Java虛擬機規範嚴格規定了有且只有5種場景必須當即對類進行初始化,如下介紹4種場景,也稱爲對一個類進行主動引用(有一種因爲參考的帖子沒有寫出,本身也就並無得知)

一、使用new關鍵字實例化對象、讀取或者設置一個類的靜態字段(被final修飾的靜態字段除外)、調用一個類的靜態方法

二、使用java.lang.reflect包中的方法對類進行反射調用的時候

三、初始化一個類,發現其父類尚未初始化過的時候

四、虛擬機啓動的時候,虛擬機會先初始化用戶指定的包含main()方法的那個類

注意:經過數組定義引用類,不會觸發此類的初始化

class A
{
    public static int a=1;
    static
    {
        a=2;
        System.out.println("father class:"+a);
    }
}
class B extends A
{
    static{
        System.out.println("this is the B");
    }
    public static int b=a;
}


public class ClassLoad 
{
    static
    {
        System.out.println("this is the main");
    }
    public static void main(String[] args) 
    {
        System.out.println("B class :"+B.b);  
    }
}

結合這個例子,咱們來分析一下上述當中所說的主動引用和<clinit>()方法的規則,在這個例子當中,JVM首先會初始化ClassLoad這一個類,由於該類包含了main函數(主動引用第四種場景),以後在main函數當中,執行B.b這一行代碼的時候,因爲涉及到了對static變量的賦值,因此B類也會被初始化,可是並非當即初始化,而是查看B所繼承的父類,即A類是否被初始化(<clinit>規則2),這時候發現A類並無被初始化,則首先初始化A類,因此能夠看到上述初始化的順序爲:ClassLoad-》A-》B,運行結果以下:

this is the main
father class:2
this is the B
B class :2

 

自定義類加載器

  要本身實現類加載器以前,咱們首先看看在java jdk1.8當中的ClassLoad是怎麼實現loadClass方法的:

protected synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    //首先,先查找當前全限定名的類是否已經被加載。
    Class c = findLoadedClass(name);

    //若是沒有被加載
    if (c == null) 
    {
        try 
        {
            //當前加載器還有父類加載器(若是父加載器不是null,不是Bootstrap 
            //ClassLoader),則經過委託父類去加載
            if (parent != null) 
            {
                c = parent.loadClass(name, false);
            } 
            //一直遞歸找到最上級的父類加載器,再先經過父類去加載
            else {
                c = findBootstrapClass0(name);
            }
        } 
        //若是父類加載不到,則再經過本身來加載
        catch (ClassNotFoundException e) {
            c = findClass(name);
        }
    }
    //根據需求解析。
    if (resolve) {
        resolveClass(c);
    }
    return c;
 }    

整個JDK的loadclass的源碼和源碼解讀都在註釋中體現了。那麼咱們要本身實現一個類加載器,能夠有如下兩種方式:

一、若是不想打破雙親委派模型,那麼只須要重寫findClass方法便可

二、若是想打破雙親委派模型,那麼就重寫整個loadClass方法

固然,咱們自定義的ClassLoader不想打破雙親委派模型,因此自定義的ClassLoader繼承自java.lang.ClassLoader而且只重寫findClass方法。

首先咱們作一個實體類,叫Person:

public class Person
{  
    public String toString()
    {
        return "I am a person, my name is " + name;
    }
}

將這個實體類編譯出來的.class文件放到D盤根目錄下(eclipse當中的工程bin目錄下,能夠找到這個類的.class文件)

而後在手動編寫一個自定義類加載器MyClassLoad,它繼承了ClassLoad:

package wellhold.bjtu.classload;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class MyClassLoader extends ClassLoader 
{
    public MyClassLoader() {
        // TODO Auto-generated constructor stub
    }
    public MyClassLoader(ClassLoader parent) 
    {
        super(parent);
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File file = getClassFile(name);
        try 
        {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        } 
        catch (Exception e) 
        {
             e.printStackTrace();
        }
        return super.findClass(name);
    }
    
    private File getClassFile(String name)
    {
        File file=new File("d:/Person.class");
        return file;
    }
    
    private byte[] getClassBytes(File file) throws Exception
    {
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        
        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        
        fis.close();
        
        return baos.toByteArray();
    }
    
}

主要包括了從磁盤當中讀取.class文件,而後重寫了findClass方法,方法當中經過defineclass方法,將io讀取到的Byte流轉換成Class對象。以後再看看咱們的測試方法:

public class TestMyClassLoader {

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        // TODO Auto-generated method stub
         MyClassLoader mcl = new MyClassLoader();        
         Class<?> c1 = Class.forName("wellhold.bjtu.classload.Person", true, mcl);
         Object obj = c1.newInstance();
         System.out.println(obj);
         System.out.println(obj.getClass().getClassLoader());
    }

}

在測試方法當中,經過反射當中的forName方法,指定類的全限定名和類加載器,而且將初始化設定爲TRUE,加載到類後,打印:

this is the Person
wellhold.bjtu.classload.MyClassLoader@6d06d69c

說明咱們的類被成功的加載進來了。自定義類加載器完成了任務。

相關文章
相關標籤/搜索