深入理解java虛擬機(4)---類加載機制


  類加載的過程包括:

  加載class到內存,數據校驗,轉換和解析,初始化,使用using和卸載unloading過程。

除了解析階段,其他過程的順序是固定的。解析可以放在初始化之后,目的就是為了支持動態加載。

從java開發者來講,我們並不關心具體細節,只要知道整個流程以及每個流程大體干了那些事情。

每個流程具體對開發代碼會有那些影響就可以了。

類的加載流程

1.加載loading

  在加載過程中,虛擬機需要完成3件事情:

1)通過一個類的全限定名來獲得此類的二進制字節流。

2)將這個直接流的靜態存儲結構轉化為方法區的運行時數據結構。

3)在內存中生成一個代表這個類的class對象,作為方法區這個類的數據訪問入口。

2.驗證

驗證是虛擬機非常重要的一步,其目的是為了確保class文件的字節流符合java虛擬機自身的要求,不會導致虛擬機崩潰。

java語言本身是比較安全的語言,它沒有數組越界等情況的發生。But,class語言並不是一定由java語言產生的。甚至於,

可以直接使用16進制工具編寫class文件。而這些文件就不能保證class文件的規范性。

大致分成4個階段的驗證過程: 文件格式驗證元數據驗證字節碼驗證符號引用驗證
 
文件格式驗證:
比如是否以魔數開頭,主次版本號是否在虛擬機可處理范圍之內,常量池是否有不支持類型等。
經過這個階段的驗證之后,字節流才會進入內存的方法區進行存儲,所以 后面的三個驗證階段全部是基於方法區的存儲結構進行的
 
元數據驗證:
對字節碼描述的信息進行 語義分析,以保證其描述的信息符合JAVA語言規范的要求,這個階段可能包括的驗證點有:
這個類是否有父類,父類是否集成了不允許繼承的類,如果不是抽象類是否實現了其父類或接口中要求實現的所有方法,類中的字段和父類是否有矛盾
 
字節碼驗證:
最復雜的一個解讀那,主要工作是 進行數據流和控制流分析。這階段對類的方法體進行校驗分析,保證該方法在運行時不會做出危害JVM安全的行為,例如:
保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作,保證跳轉指令不會跳轉到方法體以外的字節碼指令上,保證方法體中的類型轉換是有效的。。。
這個驗證並不能保證一定安全( 停機問題,通過程序去校驗程序邏輯是無法做到絕對准確的
1.6加入StackMapTable功能對這個階段做了優化,提高速度,但這個StackMapTable也可能被篡改,可以通過啟動參數來關閉這個選項。
 
符號引用驗證:
這個階段發生在虛擬機將符號引用轉化為直接引用的時候。 這個轉化動作將在連接的第三個階段----解析階段中發生
可以看作是對類自身以外的信息進行匹配性的校驗。
比如:符號引用中通過字符串描述的全限定名是否能找到對應的類,是否存在所描述的方法和字段。。。。
如果無法通過符號驗證,將會拋出一個Java.lang.IncompatibleClassChangeError異常的子類,比如Java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchMethodError
可以使用啟動參數來關閉大部分類驗證措施,縮短虛擬機類加載時間
 

3.准備

准備階段就是為類的變量正式分配內存並設置初始值。這個初始值與初始化不是同一個概念。

比如

public static int value = 12;

這個階段value的值為0 而不是12。value賦值為12的階段  

是在初始化的過程中出現的。
 
java所有的基本類型都賦值為零值。(簡單來說就是0 or null,0.0f,false等)
這里可以明確,類的屬性是會默認初始值的。而局部變量沒有初始值。所以是未定義的。
 

4.解析

解析是java語言面向對象的基礎。

解析的過程是將常量池里面的字符引用替換為直接引用的過程。

符號引用是 一組以符號來描述所引用的目標。各種虛擬機的內存布局可以各不相同,但是字面量的形式有虛擬機規范嚴格規定。

直接引用就是對虛擬機內存布局的直接描述。

所以引用的目標必須已經加載到內存里面了。

1).類或接口的解析

類和接口的解析:假設當前代碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或接口C的直接引用,那虛擬機完成整個解析的過程需要以下三個步驟:

如果C不是一個數組類型,那虛擬機將會把代表N的全限定名傳遞給D的類加載器去加載這個類C

如果C是數組類型,並且數組的元素類型是對象,則按照1的情況處理。如果元素類型不是對象,則由虛擬機生成一個代表此數組維度和元素的數組對象

如果上述步驟沒有異常,C在虛擬機中實際已經成為一個有效的類或接口了,但在解析完成之前還要進行符號引用驗證,確認C是否具有對D的訪問權限,如果沒有則拋出java.lang.IllegalAccessError異常。

2).字段解析

大體情況如下:

class D{
    
  public D(C c)
    {
         string a = c.a;   
    }  
}    

D 需要加載C.a 字段,首先,需要加載的是C類的解析內容。然后關鍵部分就是java語言繼承的東東了。

如果C類本生就含有a的字段,直接返回a的直接引用。

搜索C類的接口,按照繼承關系從上到下搜索各個接口已經父接口,直到找到a字段。如果沒有

if C is not the java.lang.Object,同上,搜索C類的父類,如果有,就使用該字段的直接引用。如果沒有

也就是C類及其相關類or接口沒有這個字段,查找失敗。

如果找到,還需要進行權限驗證。

如果接口 & 類 都包含相同名的字段,java程序員有時候會無法判斷到底使用的是哪個字段。

所以編譯器一般會拒絕這種情況的發生。

以下是使用androidstudio 實驗的結果:

public interface ICoo {
    public static int A = 1;
}

public abstract class CooAbstruct {
    public int A = 22;
}

public class Coo extends CooAbstruct implements ICoo {

//    public int geta()
//    {
//        return A;
//    }
}
public class Doo {

    public Doo(Coo c)
    {
        int a  = c.A;
    }
}

E:\GitHub\jvmdemo\app\src\main\java\com\joyfulmath\myapplication\Doo.java:13: 錯誤: 對A的引用不明確, CooAbstruct中的變量 A和ICoo中的變量 A都匹配
int a = c.A;
^
1 個錯誤

可以看到,編譯器明確 無法區分A到底是使用哪個字段。

在C++的多繼承中,類似的情況在使用時需要明確到底是使用哪個子類的字段。

 

3)類的方法加載:

同樣使用C類來描述這個過程:

類方法和接口方法 常量類型是分開的。所以如果C類方法發現是一個接口的方法的話,直接回拋出異常。類型檢測。

直接在C類里面尋找是否有匹配的字符描述的方法。沒有就繼續

在C類的父類里面遞歸尋找,沒有就繼續

在C類的接口里面遞歸尋找,找到,說明本方法未被實現,C類是抽象類。拋出異常

都沒有找到,nosuchmethod。

如果找到有效的匹配方法后,檢查權限。

4)接口的加載方法

過程同類的方法基本一致。只是不需要進行權限檢查。

5.初始化

初始化和准備階段是不同的過程,而且是java程序員最關心的部分。

1.必須初始化的情況

java虛擬機規范 規定了5種 (有且僅有)情況下,必須進行初始化的操作。

1)遇到new,getstatic,putstatic,invokestatic 這4條指令的時候。對應場景:

實例化一個類,讀取或者設置一個類的靜態字段,調用一個類的靜態方法時候。

2)使用反射方法調用的時候,需要先初始化。

3)當初始化一個類時,需要先初始化父類。

4)當虛擬機啟動時,需要指定一個啟動類(main類),虛擬機會首先初始化這個類。

5)當使用jdk1.7動態語言時候,具體情況本文不做分析。

一下使用幾個demo來說明我們容易誤解的地方:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TraceLog.i(String.valueOf(SubClass.value));
    }

}
public class SubClass extends SuperClass {
    static {

        TraceLog.i("subclass init!");
    }
}
public class SuperClass {
    static {
        TraceLog.i("SuperClass init!");
    }

    public static int value = 12;
}

結果log:

05-08 10:10:33.783 868-868/com.joyfulmath.myapplication I/SuperClass: <clinit>: SuperClass init! [at (SuperClass.java:13)]
05-08 10:10:33.783 868-868/com.joyfulmath.myapplication I/MainActivity: onCreate: 12 [at (MainActivity.java:19)]

是的,只有父類被初始化了,子類沒有初始化,why?

應為value定義的父類,所以只需要初始化父類就可以的。

public class SuperClass {
    static {
        TraceLog.i("SuperClass init!");
    }

    public static int value = 12;

    public SuperClass()
    {
        TraceLog.i("SuperClass construct");
    }
}

實例化construction函數沒有走到,所以沒有實例被創建!!!but,我們在看log,<clinit> 這個是神馬?這個就是打印SuperClass.init所在的函數!!!

這個等到下面在講,我們繼續我們的demo。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
//        TraceLog.i(String.valueOf(SubClass.value));
        TraceLog.i();
        SuperClass[] a  = new SuperClass[10];
    }

 

