天天看點

JVM類加載機制

概念:

  • JRE(Java運作環境):所有的Java 程式都要在JRE下才能運作。
  • JDK:開發者編譯、調試java程式用的開發工具包。JDK的工具也是Java程式,也需要JRE才能運作。在JDK的安裝目錄下有一個名為jre的目錄,用于存放JRE檔案。
  • JVM(Java虛拟機)是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上仿真模拟各種計算機功能來實作的。JVM有自己完善的硬體架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。使用JVM就是為了支援與作業系統無關,實作跨平台。JVM在執行位元組碼時,實際上最終還是把位元組碼解釋成具體平台上的機器指令執行。

JVM 類加載機制詳解:

JVM類加載機制分為五個部分:

  • 加載:
    通過一個`類的全限定名`來擷取定義此類的二進制位元組流;
    将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構;
    在記憶體中生成一個這個類的`java.lang.Class`對象,作為方法區這個類的各種資料的通路入口;           
  • 驗證 :

    為了確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。包括檔案格式驗證,中繼資料驗證,位元組碼驗證,符号引用驗證。可以采用

    -Xverifynone

    參數來關閉大部分的類驗證措施,以縮短虛拟機類加載的時間
  • 準備:

    為類變量配置設定記憶體并設定類變量初始值的階段,這些變量所使用的記憶體都将在方法區中進行配置設定。

public static int v = 8080; #變量v在準備階段過後的初始值為0而不是8080,将v指派為8080的putstatic指令是程式被編譯後,存放于類構造器()方法之中,把value指派為123的動作将在初始化階段才會執行。
public static final int v = 8080; #在編譯階段會為v生成`ConstantValue`屬性,在準備階段虛拟機會根據`ConstantValue`屬性将v指派為8080。           
  • 解析:

    是虛拟機将常量池中的

    符号引用

    替換為

    直接引用

    的過程。符号引用就是class檔案中的

    CONSTANT_Class_info

    CONSTANT_Field_info

    CONSTANT_Method_info

符号引用:引用的目标并不一定要已經加載到記憶體中。各種虛拟機實作的記憶體布局可以各不相同,但是它們能接受的符号引用必須是一緻的,因為符号引用的字面量形式明确定義在Java虛拟機規範的Class檔案格式中。

直接引用:指向目标的指針,相對偏移量或是一個能間接定位到目标的句柄。如果有了直接引用,那引用的目标必定已經在記憶體中存在。

  • 初始化:

    初始化階段是執行類構造器方法的過程。方法是由編譯器自動收集類中的類變量的指派操作和靜态語句塊中的語句合并而成的。虛拟機會保證方法執行之前,父類的方法已經執行完畢。如果一個類中沒有對靜态變量指派也沒有靜态語句塊,那麼編譯器可以不為這個類生成()方法。

什麼時候需要對類進行初始化?
使用new該類執行個體化對象的時候;
讀取或設定類靜态字段的時候(但被final修飾的字段,在編譯器時就被放入常量池的靜态字段除外static final);
調用類靜态方法的時候;
使用反射Class.forName(“xxxx”)對類進行反射調用的時候,該類需要初始化;
初始化一個類的時候,有父類,先初始化父類(注:1. 接口除外,父接口在調用的時候才會被初始化;2.子類引用父類靜态字段,隻會引發父類初始化);
被标明為啟動類的類(即包含main()方法的類)要初始化;
當使用JDK1.7的動态語言支援時,如果一個java.invoke.MethodHandle執行個體最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。           
通過子類引用父類的靜态字段,隻會觸發父類的初始化,而不會觸發子類的初始化。
定義對象數組,不會觸發該類的初始化。
常量在編譯期間會存入調用類的常量池中,本質上并沒有直接引用定義常量的類,不會觸發定義常量所在的類。
通過類名擷取Class對象,不會觸發類的初始化。
通過Class.forName加載指定類時,如果指定參數initialize為false時,也不會觸發類初始化,其實這個參數是告訴虛拟機,是否要對類進行初始化。
通過ClassLoader預設的loadClass方法,也不會觸發初始化動作。           
Java語言系統自帶三個類加載器:

虛拟機設計團隊把加載動作放到JVM外部實作,以便讓應用程式決定如何擷取所需的類:

  • Bootstrap ClassLoader

    最頂層的啟動類加載器,主要加載核心類庫,

    %JRE_HOME%\lib

    下的jar包和class檔案。或通過

    -Xbootclasspath

    參數指定路徑中的類。
  • Extention ClassLoader

    擴充的類加載器,加載目錄

    %JRE_HOME%\lib\ext

    目錄下的jar包和class檔案。
  • Appclass Loader

    應用程式類加載器,加載使用者路徑的

    classpath的所有類

三個類加載器的加載順序

  • 先建立個項目,再建立個

    Test.java

java.lang.ClassLoader cl = Test.class.getClassLoader();        
System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString()); 
System.out.println("ClassLoader\'s grand father is:"+cl.getParent().getParent().toString());            
  • 自己編寫的

    Test.class

    檔案是由

    AppClassLoader

    加載的
  • AppClassLoader

    的parent是

    ExtClassLoader

    ExtClassLoader

    null

  • BootStrapClassLoader

    ExtClassLoader

    AppClassLoader

    都是加載指定路徑下的jar包

JVM通過雙親委派模型進行類的加載,也可以通過繼承

java.lang.ClassLoader

實作自定義的類加載器。

當一個類加載器收到類加載任務,會先交給其父類加載器去完成,是以最終加載任務都會傳遞到頂層的啟動類加載器,隻有當父類加載器無法完成加載任務時,才會嘗試自己執行加載任務。采用雙親委派的一個好處是比如加載位于

rt.jar

包中的類

java.lang.Object

,不管是哪個加載器加載這個類,最終都是委托給頂層的啟動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個Object對象。

加載過程

  • 首先通過Class c = findLoadedClass(name);判斷一個類是否已經被加載過。
  • 如果沒有被加載過執行if (c == null)中的程式,遵循雙親委派的模型,首先會通過遞歸從父加載器開始找,直到父類加載器是Bootstrap ClassLoader為止。
  • 最後根據resolve的值,判斷這個class是否需要解析。

一個執行個體

class Singleton{ 
    private static Singleton singleton = new Singleton(); 
    public static int value1; 
        public static int value2 = 0; 
    private Singleton(){ 
        value1++; 
        value2++; 
        } 
    public static Singleton getInstance(){ 
        return singleton; 
        } 
    } 
class Singleton2{ 
    public static int value1; 
    public static int value2 = 0; 
    private static Singleton2 singleton2 = new Singleton2(); 
    private Singleton2(){ 
        value1++; 
        value2++; 
        } 
    public static Singleton2 getInstance2(){ 
        return singleton2;
        }
    }
public class jvm {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("Singleton1 value1:" + singleton.value1); 
        System.out.println("Singleton1 value2:" + singleton.value2); 
        Singleton2 singleton2 = Singleton2.getInstance2(); 
        System.out.println("Singleton2 value1:" + singleton2.value1); 
        System.out.println("Singleton2 value2:" + singleton2.value2); 
        }
}           
結果

Singleton輸出結果:1 0

首先執行main中的Singleton singleton = Singleton.getInstance();
加載:加載類Singleton
準備:為靜态變量配置設定記憶體,設定預設值。這裡為singleton(引用類型)設定為null,value1,value2(基本資料類型)設定預設值0
初始化(按照指派語句進行修改):
    執行private static Singleton singleton = new Singleton();
    執行Singleton的構造器:value1++;value2++; 此時value1,value2均等于1
    執行
    public static int value1;
    public static int value2 = 0;
    此時value1=1,value2=0           

Singleton2輸出結果:1 1

執行main中的Singleton2 singleton2 = Singleton2.getInstance2();
加載:加載類Singleton2
準備:為靜态變量配置設定記憶體,設定預設值。這裡為value1,value2(基本資料類型)設定預設值0,singleton2(引用類型)設定為null,
初始化(按照指派語句進行修改):
    執行public static int value2 = 0;
    此時value2=0(value1不變,依然是0);
    執行
    private static Singleton singleton = new Singleton();
    執行Singleton2的構造器:value1++;value2++;
    此時value1,value2均等于1,即為最後結果           

參考文獻:

https://blog.csdn.net/briblue/article/details/54973413 https://yq.aliyun.com/articles/518315 https://blog.csdn.net/noaman_wgs/article/details/74489549 http://www.importnew.com/30567.html