字節碼文件的格式應詳細介紹了之後,如何執行字節碼文件,字節碼文件加載到jvm中後是怎麼樣的一個過程
1 類加載的過程
類從被加載到虛擬機內存中開始到卸載出內存為止,整個生命週期包括: 加載,驗證,準備,解析,初始化,使用和卸載。其中驗證準備解析統稱為連接模塊。
類裝載器把一個類裝入JVM中,要經過以下步驟:
1 加載 查找和導入class文件
2 校驗 檢查導入的字節流對應的數據結構格式是否正確。
3 準備 為類變量分配內存並設置類變量初始值
4 解析 虛擬機將常量池內的服務引用替換為直接引用
5 初始化 執行源碼中類變量的初始化和其他的資源處理
1.1 加載
加載是類加載過程的第一個階段,在加載階段,虛擬機需要完成以下三件事情:
1. 通過一個類的全限定名來獲取其定義的二進制字節流。
2. 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
3. 在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些數據的訪問入口。
通過上面的描述可以知道是通過一個限定名來查找並讀取對應的二進制流,限定名查找的話,滿足的方式就可以有很多
1 可以通過從zip文件中讀取。例如現在的jar,war包等都是通過這種方式來演變出來的
2 通過從網絡中加載
3 運行時計算生成。也就是現在的動態代理技術。
4 通過其他文件生成。例如現在jsp
下面是一個簡單的例子,通過指定加載路徑和類的包路徑來加載具體的類,並執行其中已知的一個方法:
public static void main(String[] args) throws Exception{ String ROOT_PATH = "/data/study/java-jvm/"; //通過路徑地址來加載對應的類,並執行其中的方法 ClassLoader loader = new ClassLoader() { @Override protected Class> findClass(String className) throws ClassNotFoundException { byte[] clazzData = loadClassData(className); if (clazzData == null) { throw new ClassNotFoundException(); } else { return defineClass(className, clazzData, 0, clazzData.length); } } private byte[] loadClassData(String className){ String path = ROOT_PATH + className.replace('.', '/').concat(".class"); ByteArrayOutputStream bos = null; InputStream is = null; try { bos = new ByteArrayOutputStream(); is = new FileInputStream(path); IOUtils.copy(is,bos); return bos.toByteArray(); } catch (Exception e) { e.printStackTrace(); return null; }finally { try{ bos.close(); is.close(); }catch (Exception e){ e.printStackTrace(); } } } }; //加載本地編譯好的一個類 Class clazz = loader.loadClass("Test"); clazz.getMethod("hello").invoke(clazz.newInstance()); }
類加載過程中第一步加載可以通過classloader來進行之定義處理,在讀取遠端,本地等讀取加載字節碼文件。通過上面的例子可以看到可以針對讀取過後的字節數組進行自定義的處理,可以追加內容或者是替換內容在進行編譯處理。
1.2 校驗
加載過程存在的問題
支持用戶自定義加載指定的字節碼文件。這裡可以通過自己構造的字節碼文件流訪問數組邊界意外的數據,將對象指向其並未實現的類型等惡意篡改字節碼文件
鑑於此,必須要對輸入的字節碼文件進行校驗。校驗的範圍有:
1 文件格式的校驗
2 元數據的校驗
3 字節碼的校驗
4 符號引用的校驗
文件格式校驗,需要校驗是否服務字節碼文件的規範。並且能被當前虛擬機執行。驗證的部分邏輯有為:
1 是否以魔數0xCAFEBABE
2 主次版本是否在當前虛擬機可處理範圍內。具體的可以看到上節內容
3 常量池中敞亮是否有不被支持的常量類型
4 指向常量的各種索引是否有不存在的或者是不符合類型的常量
5 字符編碼是否有不符合UTF8編碼的數據
6 是否有被刪除或者是附加的其他信息
元數據的校驗,對字節碼描述的信息進行語義分析,保證其描述的信息符合Java語言的規範。驗證的部分邏輯為:
1 類是否有父類
2 類是否繼承了不允許被繼承的類
3 如果類不是抽象類,是否實現了其父類或者接口中要求實現的方法
4 類中字段,防範是否與父類產生矛盾(如果覆蓋了父類的final字段,是否有不符合規範的方法重載)
字節碼驗證,字節碼驗證的過程是最複雜的一個階段。主要是通過數據流和控制流分析確定語義是否合法,是否符合邏輯。
1 保證任意時刻操作數棧的數據類型與指令代碼序列都能配置和工作
2 保證跳轉指令不會跳轉到異常位置
3 保證方法中的轉換是有效的
符號引用驗證
符號引用中通過字符串描述的全限定名是否能找到對應的類。
在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。
符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當前類訪問
1.3 準備
準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行分配。
1 只進行靜態變量的初始化,而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在Java堆中。
2 所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值
1.4 解析
把類中的符號引用轉換為直接引用。
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。
直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
1.5 初始化
到了初始化階段,才真正開始執行類中定義的Java程序代碼。 初始化,為類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。
1 初始化過程是對類變量進行初始化和順序執行靜態代碼塊
2 初始化過程中,先執行父類的初始化。因此父類的靜態塊會先於子類的靜態庫初始化
下面看一個例子:
public class SuperClass { static { System.out.println("Super Class"); } public static int value = 10; } public class SubClass extends SuperClass { static { System.out.println("Sub Class"); } } public class Application { public static void main(String[] args) throws Exception{ System.out.println(SubClass.value); } }
上面這段代碼,執行後結果如下:
可以看到通過子類來引用父類的某個靜態變量,子類沒有被初始化。
在jvm初始化過程中,虛擬機有嚴格的規定有且只有5中情況必須立即對類進行初始化(初始化的前提是加載,驗證,準備都需要提前開始):
1 遇到new、getstatic、pustatic或者是invokestatic這4條字節碼指令時,如果類沒有進行出斯卡哈,則需要先觸發其初始化
2 使用反射進行調用時,如果類沒有初始化,則先觸發其初始化
3 當初始化一個類是,如果發現其父類沒有初始化,則先觸發其父類的初始化
4 當虛擬機啟東市,用戶指定一個要執行的主類(包含main方法),虛擬機會先初始化這個主類
5 當使用動態語言支持時,如果一個MethodHandle實例最後解析的結果為REF_getstaticc,REF_putstatic,REF_invokestatic時,並且這個放入發沒有進行初始化,則需要先觸發其初始化
上面的這個例子,我們可以通過javap來編譯一下,觀察一下初始化方法是否有被調用:
從截圖中可以看到,第一個常量對應的是父類的init方法,整個字節碼中並沒有標記子類的實例化方法。
被動引用的常見場景
1 通過子類應用父類的靜態字段,不會導致子類初始化
2 通過數組定義來引用,不會觸發此類的初始化
3 常量在編譯階段會存入調用類的常量池中,本質沒有直接引用到定義的常量類中,因此不會觸發定義的常量類初始化
其中第一個已經有例子說明了,詳細看第一個代碼塊。數組的初始化,不會觸發此類的初始化,詳細看例子:
public class Application { public static void main(String[] args) throws Exception{ System.out.println(new SubClass[]{}); } }
執行後結果為:[Lcom.study.code.classloading.SubClass;@681a9515 並沒有打印SubClass中定義的靜態塊。編譯一下字節碼文件看一下內容:
截圖只是截取了當前方法部門。可以看到數組的部分是:
4: anewarray #3 // class com/study/code/classloading/SubClass
主動引用中指出的四種字節碼指令為new、getstatic、putstatic和invokestatic。anewarray並不在,因此不進行初始化。同時通過執行響應可以看到,這個類名並不是規範的。主要是數組是JVM進行了包裝。通過虛擬機自動生成直接繼承java.lang.Object,通過anewarray來觸發。同時自動生成一個包裝類,將相應的操作都封裝起來有效的保障了數組訪問的安全性
常量的被動引用問題:
public class Application { public static void main(String[] args) throws Exception{ System.out.println(SuperClass.value); } }
輸出結果為: 10。並沒有對應引用類的初始化標記。觀察一下字節碼文件:
可以看到截圖中方法並未引用到SupClass中的敞亮。而是通過
3: ldc #4 // String Hello World
直接指向當前類常量池中。
2 類加載器
類加載器主要用於將類加載到內存中。對於任意一個類,都需要有加載他的類加載器和其本身一起確認在Java虛擬機中的唯一性。
類加載器的介紹:
1 類加載器是一個抽象類,可以通過繼承實現自定義加載器
2 可以通過自定義加載器來實現字節碼文件的自定義處理
3 類加載器主要負責類的加載,將字節碼文件裝載到JVM中
JVM中有的類加載器
啟動類加載器(BootStrap ClassLoader)
這個類加載器負責將存放在\lib目錄中的或者是被-Xbootclasspath參數所指定的路徑中符合規範的類庫加載到虛擬機內存中。啟動類加載器無法被Java程序直接引用,如果想將加載請求交由啟動類加載器,只需要在自定義加載器時使用null代替即可
擴展類加載器(Extension ClassLoader)
這個加載器有sun.misc.Launcher$ExtClassLoader實現,它負責加載\lib\ext目錄彙總的,或者是被java.ext.dirs系統變量所指定的路徑中的所有類庫。Java程序中可以直接使用擴展類加載器
應用程序類加載器
這個類加載器由sun.misc.Launcher$AppClassLoader實現。這個類加載器可以通過getSystemClassLoader()方法獲取。它負責加載用戶路徑上的所指定的類庫
2.1 雙親委派模型
上面介紹了三種類加載器,那具體的類加載器之間是什麼關係。詳細如下:
圖中的類加載層次關係被稱為類加載雙親委派模型。具體的工作過程是:
如果一個類加載器收到了類加載請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。每一個層次的加載器都是如此。因此所有的加載請求最終都會傳送到引導類加載器中去完成(引導類加載器又稱作啟動類加載器)。只有當父類加載器反饋自己無法完成這個加載請求,子類加載器才會去嘗試自己去加載
如果沒有雙親委派模型,容易造成混亂:
1 同一個類經過不通的類加載器來加載後,不能保證類相等。也就是通過equals()方法等無法驗證一致性。
2 一致性得不到保證後,JDK一些底層類庫可以通過不通的類加載器來加載,Java的一些最基礎的行為就無法得到保證
類加載源碼示例:
/** * Loads the class with the specified binary name. The * default implementation of this method searches for classes in the * following order: * ** *
* *- * *
Invoke {@link #findLoadedClass(String)} to check if the class * has already been loaded.
- * *
Invoke the {@link #loadClass(String) loadClass} method * on the parent class loader. If the parent is null the class * loader built-in to the virtual machine is used, instead.
- * *
Invoke the {@link #findClass(String)} method to find the * class.
If the class was found using the above steps, and the * resolve flag is true, this method will then invoke the {@link * #resolveClass(Class)} method on the resulting Class object. * *
Subclasses of ClassLoader are encouraged to override {@link * #findClass(String)}, rather than this method.
* *Unless overridden, this method synchronizes on the result of * {@link #getClassLoadingLock getClassLoadingLock} method * during the entire class loading process. * * @param name * The binary name of the class * * @param resolve * If true then resolve the class * * @return The resulting Class object * * @throws ClassNotFoundException * If the class could not be found */ protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 檢查類是否已經被加載過 Class> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //使用父類來加載當前這個類 c = parent.loadClass(name, false); } else { //如果父類為NULL,則使用啟動類加載器加載當前類 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果拋出異常,則開始使用本身去加載 } if (c == null) { // 如果一直未加載成功,則使用自己加載當前類 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; } }
通過上面可以很明確的看到類加載順序
1 直接使用父類加載器加載
2 如果父類加載器為NULL,使用啟動類加載器加載
3 如果還是未加載到,則使用自己進行加載
自定義類加載器:
ClassLoader loader = new ClassLoader() { @Override protected Class> findClass(String className) throws ClassNotFoundException { byte[] clazzData = loadClassData(className); if (clazzData == null) { throw new ClassNotFoundException(); } else { return defineClass(className, clazzData, 0, clazzData.length); } } }
一般情況下實現自定義類加載器,繼承實現findClass方法即可。上面的代碼為文檔第一個代碼中部分截取