1. 程式人生 > >Java 虛擬機器類載入機制

Java 虛擬機器類載入機制

看到這個題目,很多人會覺得我寫我的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)。如圖所示。

640?wx_fmt=png

載入、驗證、準備、初始化和解除安裝這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;

}

<clinit>()方法與例項構造器<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

需要注意的是,其他執行緒雖然會被阻塞,但如果執行<clinit>()方法的那條執行緒退出<clinit>()方法後,其他執行緒喚醒之後不會再次進入<clinit>()方法。同一個類載入器下,一個型別只會初始化一次。

將上面程式碼中的靜態塊替換如下:

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類已在本文開篇定義)

public class NotInitialization

{

    public static void main(String[] args)

    {

        SuperClass[] sca = new SuperClass[10];

    }

}

執行結果:(無)

2. 常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化:

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

附:昨天從論壇上看到一個例子,很有意思,如下:

package jvm.classload;

public class StaticTest

{

    public static void main(String[] args)

    {

        staticFunction();

    }

    static StaticTest st = new StaticTest();

    static

    {

        System.out.println("1");

    }

    {

        System.out.println("2");

    }

    StaticTest()

    {

        System.out.println("3");

        System.out.println("a="+a+",b="+b);

    }

    public static void staticFunction(){

        System.out.println("4");

    }

    int a=110;

    static int b =112;

}

問題是:請問輸出是什麼?

相關推薦

java虛擬機器載入機制學習

1、什麼是類的載入 類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構。類的載入的最終產品是位於堆區中的Class物件,Class物件封裝了類在方法區內的資料結構

Java虛擬機器載入機制經典案例

