程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> JAVA類的加載實現教程

JAVA類的加載實現教程

編輯:關於JAVA
 

一、概述

本文主要講述虛擬機如何把 Class文件加載到內存的過程。校驗、轉換解析和初始化,最終形成可被虛擬機使用的Java類型,這就是虛擬機的類加載機制。類型的加載、連接和初始化都是在程序運行期間完成,這樣做的優劣勢,如下:

  • 優勢:提高Java程序的靈活性,Java動態擴展的語言特性就是依賴運行期動態加載和動態連接。當面向接口的應用程序,可以等到運行時指定實現類;可以通過類加載器,讓程序運行時加載一個二進制流作為程序一部分。
  • 劣勢:增加類加載的性能開銷。

二、 類加載的生命周期

類的生命周期是指把Class字節碼從文件中加載到內存,直到卸載內存整個過程,分為7個步驟。

jvm_class_loading_2

圖中用紅色圈起來的3個過程分別為驗證、准備、解析,它們合稱為鏈接(Linking)過程。另外圖中紫色的5項是嚴格按照執行。而藍色的解析階段不一定要在初始化之前, 也可以在初始化之後再解析,這種情況稱為動態綁定或晚期綁定。

1. 加載

虛擬機在加載階段,主要工作如下:

  1. 通過類的全限定名獲取該類的二進制字節流;
  2. 將字節流所代表的靜態存儲結構 轉化為 方法區的運行時數據結構;
  3. 生成代表該類的Class對象並存放方法區,作為方法區該類的各種數據的訪問入口。

對於上述字節流,可能來源:

  • 壓縮包,例如jar/war等格式;
  • 網絡,典型場景applet;
  • 運行時計算生成,例如動態代理技術,在java.lang.reflect.Proxy中,利用ProxyGenerator.generateProxyClass來為特定接口生成形如“*$Proxy”的代理類的二進制字節流;
  • 數據庫,例如中間件服務器(SAP Netweaver)。

注:對於數組類,不通過類加載器創建,而是由虛擬機直接創建的。另外加載階段尚未完成,連接階段可能已經開始。

2. 驗證

驗證是連接階段(Linking)的第一步,目的是為了確保Class文件的字節流符合虛擬機規范,不會危害虛擬機自身安全。比如:訪問數組越界問題,將對象轉型為未實現的類型,跳轉到不存在的代碼區等情緒編譯器都會拒絕編譯,也就是無法生成Class文件,既然如此,為什麼還要驗證呢?原因是Class文件不一定都是由java源碼編譯而成,可以是任何途徑,所以驗證還是很有必要的,盡可能保證系統能承受住惡意代碼攻擊。

驗證主要工作分4階段:

  1. 文件格式驗證:驗證是否符合Class文件格式規范;
  2. 元數據驗證:驗證是否符合Java語言規范;
  3. 字節碼驗證:驗證數據流和控制流分析;
  4. 符號引用驗證:驗證符號引用轉化為直接引用。

2.1 文件格式驗證

驗證點有比如是否魔數0xCAFEBABE開頭;主、次版本號是否范圍之內;常量池中常量tag標示是否正確等等,只有通過全部的驗證,才能把字節流存儲到內存的方法區。

2.2 元數據驗證

經過文件格式驗證,字節流已加載到方法區,這個階段工作是對方法區的字節碼進行語義分析,保證符合Java語言規范。 驗證點比如:

  • 該類是否有父類(除Object之外,所有類都應該有父類)
  • 該類是否繼承不允許繼承的類(final類)
  • 非抽象類,是否都實現其父類的抽象方法或接口中的方法
  • 類的字段、方法是否與父類矛盾(例如覆蓋父類的final字段,或重載不符合規則)
  • … 除上面列舉外,還有很多。經過元數據驗證,能確保元數據都是符合規范。

2.3 字節碼驗證

比如操作數棧的數據類型和指令代碼序列配合,跳轉指令不會跳到方法體之外等。HotSpot虛擬機提供 -XX:-UseSplitVerifier選項來關閉這項優化。

2.4 符號引用驗證

校驗點:

  • 符號引用中通過字符串描述的全限定名是否能找到對應的類;
  • 在指定類是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段;
  • 符號引用中的類、字段、方法的訪問權限檢查。
  • …等等

對於虛擬機的類加載機制來說,驗證階段非常重要的,但不是一定必要的。如果所運行的全部代碼(包含自己編寫以及第三方包的代碼)都已經被反復使用和驗證過,那麼可以考慮使用 -Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

3. 准備

主要工作:static變量分配內存,並設置類變量的初始值的階段。

(1). 類變量:賦予零值

數據類型的零值表,如下:

類型 int long float double short byte char boolean reference 零值 0 0L 0.0f 0.0d (short)0 (byte)0 ‘\u0000’ false null

