JVM效能優化系列-(3) 虛擬機器執行子系統
3. 虛擬機器執行子系統
3.1 Java跨平臺的基礎
Java剛誕生的宣傳口號:一次編寫,到處執行(Write Once, Run Anywhere),其中位元組碼是構成平臺無關的基石,也是語言無關性的基礎。
Java虛擬機器不和包括Java在內的任何語言繫結,它只與Class檔案這種特定的二進位制檔案格式所關聯,這使得任何語言的都可以使用特定的編譯器將其原始碼編譯成Class檔案,從而在虛擬機器上執行。
3.2 Class類的檔案結構
任何一個Class檔案都對應著唯一一個類或介面的定義資訊,但反過來說,Class檔案實際上它並不一定以磁碟檔案的形式存在。
Class檔案是一組以8位位元組為基礎單位的二進位制流。
各個資料專案嚴格按照順序緊湊地排列在Class檔案之中,中間沒有新增任何分隔符,這使得整個Class檔案中儲存的內容幾乎全部是程式執行的必要資料,沒有空隙存在。
Class檔案格式採用一種類似於C語言結構體的偽結構來儲存資料,這種偽結構中只有兩種資料型別:無符號數和表。
無符號數屬於基本的資料型別,以u1、u2、u4、u8來分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字串值。
表是由多個無符號數或者其他表作為資料項構成的複合資料型別,所有表都習慣性地以“_info”結尾。表用於描述有層次關係的複合結構的資料,整個Class檔案本質上就是一張表。
整個class類的檔案結構如下表所示:
佔用大小 | 欄位描述 | 數量 |
---|---|---|
佔用大小 | 欄位描述 | 數量 |
u4 | magic:魔數,用於標識檔案型別,對於java來說是0xCAFEBABE | 1 |
u2 | minor_version:次版本號 | 1 |
u2 | major_version:主版本號 | 1 |
u2 | constant_pool_count:常量池大小,從1開始而不是0。當這個值為0時,表示後面沒有常量 | 1 |
cp_info | constant_pool:#常量池 | constant_pool_count-1 |
u2 | access_flags:訪問標誌,標識這個class是類還是介面、public、abstract、final等 | 1 |
u2 | this_class:類索引 #類索引查詢全限定名的過程 | 1 |
u2 | super_class:父類索引 | 1 |
u2 | interfaces_count:介面計數器 | 1 |
u2 | interfaces:介面索引集合 | interfaces_count |
u2 | fields_count:欄位的數量 | 1 |
field_info | fields:#欄位表 | fields_count |
u2 | methods_count:方法數量 | 1 |
method_info | methods:#方法表 | methods_count |
u2 | attributes_count:屬性數量 | 1 |
attribute_info | attrbutes:#屬性表 | attributes_count |
可以使用javap -verbose輸出class檔案的位元組碼內容。
下面按順序對這些欄位進行介紹。
魔數與Class檔案的版本
每個Class檔案的頭4個位元組稱為魔數(Magic Number),它的唯一作用是確定這個檔案是否為一個能被虛擬機器接受的Class檔案。使用魔數而不是副檔名來進行識別主要是基於安全方面的考慮,因為副檔名可以隨意地改動。檔案格式的制定者可以自由地選擇魔數值,只要這個魔數值還沒有被廣泛採用過同時又不會引起混淆即可。
緊接著魔數的4個位元組儲存的是Class檔案的版本號:第5和第6個位元組是次版本號(MinorVersion),第7和第8個位元組是主版本號(Major Version)。Java的版本號是從45開始的,JDK 1.1之後的每個JDK大版本釋出主版本號向上加1高版本的JDK能向下相容以前版本的Class檔案,但不能執行以後版本的Class檔案,即使檔案格式並未發生任何變化,虛擬機器也必須拒絕執行超過其版本號的Class檔案。
常量池
常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2型別的資料,代表常量池容量計數值(constant_pool_count)。與Java中語言習慣不一樣的是,這個容量計數是從1而不是0開始的。
常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References)。
- 字面量:比較接近於Java語言層面的常量概念,如文字字串、宣告為final的常量值等。
- 符號引用:則屬於編譯原理方面的概念,包括了下面三類常量:
類和介面的全限定名(Fully Qualified Name)、欄位的名稱和描述符(Descriptor)、方法的名稱和描述符。
訪問標誌
用於識別一些類或者介面層次的訪問資訊,包括:
- 這個Class是類還是介面;
- 是否定義為public型別;
- 是否定義為abstract型別;
- 如果是類的話,是否被宣告為final等
類索引、父類索引與介面索引集合
這三項資料來確定這個類的繼承關係。
- 類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名。
- 由於Java語言不允許多重繼承,所以父類索引只有一個,除了java.lang.Object之外,所有的Java類都有父類,因此除了java.lang.Object外,所有Java類的父類索引都不為0。
- 介面索引集合就用來描述這個類實現了哪些介面,這些被實現的介面將按implements語句(如果這個類本身是一個介面,則應當是extends語句)後的介面順序從左到右排列在介面索引集合中
欄位表集合
描述介面或者類中宣告的變數。欄位(field)包括類級變數以及例項級變數。
而欄位叫什麼名字、欄位被定義為什麼資料型別,這些都是無法固定的,只能引用常量池中的常量來描述。
欄位表集合中不會列出從超類或者父介面中繼承而來的欄位,但有可能列出原本Java程式碼之中不存在的欄位,譬如在內部類中為了保持對外部類的訪問性,會自動新增指向外部類例項的欄位。
方法表集合
描述了方法的定義,但是方法裡的Java程式碼,經過編譯器編譯成位元組碼指令後,存放在屬性表集合中的方法屬性表集合中一個名為“Code”的屬性裡面。
與欄位表集合相類似的,如果父類方法在子類中沒有被重寫(Override),方法表集合中就不會出現來自父類的方法資訊。但同樣的,有可能會出現由編譯器自動新增的方法,最典型的便是類構造器“<clinit>”方法和例項構造器“<init>”
屬性表集合
儲存Class檔案、欄位表、方法表都自己的屬性表集合,以用於描述某些場景專有的資訊。如方法的程式碼就儲存在Code屬性表中。
3.3 位元組碼指令
Java虛擬機器的指令由一個位元組長度的、代表著某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其後的零至多個代表此操作所需引數(稱為運算元,Operands)而構成。
由於限制了Java虛擬機器操作碼的長度為一個位元組(即0~255),這意味著指令集的操作碼總數不可能超過256條。
大多數的指令都包含了其操作所對應的資料型別資訊。例如:
iload指令用於從區域性變量表中載入int型的資料到運算元棧中,而fload指令載入的則是float型別的資料。
- l代表long
- s代表short
- b代表byte
- c代表char
- f代表float
- d代表double
- a代表reference
大部分的指令都沒有支援整數型別byte、char和short,甚至沒有任何指令支援boolean型別。不是每種資料型別和每一種操作都有對應的指令,有一些單獨的指令可以在必要的時候用在將一些不支援的型別轉換為可被支援的型別。大多數對於boolean、byte、short和char型別資料的操作,實際上都是使用相應的int型別作為運算型別。
載入和儲存命令
載入和儲存指令用於將資料在幀棧中的區域性變量表和運算元棧之間來回傳遞。
- 將一個區域性變數載入到操作棧:iload、iload_<\n>、lload、lload_<\n>、fload、fload_<\n>、dload、dload_<\n>、aload、aload_<\n>
- 將一個數值從運算元棧儲存到區域性變量表:istore、istore_<\n>、lstore、lstore_<\n>、fstore、fstore_<\n>、dstore、dstore_<\n>、astore、astore_<\n>
- 將一個引數載入到運算元棧:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<\i>、lconst_
- 擴充區域性變量表的訪問索引的指令:wide
上面帶尖括號的指令實際上是代表的一組指令,如iload_0、iload_1、iload_2和iload_3。這些指令把運算元隱含在名稱內,不需要進行取運算元的動作。
運算指令
運算或算術指令用於對兩個運算元棧上的值進行某種特定運算,並把結果重新存入到操作棧頂,可分為整型資料和浮點型資料指令。byte、short、char和boolean型別的算術指令使用int型別的指令代替。
- 加法指令:iadd、ladd、fadd、dadd
- 減法指令:isub、lsub、fsub、dsub
- 乘法指令:imul、lmul、fmul、dmul
- 除法指令:idiv、ldiv、fdiv、ddiv
- 求餘指令:irem、lrem、frem、drem
- 取反指令:ineg、lneg、fneg、dneg
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
- 或指令:ior、lor
- 與指令:iand、land
- 異或指令:ixor、lxor
- 區域性變數自增指令:iinc
- 比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
型別轉換指令
可以將兩種不同的數值型別進行相互轉換,
- Java虛擬機器直接支援以下數值型別的寬化型別轉換(即小範圍型別向大範圍型別的安全轉換):
- int型別到long、float或者double型別。
- long型別到float、double型別。
- float型別到double型別。
- 處理窄化型別轉換(Narrowing Numeric Conversions)時,必須顯式地使用轉換指令來完成,這些轉換指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
物件建立與訪問指令
- 建立類例項的指令:new
- 建立陣列的指令:newarray、anewarray、multianewarray
- 訪問類欄位和例項欄位的例項:getfield、putfield、getstatic、putstatic
- 把一個數組元素載入到運算元棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
- 將一個運算元棧的值儲存到陣列元素中的指令:bastore、castore、sastore、iastore、fasotre、dastore、aastore
- 取陣列長度的指令:arraylength
- 檢查類例項型別的指令:instanceof、checkcast
運算元棧管理指令
- 將運算元棧的棧頂一個或兩個元素出棧:pop、pop2
- 複製棧頂一個或兩個數值並將複製值或雙份的複製值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
- 將棧最頂端的兩個數值互換:swap
控制轉移指令
控制轉移指令可以讓Java虛擬機器有條件或無條件地從指定的位置指令而不是控制轉移指令的下一條指令繼續執行程式,從概念模型上理解,可以認為控制轉移指令就是在有條件或無條件地修改PC暫存器的值。控制轉移指令如下。
- 條件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
- 複合條件分支:tableswitch、lookupswitch。
- 無條件分支:goto、goto_w、jsr、jsr_w、ret。
方法呼叫指令
- invokevirtual指令用於呼叫物件的例項方法,根據物件的實際型別進行分派(虛方法分派),這也是Java語言中最常見的方法分派方式。
- invokeinterface指令用於呼叫介面方法,它會在執行時搜尋一個實現了這個介面方法的物件,找出適合的方法進行呼叫。
- invokespecial指令用於呼叫一些需要特殊處理的例項方法,包括例項初始化方法、私有方法和父類方法。
- invokestatic指令用於呼叫類方法(static方法)。
- invokedynamic指令用於在執行時動態解析出呼叫點限定符所引用的方法,並執行該方法,前面4條呼叫指令的分派邏輯都固化在Java虛擬機器內部,而invokedynamic指令的分派邏輯是由使用者所設定的引導方法決定的。
- 方法呼叫指令與資料型別無關。
方法返回指令
是根據返回值的型別區分的,包括ireturn(當返回值是boolean、byte、char、short和int型別時使用)、lreturn、freturn、dreturn和areturn,另外還有一條return指令供宣告為void的方法、例項初始化方法以及類和介面的類初始化方法使用。
異常處理指令
在java程式中,顯式丟擲異常的操作都由athrow指令來實現。而在java虛擬機器中,處理異常不是由位元組碼指令來實現的,而是採用異常表來完成的
同步指令
java虛擬機器可以支援方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支援的。方法級的同步是隱式的,利用方法表結構中的ACC_SYNCHRONIZED訪問標誌得知。指令序列的同步是由monitorenter和monitorexit兩條指令支援。
3.4 類載入機制
典型面試題:類載入過程?什麼是雙親委派?
這是一個非常典型的面試題,標準回答如下:
一般來說,我們把 Java 的類載入過程分為三個主要步驟:載入、連結、初始化。
1. 載入(Loading)
此階段中Java 將位元組碼資料從不同的資料來源讀取到 JVM 中,並對映為 JVM 認可的資料結構(Class 物件),這裡的資料來源可能是各種各樣的形態,如 jar 檔案、class 檔案,甚至是網路資料來源等;如果輸入資料不是 ClassFile 的結構,則會丟擲 ClassFormatError。 載入階段是使用者參與的階段,我們可以自定義類載入器,去實現自己的類載入過程。
2. 連結(Linking)
這是核心的步驟,簡單說是把原始的類定義資訊平滑地轉化入 JVM 執行的過程中。這裡可進一步細分為三個步驟:
- 驗證(Verification),這是虛擬機器安全的重要保障,JVM 需要核驗位元組資訊是符合 Java 虛擬機器規範的,否則就被認為是 VerifyError,這樣就防止了惡意資訊或者不合規的資訊危害 JVM 的執行,驗證階段有可能觸發更多 class 的載入。
- 準備(Preparation),建立類或介面中的靜態變數,並初始化靜態變數的初始值。但這裡的“初始化”和下面的顯式初始化階段是有區別的,側重點在於分配所需要的記憶體空間,不會去執行更進一步的 JVM 指令。
- 解析(Resolution),在這一步會將常量池中的符號引用(symbolic reference)替換為直接引用。在Java 虛擬機器規範中,詳細介紹了類、介面、方法和欄位等各個方面的解析。
3. 初始化(initialization)
這一步真正去執行類初始化的程式碼邏輯,包括靜態欄位賦值的動作,以及執行類定義中的靜態初始化塊內的邏輯,編譯器在編譯階段就會把這部分邏輯整理好,父型別的初始化邏輯優先於當前型別的邏輯。
雙親委派模型:
簡單說就是當類載入器(Class-Loader)試圖載入某個型別的時候,除非父載入器找不到相應型別,否則儘量將這個任務代理給當前載入器的父載入器去做。使用委派模型的目的是避免重複載入 Java 型別。
概述
類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入(Loading)、驗證(Verificatio)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。其中驗證、準備、解析3個部分統稱為連線(Linking)。
於初始化階段,虛擬機器規範則是嚴格規定了有且只有5種情況必須立即對類進行“初始化”(而載入、驗證、準備自然需要在此之前開始):
- 遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java程式碼場景是:使用new關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。
- 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
- 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類。
- 當使用JDK 1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化。
關於靜態變數的初始化,必須要注意以下三種情況下是不會觸發類的初始化的:
- 只有直接定義這個欄位的類才會被初始化,因此通過其子類來引用父類中定義的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化。
- 通過陣列定義來引用類,不會觸發此類的初始化。
- 常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
下面是測試程式:
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("Subclass init!");
}
}
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD_STRING = "hello world";
}
以下是對三種情況的測試程式:
public class NotInitialization {
public static void main(String[] args) {
// 1. 只有直接定義這個欄位的類才會被初始化,因此通過其子類來引用父類中定義的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化。
// Result: SuperClass init! 123
System.out.println(SubClass.value);
// 2. 通過陣列定義來引用類,不會觸發此類的初始化
SuperClass[] superClasses = new SubClass[10];
// 3. 常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
// Result: hello world
System.out.println(ConstClass.HELLOWORLD_STRING);
}
}
載入
在載入階段,虛擬機器需要完成下列3件事:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
- 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口
驗證
驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。驗證階段大致上會完成下面4個階段的檢驗動作:
- 檔案格式驗證:第一階段要驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理。主要目的是保證輸入的位元組流能正確解析並存儲於方法區內,格式上符合描述一個java型別資訊的要求。這個階段的驗證是基於二進位制位元組流進行的,只有通過了這個階段的驗證後,位元組流才會儲存到方法區中,所以後面的3個驗證階段全部是基於方法區的儲存結構進行的,不會再直接操作位元組流
- 元資料驗證:第二階段是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合java語言規範的要求。主要目的是對元資料資訊進行語義校驗,保證不存在不符合java語言規範的元資料資訊
- 位元組碼驗證:第三階段是整個驗證過程中最複雜的一個階段,主要目的是通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。如果一個類方法體的位元組碼沒有通過位元組碼驗證,那肯定是有問題的;但如果一個方法體通過了位元組碼驗證,也不能說明其一定就是安全的
- 符號引用驗證:最後一個階段的驗證發生在虛擬機器符號引用轉化為直接引用的時候,這個轉化動作將在連線的解析階段中發生,可以看做是對類自身以外的資訊進行匹配性校驗。目的是確保解析動作能正常執行
準備階段
是正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。
這個階段中有兩個容易產生混淆的概念需要強調一下,首先,這時候進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在Java堆中。
其次,這裡所說的初始值“通常情況”下是資料型別的零值,假設一個類變數的定義為:
public static int value=123;
那變數value在準備階段過後的初始值為0而不是123,因為這時候尚未開始執行任何Java方法,而把value賦值為123的putstatic指令是程式被編譯後,存放於類構造器<clinit>()方法之中,所以把value賦值為123的動作將在初始化階段才會執行。
表7-1列出了Java中所有基本資料型別的零值:
假設上面類變數value的定義變為:public static final int value=123;
編譯時Javac將會為value生成ConstantValue屬性,在準備階段虛擬機器就會根據ConstantValue的設定將value賦值為123。
解析階段
是虛擬機器將常量池內的符號引用替換為直接引用的過程。
符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用是能無歧義地定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標不一定已經載入到記憶體中。各種虛擬機器實現的記憶體佈局可以各不相同,但是它們能接受地符號引用必須是一致的,因為符號引用地字面量形式明確定義在java虛擬機器規範地Class檔案格式中。
直接引用(Direct References):直接引用可以是直接指向目標的指標、相對偏移量或是一個能直接定位到目標的控制代碼。直接引用是和虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在記憶體中存在。
初始化
類初始化是類載入過程的最後一步,在這個階段才真正開始執行類中的位元組碼。初始化階段是執行類構造器<clinit>()
方法的過程。
<clinit>()
方法與類的建構函式(<init>()
方法)不同,它不需要顯式呼叫父類構造器,虛擬機器會保證在子類的<clinit>()
方法執行之前,父類的<clinit>()
方法已經執行完畢。- 由於父類的
<clinit>()
方法先執行,因此父類中定義的靜態語句塊要先於子類執行。 <clinit>()
方法對於類或介面來說不是必需的,如果一個類中沒有靜態語句塊,也沒有對變數賦值操作,那麼編譯器可以不為這個類生成<clinit>()
方法。- 介面中不能使用靜態語句塊,但仍然由變數初始化的賦值操作,因此介面與類一樣都會生成
<clinit>()
方法,但與類不同的是,執行介面的<clinit>()
方法不需要先執行父介面的<clinit>()
方法,只有當父介面中定義的變數使用時,父接口才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>()
方法。 - 虛擬機器會保證一個類的
3.5 類載入器
類與類載入器
類載入器雖然只用於實現類的載入動作,但在java程式中起到的作用卻遠不止類載入階段。
對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在java虛擬機器中的唯一性,每個類載入器,都擁有一個獨立的類名稱空間。當一個Class檔案被不同的類載入器載入時,載入生成的兩個類必定不相等(equals()、isAssignableFrom()、isInstance()、instanceof關鍵字的結果為false)。
雙親委派機制
從java虛擬機器的角度來看,只存在兩種不同的類載入器:一種是啟動類載入器(Bootstrap ClassLoader),這個類載入器使用c++實現,是虛擬機器的一部分;另一種是所有其他的類載入器,這些類載入器都由java實現,獨立於虛擬機器外部,並且全部繼承自抽象類java.lang.ClassLoader。java提供的類載入器主要分以下三種:
- 啟動類載入器(Bootstrap ClassLoader):這個類負責將存放在
- 擴充套件類載入器(Extension ClassLoader):這個載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入
- 應用程式類載入器(Application ClassLoader):這個類載入器由sun.misc.Launcher$AppClassLoader實現。由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱為系統類載入器,負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器。
雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。
使用雙親委派模型來組織類載入器之間的關係,有一個顯而易見的好處就是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類載入器自行去載入的話,如果使用者自己編寫了一個稱為java.lang.Object的類,並放在程式的ClassPath中,那系統中將會出現多個不同的Object類,Java型別體系中最基礎的行為也就無法保證,應用程式也將會變得一片混亂。
自定義類載入器
首先看一下實現雙親委派模型的程式碼,邏輯就是先檢查類是否已經被載入,如果沒有則呼叫父載入器的loadClass()方法,如果父載入器為空則預設使用啟動類載入器作為父載入器。如果父類載入失敗,丟擲ClassNotFoundException異常後,再呼叫自己的findClass()方法進行載入。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先從快取查詢該class物件,找到就不用重新載入
Class<?> c = findLoadedClass(name);
if (c == null) {
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
// 如果都沒有找到,則通過自定義實現的findClass去查詢並載入
c = findClass(name);
}
}
if (resolve) {//是否需要在載入時進行解析
resolveClass(c);
}
return c;
}
}
在實現自己的類載入器時,通常有兩種做法,一種是重寫loadClass方法,另一種是重寫findClass方法。其實這兩種方法本質上差不多,畢竟loadClass也會呼叫findClass,但是最好不要直接修改loadClass的內部邏輯,以免破壞雙親委派的邏輯。推薦的做法是隻在findClass裡重寫自定義類的載入方法。
下面例子實現了檔案系統類載入器,
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
Class.forName和ClassLoader.loadClass
Class.forName是Class類的方法public static Class<?> forName(String className) throws ClassNotFoundException
ClassLoader.loadClass是ClassLoader類的方法public Class<?> loadClass(String name) throws ClassNotFoundException
Class.forName和ClassLoader.loadClass都可以用來進行型別載入,而在Java進行型別載入的時刻,一般會有多個ClassLoader可以使用,並可以使用多種方式進行型別載入。
class A {
public void m() {
A.class.getClassLoader().loadClass(“B”);
}
}
在A.class.getClassLoader().loadClass(“B”)
;程式碼執行B的載入過程時,一般會有三個概念上的ClassLoader提供使用。
- CurrentClassLoader,稱之為當前類載入器,簡稱CCL,在程式碼中對應的就是型別A的類載入器。
- SpecificClassLoader,稱之為指定類載入器,簡稱SCL,在程式碼中對應的是 A.class.getClassLoader(),如果使用任意的ClassLoader進行載入,這個ClassLoader都可以稱之為SCL。
- ThreadContextClassLoader,稱之為執行緒上下文類載入器,簡稱TCCL,每個執行緒都會擁有一個ClassLoader引用,而且可以通過Thread.currentThread().setContextClassLoader(ClassLoader classLoader)進行切換。
SCL和TCCL可以理解為在程式碼中使用ClassLoader的引用進行類載入,而CCL卻無法獲取到其引用,雖然在程式碼中CCL == A.class.getClassLoader() == SCL。CCL的載入過程是由JVM執行時來控制的,是無法通過Java程式設計來更改的。
雙親委派機制的破壞
為什麼需要破壞雙親委派?
因為在某些情況下父類載入器需要委託子類載入器去載入class檔案。受到載入範圍的限制,父類載入器無法載入到需要的檔案,以Driver介面為例,由於Driver介面定義在jdk當中的,而其實現由各個資料庫的服務商來提供,比如mysql的就寫了MySQL Connector,那麼問題就來了,DriverManager(也由jdk提供)要載入各個實現了Driver介面的實現類,然後進行管理,但是DriverManager由啟動類載入器載入,只能記載JAVA_HOME的lib下檔案,而其實現是由服務商提供的,由系統類載入器載入,這個時候就需要啟動類載入器來委託子類來載入Driver實現,從而破壞了雙親委派,這裡僅僅是舉了破壞雙親委派的其中一個情況。
Tomcat的類載入機制是違反了雙親委託原則的,對於一些未載入的非基礎類(Object,String等),各個web應用自己的類載入器(WebAppClassLoader)會優先載入,載入不到時再交給commonClassLoader走雙親委託。
如何破壞?
- JDK1.2之前,classLoader類中沒有定義findClass,當用戶繼承該類並且修改loadClass的實現時,就可能破壞雙親委派。
- 執行緒上下文類載入器(Thread Context ClassLoader)。這個類載入器可以通過java.lang.Thread類的setContextClassLoader方法進行設定。如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過多的話,那這個類載入器預設即使應用程式類載入器。有了執行緒上下文載入器,JNDI服務使用這個執行緒上下文載入器去載入所需要的SPI程式碼,也就是父類載入器請求子類載入器去完成類載入的動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類載入器,實際上已經違背了雙親委派模型的一般性原則。Java中所有涉及SPI的載入動作基本勝都採用這種方式。例如JNDI,JDBC,JCE,JAXB,JBI等。
- 為了實現熱插拔,熱部署,模組化,意思是新增一個功能或減去一個功能不用重啟,只需要把這模組連同類載入器一起換掉就實現了程式碼的熱替換。OSGI實現模組化熱部署的關鍵則是它自定義類載入器機制的實現。
Tomcat類載入器
Tomcat的類載入機制是違反了雙親委託原則的,對於一些未載入的非基礎類(Object,String等),各個web應用自己的類載入器(WebAppClassLoader)會優先載入,載入不到時再交給commonClassLoader走雙親委託。
Tomcat是個web容器, 那麼它要解決什麼問題:
- 一個web容器可能需要部署兩個應用程式,不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個伺服器只有一份,因此要保證每個應用程式的類庫都是獨立的,保證相互隔離。
- 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果伺服器有10個應用程式,那麼要有10份相同的類庫載入進虛擬機器,這是扯淡的。
- web容器也有自己依賴的類庫,不能於應用程式的類庫混淆。基於安全考慮,應該讓容器的類庫和程式的類庫隔離開來。
- web容器要支援jsp的修改,我們知道,jsp 檔案最終也是要編譯成class檔案才能在虛擬機器中執行,但程式執行後修改jsp已經是司空見慣的事情,否則要你何用? 所以,web容器需要支援 jsp 修改後不用重啟。
Tomcat 如果使用預設的類載入機制行不行 ?
答案是不行的。為什麼?
第一個問題,如果使用預設的類載入器機制,那麼是無法載入兩個相同類庫的不同版本的,預設的累加器是不管你是什麼版本的,只在乎你的全限定類名,並且只有一份。
第二個問題,預設的類載入器是能夠實現的,因為他的職責就是保證唯一性。
第三個問題和第一個問題一樣。
第四個問題,我們要怎麼實現jsp檔案的熱修改(樓主起的名字),jsp 檔案其實也就是class檔案,那麼如果修改了,但類名還是一樣,類載入器會直接取方法區中已經存在的,修改後的jsp是不會重新載入的。那麼怎麼辦呢?我們可以直接解除安裝掉這jsp檔案的類載入器,所以你應該想到了,每個jsp檔案對應一個唯一的類載入器,當一個jsp檔案修改了,就直接解除安裝這個jsp類載入器。重新建立類載入器,重新載入jsp檔案。
Tomcat 如何實現自己獨特的類載入機制?
前面3個類載入和預設的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader則是Tomcat自己定義的類載入器,它們分別載入/common/*
、/server/*
、/shared/*
(在tomcat 6之後已經合併到根目錄下的lib目錄下)和/WebApp/WEB-INF/*
中的Java類庫。其中WebApp類載入器和Jsp類載入器通常會存在多個例項,每一個Web應用程式對應一個WebApp類載入器,每一個JSP檔案對應一個Jsp類載入器。
- commonLoader:Tomcat最基本的類載入器,載入路徑中的class可以被Tomcat容器本身以及各個Webapp訪問;
- catalinaLoader:Tomcat容器私有的類載入器,載入路徑中的class對於Webapp不可見;
- sharedLoader:各個Webapp共享的類載入器,載入路徑中的class對於所有Webapp可見,但是對於Tomcat容器不可見;
- WebappClassLoader:各個Webapp私有的類載入器,載入路徑中的class只對當前Webapp可見;
從圖中的委派關係中可以看出:
CommonClassLoader能載入的類都可以被Catalina ClassLoader和SharedClassLoader使用,從而實現了公有類庫的共用,而CatalinaClassLoader和Shared ClassLoader自己能載入的類則與對方相互隔離。
WebAppClassLoader可以使用SharedClassLoader載入到的類,但各個WebAppClassLoader例項之間相互隔離。
而JasperLoader的載入範圍僅僅是這個JSP檔案所編譯出來的那一個.Class檔案,它出現的目的就是為了被丟棄:當Web容器檢測到JSP檔案被修改時,會替換掉目前的JasperLoader的例項,並通過再建立一個新的Jsp類載入器來實現JSP檔案的HotSwap功能。
下圖展示了Tomcat的類載入流程:
當tomcat啟動時,會建立幾種類載入器:
1. Bootstrap 引導類載入器
載入JVM啟動所需的類,以及標準擴充套件類(位於jre/lib/ext下)
2. System 系統類載入器
載入tomcat啟動的類,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位於CATALINA_HOME/bin下。
3. Common 通用類載入器
載入tomcat使用以及應用通用的一些類,位於CATALINA_HOME/lib下,比如servlet-api.jar
4. webapp 應用類載入器
每個應用在部署後,都會建立一個唯一的類載入器。該類載入器會載入位於 WEB-INF/lib下的jar檔案中的class 和 WEB-INF/classes下的class檔案。
典型面試題
tomcat 違背了java 推薦的雙親委派模型了嗎?
違背了,雙親委派模型要求除了頂層的啟動類載入器之外,其餘的類載入器都應當由自己的父類載入器載入。tomcat 不是這樣實現,tomcat 為了實現隔離性,沒有遵守這個約定,每個webappClassLoader載入自己的目錄下的class檔案,不會傳遞給父類載入器。
如果tomcat 的 Common ClassLoader 想載入 WebApp ClassLoader 中的類,該怎麼辦?
可以使用執行緒上下文類載入器實現,使用執行緒上下文載入器,可以讓父類載入器請求子類載入器去完成類載入的動作。
參考:
- https://www.cnblogs.com/aspirant/p/8991830.html
- https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html
- https://www.jianshu.com/p/f745187e4010
- https://www.jianshu.com/p/aedee0e14319
3.6 執行時棧幀結構
棧幀(Stack Frame)是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,它是虛擬機器執行時資料區中的虛擬機器棧的棧元素。典型棧幀結構:
下面對各個部分進行仔細介紹:
區域性變量表
區域性變量表(Local Variable Table)是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數。區域性變量表的容量以變數槽(Variable Slot)為最小單位,虛擬機器規範中並沒有明確指定一個Slot應占用的記憶體空間大小,只是規定每個Slot都應該能存放一個boolean、byte、char、short、int、float、reference或returnAddress型別的資料,這樣可以遮蔽32位跟64位虛擬機器在記憶體空間上的差異。
虛擬機器通過索引定位的方式使用區域性變量表,索引值的範圍從0到最大Slot數量,索引n對應第n個Slot。區域性變量表中第0位索引的Slot預設是用於傳遞方法所屬物件例項的引用,即this。
為了儘可能的節省棧幀空間,區域性變量表中的Slot是可以重用的,同時這也影響了垃圾收集行為。即對已使用完畢的變數,區域性變量表仍持有該物件的引用,導致物件無法被GC回收,佔用大量記憶體。這也是“不使用的物件應手動賦值為null”這條推薦編碼規則的原因。不過從執行角度使用賦null值的操作來優化記憶體回收是建立在對位元組碼執行引擎概念模型的理解之上,程式碼在經過編譯器優化後才是虛擬機器真正需要執行的程式碼,這時賦null值會被消除掉,因此更優雅的解決辦法是以恰當的變數作用域來控制變量回收時間。
運算元棧
運算元棧(Operand Stack)也常稱操作棧,它是一個後入先出(Last In First Out,LIFO)棧。方法在執行過程中,通過各種位元組碼指令對棧進行操作,出棧/入棧。java虛擬機器的解釋執行引擎稱為“基於棧的執行引擎”,其中所指的“棧”就是運算元棧。
動態連線
每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用時為了執行方法呼叫過程中的動態連線(Dynamic Linking)。
方法返回地址
當一個方法開始執行後,只有兩種方式可以退出這個方法:
執行引擎遇到任意一個方法返回的位元組碼指令,這個時候可能會有返回值傳遞給上層的方法呼叫者(呼叫當前方法的方法稱為呼叫者),這種退出方式稱為正常完成出口(Normal Method Invocation Completion)。
方法執行過程中遇到了異常,並且這個異常沒有在方法體內得到處理,無論是java虛擬機器內部產生的異常,還是程式碼使用athrow位元組碼指令產生的異常,只要在本方法的異常表中沒有搜尋到匹配的異常處理器,就會導致方法退出,這種退出方式稱為異常完成出口(Abrupt Method Invocation Completion),這時不會給它的上層呼叫者產生任何返回值。
方法退出的過程實際上就等同於把當前棧幀出棧,因此退出時可能執行的操作有:
- 恢復上層方法的區域性變量表和運算元棧。
- 把返回值(如果有)壓入呼叫者棧幀的運算元棧。
- 調整PC計數器的值以指向方法呼叫指令後面的一條指定等。
附加資訊
虛擬機器規範允許具體的虛擬機器實現增加一些規範裡沒有描述的資訊到棧幀中,稱之為棧幀資訊。
3.7 方法呼叫
方法呼叫並不等同於方法執行,方法呼叫階段的唯一任務就是確定被呼叫方法的版本,即呼叫哪一個方法,暫時還不涉及方法內部的具體執行過程,就是類載入過程中的類方法解析。
解析
解析就是將Class的常量池中的符號引用轉化為直接引用(記憶體佈局中的入口地址)。
在java虛擬機器中提供了5條方法呼叫位元組碼指令:
- invokestatic:呼叫靜態方法
System.exit(1);
==>編譯
iconst_1 ;將1放入棧內
;執行System.exit()
invokestatic java/lang/System/exit(I)V
- invokespecial:呼叫例項構造器
//<init>方法
new StringBuffer()
==>編譯
new java/lang/StringBuffer ;建立一個StringBuffer物件
dup ;將物件彈出棧頂
;執行<init>()來初始化物件
invokespecial java/lang/StringBuffer/<init>()V
//父類方法
super.equals(x);
==>編譯
aload_0 ;將this入棧
aload_1 ;將第一個引數入棧
;執行Object的equals()方法
invokespecial java/lang/Object/equals(Ljava/lang/Object;)Z
//私有方法
與父類方法類似
- invokevirtual:呼叫所有的虛方法。
X x;
...
x.equals("abc");
==>編譯
aload_1 ;將x入棧
ldc "abc" ;將“abc”入棧
;執行equals()方法
invokevirtual X/equals(Ljava/lang/Object;)Z
- invokeinterface:呼叫介面方法,會在執行時再確定一個實現此介面的物件。
List x;
...
x.toString();
==>編譯
aload_1 ;將x入棧
;執行toString()方法
invokeinterface java/util/List/toString()Z
- invokedynamic:先在執行時動態解析出呼叫點限定符所引用的方法,然後再執行該方法。
在編譯階段就可以確定唯一呼叫版本的方法有:靜態方法(類名)、私有方法、例項構造器(、父類方法(super)、final方法。其它統稱為虛方法,在編譯階段無法確定呼叫版本,需要在執行期通過分派將符號引用轉變為直接引用。
3.8 分派
靜態分派
指在執行時對類內相同名稱的方法根據描述符來確定執行版本的分派,多見於方法的過載。
下面的例子中,輸出結果均為hello guy
。
“Human”稱為變數的靜態型別(Static Type),或者叫做的外觀型別(Apparent Type),後面的“Man”則稱為變數的實際型別(Actual Type),靜態型別和實際型別在程式中都可以發生一些變化,區別是靜態型別的變化僅僅在使用時發生,變數本身的靜態型別不會被改變,並且最終的靜態型別是在編譯期可知的;而實際型別變化的結果在執行期才可確定,編譯器在編譯程式的時候並不知道一個物件的實際型別是什麼。
程式碼中定義了兩個靜態型別相同但實際型別不同的變數,但虛擬機器(準確地說是編譯器)在過載時是通過引數的靜態型別而不是實際型別作為判定依據的。並且靜態型別是編譯期可知的,因此,在編譯階段,Javac編譯器會根據引數的靜態型別決定使用哪個過載版本,所以選擇了sayHello(Human)作為呼叫目標。所有依賴靜態型別來定位方法執行版本的分派動作稱為靜態分派。靜態分派的典型應用是方法過載。靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機器來執行的。
動態分派
指對於相同方法簽名的方法根據實際執行物件來確定執行版本的分派。編譯器是根據引用型別來判斷方法是否可執行,真正執行的是實際物件方法。多見於類多型的實現。
動態分配的實現,最常用的手段就是為類在方法區中建立一個虛方法表。虛方法表中存放著各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那子類的虛方法表裡面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實現入口。如果子類中重寫了這個方法,子類方法表中的地址將會替換為指向子類實現版本的入口地址。PPT圖中,Son重寫了來自Father的全部方法,因此Son的方法表沒有指向Father型別資料的箭頭。但是Son和Father都沒有重寫來自Object的方法,所以它們的方法表中所有從Object繼承來的方法都指向了Object的資料型別。
3.9 基於棧的位元組碼解釋執行引擎
Java語言經常被人們定位為“解釋執行”語言,在Java初生的JDK1.0時代,這種定義還比較準確的,但當主流的虛擬機器中都包含了即時編譯後,Class檔案中的程式碼到底會被解釋執行還是編譯執行,就成了只有虛擬機器自己才能準確判斷的事情。再後來,Java也發展出來了直接生成原生代碼的編譯器[如何GCJ(GNU Compiler for the Java)],而C/C++也出現了通過直譯器執行的版本(如CINT),這時候再籠統的說“解釋執行”,對於整個Java語言來說就成了幾乎沒有任何意義的概念。
基於棧的指令集與基於暫存器的指令集
基於棧的指令集:指令流中的指令大部分都是零地址指令,它們依賴運算元棧進行工作。
基於暫存器的指令集:最典型的就是X86的地址指令集,通俗一點,就是現在我們主流的PC機中直接支援的指令集架構,這些指令集依賴暫存器工作。
舉個簡單例子,分別使用這兩種指令計算1+1的結果,基於棧的指令集會是這個樣子:
iconst_1
iconst_1
iadd
istore_0
兩條iconst_1指令連續把兩個常量1壓入棧後,iadd指令把棧頂的兩個值出棧、相加,然後將結果放回棧頂,最後istore_0把棧頂的值放到區域性變量表中的第0個Slot中。
如果基於暫存器的指令集,那程式可能會是這個樣子:
mov eax, 1
add eax, 1
mov指令把EAX暫存器的值設定為1,然後add指令再把這個值加1,將結果就儲存在EAX暫存器裡面。
基於棧的指令集:
優點:可移植、程式碼相對更緊湊、編譯器實現更簡單等
缺點:執行速度慢、完成相同功能的指令數量更多、棧位於記憶體中基於暫存器的指令集:
優點:速度快
缺點:與硬體結合緊密
參考連結:
- https://www.jianshu.com/p/62241c6cd5ef
本文由『後端精進之路』原創,首發於部落格 http://teckee.github.io/ , 轉載請註明出處
搜尋『後端精進之路』關注公眾號,立刻獲取最新文章和價值2000元的BATJ精品面試課程。
相關推薦
JVM效能優化系列-(3) 虛擬機器執行子系統
3. 虛擬機器執行子系統 3.1 Java跨平臺的基礎 Java剛誕生的宣傳口號:一次編寫,到處執行(Write Once, Run Anywhere),其中位元組碼是構成平臺無關的基石,也是語言無關性的基礎。 Java虛擬機器不和包括Java在內的任何語言繫結,它只與Class檔案這種特定的二進位制檔案
優化提高VMware虛擬機器執行速度的技巧(詳細圖文教程)
vmware虛擬機器如何設定不當的話會造成執行速度慢,並影響主機執行,甚至會出現宕機。以下是提高vmware虛擬機器執行速度的幾個技巧,1 修改preference中的選項(全域性設定)a 進入設定介面的方法如下圖所示:這步也是關鍵步驟之一,否則
JVM深度學習系列之虛擬機器引數彙總(五)
以下資訊摘自:https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html 不管是YGC還是Full GC,GC過程中都會對導致程式執行中中斷,正確的選擇不同的GC策略,調整JVM、GC的引數,可以極大的減少由於GC
JVM效能優化系列-(2) 垃圾收集器與記憶體分配策略
目前已經更新完《Java併發程式設計》和《Docker教程》,歡迎關注【後端精進之路】,輕鬆閱讀全部文章。 Java併發程式設計: Java併發程式設計系列-(1) 併發程式設計基礎 Java併發程式設計系列-(2) 執行緒的併發工具類 Java併發程式設計系列-(3) 原子操作與CAS Java
JVM效能優化系列-(4) 編寫高效Java程式
4. 編寫高效Java程式 4.1 面向物件 構造器引數太多怎麼辦? 正常情況下,如果構造器引數過多,可能會考慮重寫多個不同引數的建構函式,如下面的例子所示: public class FoodNormal { //required private final String foodNa
JVM效能優化系列-(5) 早期編譯優化
5. 早期編譯優化 早起編譯優化主要指編譯期進行的優化。 java的編譯期可能指的以下三種: 前端編譯器:將.java檔案變成.class檔案,例如Sun的Javac、Eclipse JDT中的增量式編譯器(ECJ) JIT編譯器(Just In Time Compiler):將位元組碼變成機器碼,例如
Java虛擬機器 虛擬機器執行子系統
程式碼編譯的結構從本地機器碼轉變為位元組碼,是儲存格式發展的一小步,卻是程式語言發展的一大步。 主要內容 類檔案結構 虛擬機器類載入機制 虛擬機器位元組碼執行引擎 類檔案結構 無關性基石 各種不同的虛擬機器都可以載入和執行一種平臺無關的位元組碼,從而實現“一次編寫,到處執行”。
虛擬機器執行子系統
類檔案結構 java語言中的各種變數,關鍵字和運算子號的語義最終都是由多條位元組碼命令組合而成的,因此位元組碼命令所能提供的語義描述能力肯定會比java語言本身更加強大。 Class類檔案的結構 Class檔案是一組以8位位元組為基礎單位的二進位制流,各個
深入理解Java虛擬機器——JVM效能優化
一、效能監控 當開發或執行一個Java應用的時候,對JVM的效能進行監控是很重要的。配置JVM不是一次配置就萬事大吉的,特別是你要應對的是Java伺服器應用的情況。你必須持續的檢查堆記憶體和非堆記憶體的分配和使用情況,執行緒數的建立情況和記憶體中載入的類的資料
Java虛擬機器(JVM原始碼):JDK10對Java虛擬機器執行時資料區的劃分(詳細圖解)
Java虛擬機器執行時資料區 為什麼要研究這個,因為JDK都已經發布到10了,必須要更新自己對Java虛擬機器新的認識。 一、執行時資料區的劃分 1.1 官方劃分 關於JDK10對執行時資料區的劃分,在官方文件說的非常清楚。 學習技術,一定要學會看第一手資料。 Ja
提交訂單效能優化系列之006-普通的Thread多執行緒改為Java8的parallelStream併發流
概括總結 Java8的parallelStream併發流能達到跟多執行緒類似的效果,但它也不是什麼善茬,為了得到跟上一版本的多執行緒類似的效果,一改再改,雖然最後改出來了,但是還是存在理解不了的地方。
MongoDB效能優化系列:檢視當前正在執行的操作
檢視MongoDB正在執行哪些操作 rs1:PRIMARY> db.currentOp() { "inprog" : [ { "desc" : "conn111911",
Java效能優化系列二(jvm記憶體調優)
首先需要注意的是在對JVM記憶體調優的時候不能只看作業系統級別Java程序所佔用的記憶體,這個數值不能準確的反應堆記憶體的真實佔用情況,因為GC過後這個值是不會變化的,因此記憶體調優的時候要更多地使用JDK提供的記憶體檢視工具,比如JConsole和Java VisualVM(jvisua
JVM虛擬機器執行機制
什麼是JVM? 虛擬機器是物理機器的軟體實現。Java是用在VM上執行的WORA(Write Once Run Anywhere)概念而開發的。編譯器將Java檔案編譯為Java .class檔案,然後將.class檔案輸入到JVM中,JVM會載入並執行類檔案。 JVM基本概
ernel 3.10核心原始碼分析--KVM相關--虛擬機器執行
1、基本原理 KVM虛擬機器通過字元裝置/dev/kvm的ioctl介面建立和執行,相關原理見之前的文章說明。 虛擬機器的執行通過/dev/kvm裝置ioctl VCPU介面的KVM_RUN指令實現,在VM和VCPU建立好並完成初始化後,就可以排程該虛擬機器運行了,通
深入理解jvm(四):虛擬機器位元組碼執行引擎
執行時棧幀 每一個方法從呼叫開始到執行完成都對應著一張棧幀的進棧和出棧。棧幀中儲存著區域性變量表,運算元表,動態連結和方法返回地址。位於虛擬機器最頂層的稱為當前方法棧。 區域性變量表 儲存當前方法的區域性變數和引數,區域性變量表的容量以變數槽slo
3——虛擬機器體驗之VirtualBox篇——效能強大的經典架構
前兩篇體驗了QEMU和經過KVM加速的QEMU,並體驗了第三方虛擬機器管理工具virt-manager,讓我們見識了開源社群的強大和開源虛擬機器軟體的高質量和高效能。這一篇,我來剖析一下VirtualBox。VirtualBox號稱是目前開源界最強大的虛擬機器產品,在Linux平臺上,基本上都被大家選擇
【JVM從小白學成大佬】2.Java虛擬機器執行時資料區
目錄 1.執行時資料區介紹 2.堆(Heap) 是否可能有兩個物件共用一段記憶體的事故? 3.方法區(Method Area) 4.程式計數器(Program Counter
虛擬機器執行UEFI
qemu虛擬機器執行UEFI 安裝qemu虛擬機器 dnf install qemu 編譯 Ovfm build -p OvmfPkg/OvmfPkgX64.dsc 生成ovfm韌體 qemu啟動並使用該韌體: qemu-syst
關於Class物件、類載入機制、虛擬機器執行時記憶體佈局的全面解析和推測
簡介: 本文是對Java的類載入機制,Class物件,反射原理等相關概念的理解、驗證和Java虛擬機器中記憶體佈局的一些推測。本文重點講述瞭如何理解Class物件以及Class物件的作用。 歡迎探討,如有錯誤敬請指正 如需轉載,請註明出處 http://www.cnblogs.com/nul