Java 類加載機制(阿里面試題)-什麼時候初始化類 Java探針-Java Agent技術-阿里面試題 類加載機制 Java虛擬機類加載機制

   什麼是類加載器
   類加載器與類的」相同「判斷
   類加載器種類
  雙親委派模型
  類加載過程
  自定義類加載器
  JAVA熱部署實現面試

什麼是類加載器

負責讀取 Java 字節代碼,並轉換成java.lang.Class類的一個實例;數據庫

類加載器與類的」相同「判斷

類加載器除了用於加載類外,還可用於肯定類在Java虛擬機中的惟一性。編程

即使是一樣的字節代碼,被不一樣的類加載器加載以後所獲得的類,也是不一樣的。數組

通俗一點來說,要判斷兩個類是否「相同」,前提是這兩個類必須被同一個類加載器加載,不然這個兩個類不「相同」。
這裏指的「相同」,包括類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof關鍵字等判斷出來的結果。安全

 

類加載器種類

啓動類加載器,Bootstrap ClassLoader,加載JACA_HOME\lib,或者被-Xbootclasspath參數限定的類
擴展類加載器,Extension ClassLoader,加載\lib\ext,或者被java.ext.dirs系統變量指定的類
應用程序類加載器,Application ClassLoader,加載ClassPath中的類庫
自定義類加載器,經過繼承ClassLoader實現,通常是加載咱們的自定義類網絡

 

雙親委派模型

類加載器 Java 類如同其它的 Java 類同樣,也是要由類加載器來加載的;除了啓動類加載器,每一個類都有其父類加載器(父子關係由組合(不是繼承)來實現);

所謂雙親委派是指每次收到類加載請求時,先將請求委派給父類加載器完成(全部加載請求最終會委派到頂層的Bootstrap ClassLoader加載器中),若是父類加載器沒法完成這個加載(該加載器的搜索範圍中沒有找到對應的類),子類嘗試本身加載。


雙親委派好處

  • 避免同一個類被屢次加載;
  • 每一個加載器只能加載本身範圍內的類;
類加載過程

類加載分爲三個步驟:加載鏈接初始化

以下圖 , 是一個類從加載到使用及卸載的所有生命週期,圖片來自參考資料;

加載

根據一個類的全限定名(如cn.edu.hdu.test.HelloWorld.class)來讀取此類的二進制字節流到JVM內部;

將字節流所表明的靜態存儲結構轉換爲方法區的運行時數據結構(hotspot選擇將Class對象存儲在方法區中,Java虛擬機規範並無明確要求必定要存儲在方法區或堆區中)

轉換爲一個與目標類型對應的java.lang.Class對象;

鏈接

驗證

驗證階段主要包括四個檢驗過程:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證;

準備

爲類中的全部靜態變量分配內存空間,併爲其設置一個初始值(因爲尚未產生對象,實例變量將再也不此操做範圍內);

解析

將常量池中全部的符號引用轉爲直接引用(獲得類或者字段、方法在內存中的指針或者偏移量,以便直接調用該方法)。這個階段能夠在初始化以後再執行。

初始化

  在鏈接的準備階段,類變量已賦過一次系統要求的初始值,而在初始化階段,則是根據程序員本身寫的邏輯去初始化類變量和其餘資源,舉個例子以下:

    public static int value1  = 5;
    public static int value2  = 6;
    static{
        value2 = 66;
    }

在準備階段value1和value2都等於0;

在初始化階段value1和value2分別等於5和66;

  • 全部類變量初始化語句和靜態代碼塊都會在編譯時被前端編譯器放在收集器裏頭,存放到一個特殊的方法中,這個方法就是<clinit>方法,即類/接口初始化方法,該方法只能在類加載的過程當中由JVM調用;
  • 編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量;
  • 若是超類尚未被初始化,那麼優先對超類初始化,但在<clinit>方法內部不會顯示調用超類的<clinit>方法,由JVM負責保證一個類的<clinit>方法執行以前,它的超類<clinit>方法已經被執行。
  • JVM必須確保一個類在初始化的過程當中,若是是多線程須要同時初始化它,僅僅只能容許其中一個線程對其執行初始化操做,其他線程必須等待,只有在活動線程執行完對類的初始化操做以後,纔會通知正在等待的其餘線程。(因此能夠利用靜態內部類實現線程安全的單例模式)
  • 若是一個類沒有聲明任何的類變量,也沒有靜態代碼塊,那麼能夠沒有類<clinit>方法;

什麼時候觸發初始化

  1. 爲一個類型建立一個新的對象實例時(好比new、反射、序列化)
  2. 調用一個類型的靜態方法時(即在字節碼中執行invokestatic指令)
  3. 調用一個類型或接口的靜態字段,或者對這些靜態字段執行賦值操做時(即在字節碼中,執行getstatic或者putstatic指令),不過用final修飾的靜態字段除外,它被初始化爲一個編譯時常量表達式
  4. 調用JavaAPI中的反射方法時(好比調用java.lang.Class中的方法,或者java.lang.reflect包中其餘類的方法)
  5. 初始化一個類的派生類時(Java虛擬機規範明確要求初始化一個類時,它的超類必須提早完成初始化操做,接口例外)
  6. JVM啓動包含main方法的啓動類時。
 

 自定義類加載器

 要建立用戶本身的類加載器,只須要繼承java.lang.ClassLoader類,而後覆蓋它的findClass(String name)方法便可,即指明如何獲取類的字節碼流。

若是要符合雙親委派規範,則重寫findClass方法(用戶自定義類加載邏輯);要破壞的話,重寫loadClass方法(雙親委派的具體邏輯實現)

例子:

package classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

class TestClassLoad {
    @Override
    public String toString() {
        return "類加載成功。";
    }
}
public class PathClassLoader extends ClassLoader {
    private String classPath;

    public PathClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getData(String className) {
        String path = classPath + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num = 0;
            while ((num = is.read(buffer)) != -1) {
                stream.write(buffer, 0, num);
            }
            return stream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }



    public static void main(String args[]) throws ClassNotFoundException,
            InstantiationException, IllegalAccessException {
        ClassLoader pcl = new PathClassLoader("D:\\ProgramFiles\\eclipseNew\\workspace\\cp-lib\\bin");
        Class c = pcl.loadClass("classloader.TestClassLoad");//注意要包括包名
        System.out.println(c.newInstance());//打印類加載成功.
    }
}

 

 

JAVA熱部署實現

首先談一下何爲熱部署(hotswap),熱部署是在不重啓 Java 虛擬機的前提下,能自動偵測到 class 文件的變化,更新運行時 class 的行爲。Java 類是經過 Java 虛擬機加載的,某個類的 class 文件在被 classloader 加載後,會生成對應的 Class 對象,以後就能夠建立該類的實例。默認的虛擬機行爲只會在啓動時加載類,若是後期有一個類須要更新的話,單純替換編譯的 class 文件,Java 虛擬機是不會更新正在運行的 class。若是要實現熱部署,最根本的方式是修改虛擬機的源代碼,改變 classloader 的加載行爲,使虛擬機能監聽 class 文件的更新,從新加載 class 文件,這樣的行爲破壞性很大,爲後續的 JVM 升級埋下了一個大坑。

另外一種友好的方法是建立本身的 classloader 來加載須要監聽的 class,這樣就能控制類加載的時機,從而實現熱部署。 

 熱部署步驟:

一、銷燬自定義classloader(被該加載器加載的class也會自動卸載);

二、更新class

三、使用新的ClassLoader去加載class 

JVM中的Class只有知足如下三個條件,才能被GC回收,也就是該Class被卸載(unload):

   - 該類全部的實例都已經被GC,也就是JVM中不存在該Class的任何實例。
   - 加載該類的ClassLoader已經被GC。
   - 該類的java.lang.Class 對象沒有在任何地方被引用,如不能在任何地方經過反射訪問該類的方法

 

延伸出來問題進行分析:

看到這個題目,不少人會以爲我寫個人java代碼,至於類,JVM愛怎麼加載就怎麼加載,博主有很長一段時間也是這麼認爲的。隨着編程經驗的日積月累,愈來愈感受到了解虛擬機相關要領的重要性。閒話很少說,老規矩,先來一段代碼吊吊胃口。

public class SSClass
{
    static
    {
        System.out.println("SSClass");
    }
}   
public class SuperClass extends SSClass
{
    static
    {
        System.out.println("SuperClass init!");
    }
 
    public static int value = 123;
 
    public SuperClass()
    {
        System.out.println("init SuperClass");
    }
}
public class SubClass extends SuperClass
{
    static
    {
        System.out.println("SubClass init");
    }
 
    static int a;
 
    public SubClass()
    {
        System.out.println("init SubClass");
    }
}
public class NotInitialization
{
    public static void main(String[] args)
    {
        System.out.println(SubClass.value);
    }
}

 

運行結果:
SSClass
SuperClass init!
123

 

答案答對了嚒?

也許有人會疑問:爲何沒有輸出SubClass init。ok~解釋一下:對於靜態字段,只有直接定義這個字段的類纔會被初始化,所以經過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。

上面就牽涉到了虛擬機類加載機制。若是有興趣,能夠繼續看下去。

 

類加載過程

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準備、驗證、解析3個部分統稱爲鏈接(Linking)。如圖所示。

加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。如下陳述的內容都已HotSpot爲基準。

加載

在加載階段(能夠參考java.lang.ClassLoader的loadClass()方法),虛擬機須要完成如下3件事情:

  1. 經過一個類的全限定名來獲取定義此類的二進制字節流(並無指明要從一個Class文件中獲取,能夠從其餘渠道,譬如:網絡、動態生成、數據庫等);
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;
  3. 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口;