05-08 10:22:33.100 12438-12438/com.joyfulmath.myapplication I/MainActivity: onCreate:  [at (MainActivity.java:21)]

 

what? 對於SuperClass 沒有一行log,也就是根本沒有初始化SuperClass。

它觸發了一個類為“[xxx.Superclass“ , 這是SuperClass對應的數組類,是由虛擬機自動生成的。 

 

 TraceLog.i(a[0].toString());
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.Object.toString()' on a null object reference
                                                                                  at com.joyfulmath.myapplication.MainActivity.onCreate(MainActivity.java:23)
                                                                                  at android.app.Activity.performCreate(Activity.java:5961)
                                                                                  at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1129)
                                                                                  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2364)

a[0] 居然是null? 是的,數組a里面都是null。對的a只是一個數組,a的類型為”[xxx.Superclass“ 不是SuperClass。所以數組不會自動初始化元數據。

常量。 

常量存放在常量池里面,所以對常量的引用在編譯階段就已經被優化。

下面我們來講講<clinit> 這個東東。

靜態代碼塊+所有類的變量的賦值動作。

這里有一點需要強調:編譯器收集的順序與由源代碼在文件中的順序是一致的。

<clinit>()方法是由編譯器自動收集類中的所有類變量的復制動作和靜態語句塊中的語句合並而成。編譯器收集的順序和語句在源文件中出現的順序一致,靜態語句塊中只能訪問到定義在它之前的變量,定義在它之后的變量,只能賦值,不能訪問

<clinit>()方法與類的構造函數<init>()不同,不需要顯式的調用父類構造器,虛擬機會保證父類的<clinit>()在子類的之前完成。因此,虛擬機執行的第一個<clinit>()方法肯定是java.lang.Object.

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

先看個例子來說明上述概念:

public class SuperClass {
    public static int A = 1;

    static {
        A = 2;
        TraceLog.i("SuperClass init!");
    }

    public static int value = 12;

    public SuperClass()
    {
        A = 3;
        TraceLog.i("SuperClass construct");
    }
}
public class SubClass extends SuperClass {
    public static int B = A;
    static {

        TraceLog.i("subclass init! B:"+B);
    }
}

如圖所示:父類里面A有3個地方賦值。那么B到底是多少呢?

subclass 在給B賦值以前,會首先走完superclass的<clinit>.所以 A的值是2.

so, B輸出的值 就是2.在B賦值的時候,構造函數沒有調用。(construction操作只有在實例化的時候,會被調用!)

 

<clinit>()方法並不是必須的,如果一個類沒有靜態語句塊也沒有對變量賦值操作,就不會生成

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

虛擬機會保證一個類的<clinit>()方法在多線程環境中正確的加鎖同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法,其他線程都會阻塞,直到該方法執行完,如果在一個類的<clinit>()方法中有耗時很長的操作,可能會造成多個進程阻塞,在實際應用中,這種阻塞往往很隱蔽。

類加載器

關於類的解析,C++等語言都是有編譯器執行器或者說IDE環境解決了,我們也無法進行干預。

但是java是由虛擬機來加載類,一般情況下虛擬就就可以加載類。But如果通過網絡下發類,就會轉化成2進制的代碼

由於加密的原因,這個類無法被虛擬機解析,所以需要我們自己寫類加載器來解析這個類。這是java流行的一個重要原因。

而目前流行的技術就是OSGi。OSGi是非常好的一個代表,所以關於這部分的內容,如果OSGi研究后,就可以非常了解類加載器。

6.1 類 & 類加載器

如何確定2個類是相同的,包括equals & instanceof等。 

相同的二進制代碼,由不用的加載器加載,對應的是不同的類型對象。

所以判斷相同的類對象,必須是相同的二進制代碼+相同的類加載器。

6.2 雙親委派模型

除了頂層加載器之外,所有的加載器都有父加載器。這里類加載器之間的父子關系一般不會以繼承關系來實現,而是都使用組合關系來復用父加載器的代碼。

工作過程:
   如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳遞到頂層的啟動類加載器中,
   只有當父類加載器反饋自己無法完成這個請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載
  好處:
   Java類隨着它的類加載器一起具備了一種帶有優先級的層次關系。例如類Object,它放在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類
   判斷兩個類是否相同是通過classloader.class這種方式進行的,所以哪怕是同一個class文件如果被兩個classloader加載,那么他們也是不同的類。

參考:

《深入理解java虛擬機》 周志明著

http://wangwengcn.iteye.com/blog/1618337


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM