1. 程式人生 > 其它 >JVM系列--類載入過程

JVM系列--類載入過程

技術標籤:Java(coding)

基礎

其他網址

Java類載入的過程_Java_持之以恆!-CSDN部落格
另見:《深入理解Java虛擬機器 JVM高階特性與最佳實踐 第2版》=>第7章 虛擬機器類載入機制

載入過程

載入=> 連結(驗證+準備+解析)=> 初始化=> 使用=> 解除安裝

1.載入

(1)通過一個類的全限定名類獲取其定義的二進位制位元組流
(2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
(3)在堆中生成一個代表這個類的Class物件,作為方法區中這些資料的訪問入口。

2.連結:其中解析步驟是可選的。

(a)驗證:檢查載入的class檔案資料的正確性

(b)準備:給類變數(靜態變數)分配儲存空間並全部初始化為0
(c)解析:將常量池符號引用轉成直接引用

3.初始化:初始化類變數(靜態變數)、執行靜態語句塊

執行類變數(靜態變數)的賦值動作和靜態語句塊(賦值與語句塊執行是按照定義的順序進行的)。優先順序:靜態、父類、子類)

注意:初始化是操作類變數(也就是Class的變數),不是物件的變數。

4.使用:以new一個物件為例

  • 若是第一次建立 Dog 物件(物件所屬的類沒有載入到記憶體中)則先執行上面的初始化操作。
  • 在堆上為 Dog 物件分配空間,所有屬性和方法都設成預設值(數字為 0,字元為 null,布林為 false,引用被設成 null)
  • 初始化例項:給例項變數賦值、執行初始化語句塊
  • 執行建構函式檢查是否有父類,如果有父類會先呼叫父類的建構函式
  • 執行建構函式。

虛擬機器規範嚴格規定: 有且只有 5 種情況 必須立即對類進行初始化(載入、驗證、準備自然需要在之前執行):

1) 遇到 new 、getstatic、putstatic 或 invokestatic 這 4 條位元組碼指令時,若沒有對類進行初始化,則要先觸發其初始化。
這4個指令含義是:使用 new 新建一個 Java 物件,訪問或者設定一個類的靜態欄位,訪問一個類的靜態方法。
2)使用 java.lang.reflect 包的方法對類進行反射呼叫的時候,如果類沒有進行初始化,則需要先觸發其初始化。

3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
4)當虛擬機器啟動的時候,使用者需要指定一個需要執行的主類(包含 main 方法的那個類),虛擬機器會先初始化這個類。
5)當使用 JDK 1.7 的動態語言支援時,如果一個 java.lang.invoke.MethodHandle 例項最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法控制代碼,並且這個方法控制代碼對應的類沒有進行過初始化,則需要先觸發其初始化。

驗證類載入

package org.example.a;

class Tester1 {
    //如果靜態變數的賦值放到靜態程式碼塊之後,則會報錯:非法向前引用
    static int i = 2;

    static{
        System.out.println("初始化類:i = " + i);
    }

    Tester1(){
        System.out.println("構造方法");
    }

    public void test(){
        System.out.println("test 方法");
    }
}

public class Demo {

    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        //裝載類
        cl.loadClass("org.example.a.Tester1");
        System.out.println("裝載類");
        //初始化類
        Class.forName("org.example.a.Tester1");
        System.out.println("初始化結束");
        Tester1 test = new Tester1();
        test.test();
    }
}

執行結果

裝載類
初始化類:i = 2
初始化結束
構造方法
test 方法

問答與驗證

為什麼靜態方法不能呼叫非靜態方法和變數?

靜態方法是屬於類的,在類載入的時候就會分配記憶體,可以通過類名直接去訪問。非靜態成員(變數和方法)屬於類的物件,只有該物件例項化之後才存在,然後通過類的物件去訪問。

載入階段的第二步((2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構)會將所有static修飾的內容(包括靜態屬性、靜態程式碼塊、靜態方法等)轉為方法區的執行時資料結構,但此時非靜態的方法和變數根本沒經過初始化(沒有記憶體),所以會失敗。

類初始化順序例項驗證

package org.example.a;

class Test{
    int a = func1();
    static int b = func2();

    static {
        System.out.println("static block");
    }
    {
        System.out.println("instance block");
    }
    public Test(){
        System.out.println("constructor");
    }

    public int func1(){
        System.out.println("instance func1");
        return 2;
    }

    public static int func2(){
        System.out.println("static func2");
        return 2;
    }
}

public class Demo {
    public static void main(String[] args) {
        Test child = new Test();
    }
}

執行結果

static func2
static block
instance func1
instance block
constructor

單獨裝載與初始化

package org.example.a;

class Test{
    int a = func1();
    static int b = func2();

    static {
        System.out.println("static block");
    }
    {
        System.out.println("instance block");
    }
    public Test(){
        System.out.println("constructor");
    }

    public int func1(){
        System.out.println("instance func1");
        return 2;
    }

    public static int func2(){
        System.out.println("static func2");
        return 2;
    }
}

public class Demo {
    public static void main(String[] args) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        try {
            //裝載類
            cl.loadClass("org.example.a.Test");
            System.out.println("裝載類結束");
            //初始化類
            Class.forName("org.example.a.Test");
            System.out.println("初始化結束");
            Test test = new Test();
            test.func1();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

執行結果

裝載類結束
static func2
static block
初始化結束
instance func1
instance block
constructor
instance func1

訪問類或介面的靜態域時,只有真正宣告這個域的類或接口才會被初始化

package org.example.a;

class B {
    static int value = 100;
    static {
        System.out.println("Class B is initialized."); //輸出
    }
}
class A extends B {
    static {
        System.out.println("Class A is initialized."); //不會輸出
    }
}

public class Demo {
    public static void main(String[] args) {
        System.out.println(A.value); //輸出100
    }
}

執行結果

Class B is initialized.
100

執行緒安全

其他網址

類載入過程的執行緒安全性保證(讓實現執行緒安全的單例,又不讓使用synchronized!)_zhangustb-CSDN部落格
Java類的載入、連結和初始化-HollisChuang's Blog

《深入理解Java虛擬機器 JVM高階特性與最佳實踐 第2版》=>第7章 虛擬機器類載入機制=>7.3 類載入的過程=>7.3.5 初始化

簡介

Java類的載入和初始化過程都是執行緒安全的。具體原因如下:

載入:類載入整個過程是執行緒安全的,因為loadClass方法內有synchronized。

初始化:虛擬機器會保證一個類的<Clinit>()方法在多執行緒環境中被正確地加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<dinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行<dinit>()方法完畢。

總結

類的靜態資源都是執行緒安全的,而且是單例的。因此,在寫單例模式時,經常會使用static標記例項變數。比如:

載入原始碼

見java.lang.ClassLoader#loadClass:

下邊方法前邊有個註釋:
Unless overridden, this method synchronizes on the result of <getClassLoadingLock> method during the entire class loading process.
//除非被重寫,否則整個裝載過程中是執行緒安全的。注意:本處裝載是裝載、連結、初始化的裝載,而不是整個載入流程

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);
		if (c == null) {
			long t0 = System.nanoTime();
			try {
				if (parent != null) {
					c = parent.loadClass(name, false);
				} else {
					c = findBootstrapClassOrNull(name);
				}
			} catch (ClassNotFoundException e) {
				// ClassNotFoundException thrown if class not found
				// from the non-null parent class loader
			}

			if (c == null) {
				// If still not found, then invoke findClass in order
				// to find the class.
				long t1 = System.nanoTime();
				c = 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;
	}
}

Class物件

基礎

其他網址

《深入理解Java虛擬機器 JVM高階特性與最佳實踐 第2版》=> 第7章 虛擬機器類載入機制

程式碼例項

一旦類被載入了到了記憶體中,那麼不論通過哪種方式獲得該類的Class物件,它們返回的都是指向同一個java堆地址(對於HotSpot是方法區)上的Class引用。

package org.test.a;

class Cat{
    static {
        System.out.println("static cat");
    }
}

public class Demo {
    public static void main(String[] args) throws ClassNotFoundException {
        Class c1 = Cat.class;
        Class c2 = new Cat().getClass();
        Class c3 = new Cat().getClass();
        Class c4 = Class.forName("org.test.a.Cat");
        System.out.println(c1 == c2);
        System.out.println(c2 == c3);
        System.out.println(c3 == c4);
    }
}

輸出結果

static cat
true
true
true

分析

記憶體的位元組碼塊就是完整的把整個類裝到了記憶體。使用的主要步驟如下:

  1. 當一個ClassLoder啟動的時候,ClassLoader生存在jvm中的堆。
  2. ClassLoder去主機硬碟上將A.class裝載到jvm的方法區。
  3. new A()時:虛擬機器使用方法區中的位元組檔案在堆記憶體生成了一個A位元組碼的物件
  4. 然後A位元組碼這個記憶體檔案有兩個引用:一個指向A的class物件,一個指向載入自己的classLoader

靜態內部類的載入時機

只有當我們有對類的引用的時候,才會將類初始化。比如以下情況

  • new一個非靜態類的物件
  • 訪問靜態類的成員(包括方法和屬性)

new一個外部類的時候,載入階段會掃描static修飾的東西,初始化階段只會執行static修飾的屬性的賦值以及static程式碼塊。利用這個特性,可以做基於靜態內部類的單例模式:Java設計模式系列--單例模式的5種寫法_設計模式_feiying0canglang的部落格-CSDN部落格

測試

package org.example.a;

class OuterClass {
    static {
        System.out.println("OuterClass static");
    }
    public static class InnerClass {
        static int a = 1;
        static{
            System.out.println("InnerClass static");
        }

        static void innerMethod(){
            System.out.println("innerMethod");
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        //OuterClass outerClass = new OuterClass();
        //OuterClass.InnerClass.a = 2;
        //OuterClass.InnerClass.innerMethod();
    }
}

只放開第一個測試的執行結果

OuterClass static

只放開第二個測試的執行結果

InnerClass static

只放開第三個測試的執行結果

InnerClass static
innerMethod