加載階段和鏈接階段(Linking)的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,但這些夾在加載階段之中進行的動做,仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序。

驗證

驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
驗證階段大體會完成4個階段的檢驗動做:

  1. 文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。
  2. 元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object以外。
  3. 字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。
  4. 符號引用驗證:確保解析動做能正確執行。

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

準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值,假設一個類變量的定義爲:

public static int value=123;

那變量value在準備階段事後的初始值爲0而不是123.由於這時候還沒有開始執行任何java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器()方法之中,因此把value賦值爲123的動做將在初始化階段纔會執行。

至於「特殊狀況」是指:public static final int value=123,即當類字段的字段屬性是ConstantValue時,會在準備階段初始化爲指定的值,因此標註爲final以後,value的值在準備階段初始化爲123而非0.

解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

初始化

類初始化階段是類加載過程的最後一步,到了初始化階段,才真正開始執行類中定義的java程序代碼。在準備極端,變量已經付過一次系統要求的初始值,而在初始化階段,則根據程序猿經過程序制定的主管計劃去初始化類變量和其餘資源,或者說:初始化階段是執行類構造器<clinit>()方法的過程.

<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。以下:

public class Test
{
    static
    {
        i=0;
        System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前應用)
    }
    static int i=1;
}

那麼去掉報錯的那句,改爲下面:

public class Test
{
    static
    {
        i=0;
//      System.out.println(i);
    }
    static int i=1;
 
    public static void main(String args[])
    {
        System.out.println(i);
    }
}

輸出結果是什麼呢?固然是1啦~在準備階段咱們知道i=0,而後類初始化階段按照順序執行,首先執行static塊中的i=0,接着執行static賦值操做i=1,最後在main方法中獲取i的值爲1。

()方法與實例構造器<init>()方法不一樣,它不須要顯示地調用父類構造器,虛擬機會保證在子類<init>()方法執行以前,父類的<clinit>()方法方法已經執行完畢,回到本文開篇的舉例代碼中,結果會打印輸出:SSClass就是這個道理。

因爲父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做。

<clinit>()方法對於類或者接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生產<clinit>()方法。

接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,所以接口與類同樣都會生成<clinit>()方法。但接口與類不一樣的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。

虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有耗時很長的操做,就可能形成多個線程阻塞,在實際應用中這種阻塞每每是隱藏的。

package jvm.classload;
 
public class DealLoopTest
{
    static class DeadLoopClass
    {
        static
        {
            if(true)
            {
                System.out.println(Thread.currentThread()+"init DeadLoopClass");
                while(true)
                {
                }
            }
        }
    }
 
    public static void main(String[] args)
    {
        Runnable script = new Runnable(){
            public void run()
            {
                System.out.println(Thread.currentThread()+" start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread()+" run over");
            }
        };
 
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}

 

 

運行結果:(即一條線程在死循環以模擬長時間操做,另外一條線程在阻塞等待)

Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main]init DeadLoopClass

 

須要注意的是,其餘線程雖然會被阻塞,但若是執行()方法的那條線程退出()方法後,其餘線程喚醒以後不會再次進入()方法。同一個類加載器下,一個類型只會初始化一次。
將上面代碼中的靜態塊替換以下:

static
{
    System.out.println(Thread.currentThread() + "init DeadLoopClass");
    try
    {
        TimeUnit.SECONDS.sleep(10);
    }
    catch (InterruptedException e)
    {
        e.printStackTrace();
    }
}

運行結果:

Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-1,5,main]init DeadLoopClass (以後sleep 10s)
Thread[Thread-1,5,main] run over
Thread[Thread-0,5,main] run over

 

虛擬機規範嚴格規定了有且只有5中狀況(jdk1.7)必須對類進行「初始化」(而加載、驗證、準備天然須要在此以前開始):

  1. 遇到new,getstatic,putstatic,invokestatic這失調字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。生成這4條指令的最多見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。
  3. 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。
  4. 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  5. 當使用jdk1.7動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行初始化,則須要先出觸發其初始化。

開篇已經舉了一個範例:經過子類引用付了的靜態字段,不會致使子類初始化。
這裏再舉兩個例子。

  1. 經過數組定義來引用類,不會觸發此類的初始化:(SuperClass類已在本文開篇定義)
  2. public class NotInitialization
    {
        public static void main(String[] args)
        {
            SuperClass[] sca = new SuperClass[10];
        }
    }

     

 

運行結果:(無)

  1. 常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化:
    public class ConstClass
    {
        static
        {
            System.out.println("ConstClass init!");
        }
        public static  final String HELLOWORLD = "hello world";
    }
    public class NotInitialization
    {
        public static void main(String[] args)
        {
            System.out.println(ConstClass.HELLOWORLD);
        }
    }

     

 

運行結果:

hello world

 

 

 

參考:類加載機制

參考:Java虛擬機類加載機制

相關文章
相關標籤/搜索