package io.lgxkdream.test; class Father { static Father f = new Father(); static { System.out.println("father-1"); } { System.out.println("

jdk原始碼解析(七)——Java虛擬機器載入機制

前面我們講解了class檔案的格式,以及它是什麼樣的。那麼接下來需要了解它怎麼被載入到jvm中呢?jvm的載入機制又是怎麼一個過程呢?本文參考了《Java 虛擬機器規範(Java SE 7 版)》的第五章內容來詳細解釋一下 虛擬機器類載入機制:虛擬機器把描述類的資料從cla

Java 虛擬機器載入機制

看到這個題目,很多人會覺得我寫我的java程式碼,至於類,JVM愛怎麼載入就怎麼載入,博主有很長一段時間也是這麼認為的。隨著程式設計經驗的日積月累,越來越感覺到了解虛擬機器相關要領的重要性。閒話不多說,老規矩,先來一段程式碼吊吊胃口。public class SSClass{

Java虛擬機器載入機制

原文出處:http://www.importnew.com/18548.html類載入過程類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolut

JVM(三)-java虛擬機器載入機制

概述:   上一篇文章,介紹了java虛擬機器的執行時區域,Java虛擬機器根據不同的分工,把記憶體劃分為各個不同的區域。在java程式中,最小的執行單元一般都是建立一個物件,然後呼叫物件的某個 方法。通過上一篇文章我們知道呼叫某個方法是通過虛擬機器棧的棧幀並通過執行引擎來實現的,但是實際上一個方法的執行前提

《深入理解Java虛擬機器》個人讀書總結——虛擬機器載入機制

我們都知道Java虛擬機器是用來執行我們編譯好的.class檔案的,class檔案中夾帶類的各種資訊,虛擬機器要執行這些檔案,第一件事就是要載入到虛擬機器中,這就引出了這次總結的問題——虛擬機器是如何載入這些class檔案的?載入後虛擬機器是怎麼處理檔案中夾帶的資訊的? 類載入機制

JAVA虛擬機器(七)虛擬機器載入機制

虛擬機器的類載入機制是指 把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗,轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。類的載入連線和初始化過程都是在程式執行期間完成的。 類的生命週期: 載入->連線(驗證,準備,解析)->初始化->使用

深入理解Java虛擬機器筆記——虛擬機器載入機制

虛擬機器類載入機制 類載入機制:虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗、  轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。 在Java中,型別的載入和連線過程都是在程式執行期間完成的。   類載入時機(類從載入到虛擬

讀書筆記 ---- 《深入理解Java虛擬機器》---- 第6篇:虛擬機器載入機制

上一篇:類檔案結構:https://blog.csdn.net/pcwl1206/article/details/84197219 第6篇:虛擬機器類載入機制 1、概述 上一篇文章中講訴了Class檔案儲存格式的具體細節,在Class檔案中的描述的各種資訊,最終都要載入到虛擬機器中之後才

讀薄《深入理解 Java 虛擬機器虛擬機器載入機制

#虛擬機器類載入機制 類被載入的生命週期包括 載入→驗證→準備→解析→初始化→使用→解除安裝 解析階段在某些情況下可以在初始化階段之後開始,這是為了支援 Java 語言的執行時繫結。 虛擬機器規範嚴格規定了有且只有 5 種情況必須立即對類進行初始化。 遇到 n

深入理解java虛擬機器---4虛擬機器載入機制

類載入的整個生命週期:    載入、連線(驗證、準備、解析)、初始化、使用、解除安裝。 載入:      class檔案中的二進位制資料讀取到記憶體中,然後將該位元組流所代表的靜態資料結構轉化為方法區中執行的資料結構,並且在堆記憶體中生成一個java.lang.Class物

詳解Java記憶體區域?虛擬機器載入機制

一、Java執行時資料區域 1、程式計數器 “執行緒私有”的記憶體,是一個較小的記憶體空間,它可以看做當前執行緒所執行的位元組碼的行號指示器。Java虛擬機器規範中唯一一個沒有OutOfMemoryError情況的區域。 位元組碼直譯器工作時就說通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,

深入理解Java虛擬機器學習筆記——三、虛擬機器載入機制

1、概述 虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成能夠被虛擬機器直接使用的資料型別,這就是虛擬機器的類載入機制。在Java中,類的載入、校驗、解析和初始化都是在執行期間完成的。 2、類載入的時機 類從被載入都虛擬機器記

深入理解JAVA虛擬機器(四):虛擬機器載入機制

虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。在Java語言裡面,型別的載入、連線和初始化過程都是在程式執行期間完成的。 1、類載入的時機 類從被載入到虛擬機

Java記憶體區域與虛擬機器載入機制

一、Java執行時資料區域 1、程式計數器  “執行緒私有”的記憶體,是一個較小的記憶體空間,它

深入理解JAVA虛擬機器讀書筆記----虛擬機器載入機制

概述 虛擬機器類載入機制:虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。 不像C語言,寫好程式碼後,編譯-》連結-》執行;Java語言裡,型別的載入和連線過程是在程式執行

Java虛擬機器載入器及雙親委派機制

所謂的類載入器(Class Loader)就是載入Java類到Java虛擬機器中的,前面《面試官,不要再問我“Java虛擬機器類載入機制”了》中已經介紹了具體載入class檔案的機制。本篇文章我們重點介紹載入器和雙親委派機制。 類載入器 在JVM中有三類ClassLoader構成:啟動類(或根類)載入器(Bo

JVM的虛擬機器載入機制

首先,我們要知道為什麼會存在類的載入機制。Java語言編寫的.java在經過編譯器編譯後會生成.class檔案,這個和C\C++語言是不一樣的。 C語言它們是會被編譯生成為本地機器碼,然後在被執行。這種做法的缺點就是無法完成編寫程式碼的跨平臺使用。想想就知道,windows下編譯好的程式碼在li

深入理解JVM虛擬機器讀書筆記【第七章】虛擬機器載入機制

7.1 概述 7.2 類載入的時機 7.3 類載入的過程 7.3.1 載入 7.3.2 驗證 1.檔案格式驗證 2.元資料驗證 3.位元組碼驗證