例如:

public static int value = 10;

在准備階段,會為變量value在方法區分配內存並初始化零值,即value=0,而非10。 因為對於value的賦值10,是由putstatic指令完成。該指令是在java程序被編譯後,存放在類構造器<clinit>方法之中。所以 value=10的操作是在類初始化的時候才發生,故類變量在准備期value=0

(2). 常量:賦予真實值

例如:

public static final int value = 10;

對於常量,准備階段會把類字段的字段屬性表中的ConstantValue屬性所指定的值(此處是10),賦給常量(value),故常量在准備期間value =10;

(3). 實例變量:不賦任何值

該階段僅對類變量進行內存分配,而對於實例變量(或者稱呼為成員變量)並不會分配內存,也就更不用提賦值的事。實例變量的初始化,是隨著對象實例化時在Java堆上分配內存而進行的。

4. 解析

主要工作:虛擬機將常量池內的符號引用替換為直接引用的階段。

先解析下符號引用和直接引用的概念

  • 符號引用(Symbolic Reference):以一組符號來描述引用目標,符號可以是任意形式的字面量。只能要准確定位到目標即可。符號引用與虛擬機的內存布局無關,引用的目標也不一定存在內存。這樣兼容性強,各種虛擬機只需要能接受符號引用即可。
  • 直接引用(Direct Reference):直接引用就是指向目標的指針、相對偏移量或者能簡介定位到目標的句柄。直接引用和虛擬機內存布局息息相關。直接引用的目標必然存在與內存中。

同一個符號引用 在不同的虛擬機中解析出來的直接引用地址一般都是不相同的;同一個符號引用,在同一個虛擬機下,多次解析時,會對第一次解析結果進行緩存(常量池記錄直接引用,並標記已解析狀態),從而避免多次解析。

特殊情形,對於invokedynamic指令,不會進行緩存過程,每次使用前都會進行解析。

5. 初始化

主工作:主要是執行類構造器方法clinit。(class init的簡稱)

類初始化階段是類加載的最後一個階段。在初始化之前的過程中,用戶可控的地方只有通過自定義類加載器參與,其余都是虛擬機主導和控制。

到了初始化,才開始真正的執行類中定義的Java程序代碼。

(1). 類的構造方法

類構造方法是由編譯器自動收集源文件中的類變量賦值操作靜態語句塊合並而成的。收集順序是由語句在源文件的順序所決定。故靜態語句塊只能訪問定義之前的靜態變量;對於定義之後的變量可以賦值,但不能訪問。

  1. clinit方法與類的實例構造方法init不同,clinit方法不需要顯式調用父類構造器,虛擬機會保證子類的clinit方法執行之前,父類的clinit方法已經執行完畢。故第一個被執行的clinit方法的類肯定是java.lang.Object;
  2. clinit方法不是必需的,對於沒有靜態塊和類變量賦值操作,編譯器不會生成clinit方法。
  3. 父類靜態語句和靜態變量賦值優先於子類.
  4. interface中不能有靜態語句塊,但仍可以有變量初始化的賦值操作,也可以生成clinit方法。但接口和類的不同是,執行接口的clinit方法不需要先執行父接口的clinit方法。只有當父接口中定義的變量使用時,父接口才會初始化。
  5. 虛擬機保證類構造方法可以多線程正確執行,會加鎖、同步的操作。 一個線程執行類構造方法,其他線程阻塞等待,當類構造方法有耗時操作,會造成多進程的阻塞,往往比較隱蔽。

(2). 類初始化時機

虛擬機規范中嚴格規定有且只有5種情況下,當類沒有初始化時必須立即對類進行初始化:

  1. 遇到newgetstaticputstaticinvokeStatic這4條字節碼指令時。常見場景:
    1. 使用new關鍵字實例化對象時,觸發new
    2. 讀取類變量時,觸發getstatic;(final常量除外)
    3. 設置類變量時,觸發putstatic
    4. 調用類的靜態方法時,觸發invokeStatic
  2. 虛擬機啟動時,需指定一個要執行的主類(含有main()的類),虛擬機會先初始化該類;
  3. 初始化一個類時,當其父類沒有初始化,則需要先觸發其父類的初始化;
  4. 使用java.lang.reflect包中的方法對類進行反射調用時;
  5. java.lang.invoke.MethodHandle實例最後的解析結果為REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,並且該句柄所對應的類沒有進行過初始化;

上面講到final常量不能觸發類初始化,是由於在編譯時已把數據放入常量池的靜態字段,當讀取類的static final字段時,並不需要初始化類,而是從常量池中去獲取相應的數據。

上述的5種場景的行為都是對類的一個主動引用過程。除此之外,還有被動引用並不會除非類的初始化過程。 另外

 
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved