JVM虛擬機學習一、類加載機制

JVM虛擬機學習一、類加載機制

1.類的加載過程

​ 類從加載到虛擬機內存到卸載出內存的過程中,一共經歷了加載驗證準備解析初始化使用卸載這幾個階段,其中準備、驗證、解析3個部分統稱爲連接。

加載驗證準備初始化卸載,這五個階段的順序是一致的,而解析過程則不一定,它在某些情況下在初始化階段之後在進行,這是爲了支持Java的運行時綁定。(關於運行時綁定:https://www.cnblogs.com/ygj0930/p/6554103.html)

加載

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

在加載階段(可以參考java.lang.ClassLoader的loadClass()方法)。

驗證

​ 這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

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

驗證階段是非常重要的,但不是必須的。

準備

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

static int i=123;

​ 這個階段只會將i的值初始化爲0,並不是123。因爲這時候尚未開始執行任何java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器()方法之中,所以把value賦值爲123的動作將在初始化階段纔會執行。
至於“特殊情況”是指:public static final int value=123,即當類字段的字段屬性是ConstantValue時,會在準備階段初始化爲指定的值,所以標註爲final之後,value的值在準備階段初始化爲123而非0

解析

該階段主要是將常量池中的符號引用替換爲直接引用。

String str="123"

符號引用就是說這裏的“=”只是一個符號的作用,並沒有將常量池中的對象地址引用給str,而直接引用做了地址賦值的工作。

初始化

類初始化階段是類加載過程的最後一步,到了初始化階段,才真正開始執行類中定義的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;
}

<clinit>()方法與實例構造器<init>()方法不同,它不需要顯示地調用父類構造器,虛擬機會保證在子類<init>()方法執行之前,父類的<clinit>()方法方法已經執行完畢。這就很好的解釋了初始化子類之前,會先初始化父類。

<clinit>()方法對於類或者接口來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不爲這個類生產<clinit>()方法。
​ 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法。但接口與類不同的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的<clinit>()方法。
​ 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有好事很長的操作,就可能造成多個線程阻塞,在實際應用中這種阻塞往往是隱藏的。

那麼哪些情況會觸發程序的初始化呢?

  1. 遇到new,getstatic,putstatic,invokestatic這失調字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  4. 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

這些例子是不會進行初始化的:

  1. 子類引用父類的靜態字段,不會導致子類初始化。
class SSClass
{
    static
    {
        System.out.println("SSClass");
    }
}    
class SuperClass extends SSClass
{
    static
    {
        System.out.println("SuperClass init!");
    }
 
    public static int value = 123;
 
    public SuperClass()
    {
        System.out.println("init SuperClass");
    }
}
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);
    }
}


/*output:
		SSClass
		SuperClass init!
		123
*/

對於調用靜態字段,只有直接定義該字段的類纔會被初始化!

  1. 通過數組定義來引用類,不會觸發此類的初始化
class SubClass{
	static {
		System.out.println("ss");
	}
}
public class test{
    public static void main(String[] args){
    	SubClass[] ss=new SubClass[10];
    }
}
//並不會輸出任何值
  1. 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
class SubClass{
	static {
		System.out.println("ss");
	}
	public static final String str="hello";
}
public class test{
    public static void main(String[] args){
    	System.out.println(SubClass.str);
    }
}
/*
	output:hello
*/

轉載自:http://www.importnew.com/18548.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章