Java
說到 Java道媚,大家第一時間想到的類似于下面的程序語句:
package com.zqlite.jvm;
public class HelloJVM {
private static final int k = 100;
public static void main(String ...args){
int a = 0,b=3;
System.out.println(a+b+k);
}
}
但這僅僅只屬于 Java 技術(shù)體系中的 Java 程序設計語言扁掸。Java 的技術(shù)體系從傳統(tǒng)意義上來看有以下幾個:
- Java 程序設計語言
- 各硬件平臺上的 Java 虛擬機
- Class 文件格式
- Java API 類庫
- 其他商業(yè)機構(gòu)或開源社區(qū)的第三方 Java 類庫
其中 1、4最域、5 大家應該比較熟悉谴分,在編程中都能直接接觸。2镀脂、3 對于大家來說可能陌生了些牺蹄,但不夸張的講,Java 能有如今的活力薄翅,其功臣正是 2 和 3沙兰。所以接下來的內(nèi)容將圍繞這兩點展開,讓你從更深的層次了解你所熟悉的 Java匿刮。
JVM
JVM(Java Virtual Machine)就是上面所說的 Java 虛擬機僧凰,其作用是加載與運行 Class 文件。有句話大家一定不陌生熟丸,“一次編寫训措,到處運行”。簡單解釋一下光羞,因為在 Class 文件和硬件平臺中間隔著一個 JVM绩鸣,由 JVM 負責加載和執(zhí)行 Class 文件,這樣平臺的差異性就留給了 JVM 去考慮纱兑,而不是 Class 文件呀闻。也正因如此,Class 文件的格式可以真正做到平臺無關(guān)性潜慎。
JVM 中的 Java 內(nèi)存區(qū)域劃分
對于從事 C捡多、C++ 的開發(fā)人員來說,在內(nèi)存管理領(lǐng)域他們是擁有最高權(quán)力的“皇帝”铐炫,但又是從事最底層基礎工作的“勞動人民”垒手。他們即擁有與內(nèi)存直接打交道的權(quán)利,又要負責維護每一個對象的生命周期倒信。
對于 Java 開發(fā)人員來說就輕松多了科贬,在 JVM 自動內(nèi)存管理機制的幫助下,不再需要維護每一個對象的生命周期鳖悠,內(nèi)存控制的權(quán)利由開發(fā)人員轉(zhuǎn)移到了 JVM 榜掌。
因為 JVM 需要管理 Java 運行期間的各種對象的生命周期优妙,所以它在執(zhí)行 Java 程序的時候會把它所管轄的內(nèi)存分成若干個不同的數(shù)據(jù)區(qū)域,具體如下圖:
對以上幾個區(qū)域做下簡單的介紹:
- 方法區(qū):線程共享區(qū)域憎账,用于存儲被虛擬機加載的類信息套硼、常量、靜態(tài)變量等數(shù)據(jù)鼠哥。
- 堆:線程共享區(qū)域熟菲,存放類實例以及數(shù)組。我們平時聽到的 GC(垃圾回收)就是在堆上進行的朴恳。
- 虛擬機棧:線程私有區(qū)域,用于描述 Java 方法的執(zhí)行模型允蚣。每個方法在執(zhí)行同時會創(chuàng)建一個棧幀用于存儲局部變量表于颖、操作數(shù)、動態(tài)鏈接嚷兔、方法出口等信息森渐。
- 本地方法棧:線程私有區(qū)域,與虛擬機棧作用類似冒晰,區(qū)別在于本地方法棧執(zhí)行的是 Native 方法同衣。
- 程序計數(shù)器:線程私有區(qū)域,用于指向當前線程下一條需要執(zhí)行的字節(jié)碼指令壶运。
除了以上幾個主要區(qū)域外耐齐,另外還要介紹一個區(qū)域:運行時常量池。它是方法區(qū)的一部分蒋情,用于存放編譯期生成的字面量和符號引用埠况。字面量好理解,比如:
String str = "a";
上面的 a 就是字面量棵癣。而符號引用則是一些字符串辕翰,用于給虛擬機定位類或類方法等。
面試的時候有時會遇到這樣的問題狈谊,Java 中每次聲明并初始化一個 String 對象都會在堆上面分配內(nèi)存嗎喜命?這里的答案當然是否,因為有可能新聲明的對象已經(jīng)在常量池中存在了河劝,這時候 JVM 會將新對象的引用指向常量池中對應的值而不是在堆重新分配壁榕。
對象在 JVM 中的創(chuàng)建過程
在 Java 中創(chuàng)建對象很簡單,一個簡單的 new 關(guān)鍵字就可以丧裁。但在虛擬機中护桦,對象的創(chuàng)建過程是怎樣的呢?我們來一起了解一下煎娇。
在 JVM 中二庵,創(chuàng)建對象的字節(jié)碼指令是 new贪染,當 JVM 執(zhí)行引擎遇到 new 指令后會進行如下五步操作:
一、檢查這個指令的參數(shù)能否在常量池中定位到一個類的符號引用催享,并且檢查這個符號引用代表的類是否已經(jīng)加載杭隙、解析和初始化。
下面我們看一下:
package com.zqlite.jvm;
public class HelloJVM {
public static void main(String ...args){
Object obj = new Object();
}
}
然后用 javap 對這段代碼生成的 class 文件進行反匯編處理因妙,結(jié)果如下:
Classfile /Users/scott/workspace/jvm/out/production/jvm/com/zqlite/jvm/HelloJVM.class
Last modified 2017-11-29; size 446 bytes
MD5 checksum bcc0b2d9129748ef0caa00e95cf95a47
Compiled from "HelloJVM.java"
public class com.zqlite.jvm.HelloJVM
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#19 // java/lang/Object."<init>":()V
#2 = Class #20 // java/lang/Object
#3 = Class #21 // com/zqlite/jvm/HelloJVM
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcom/zqlite/jvm/HelloJVM;
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 args
#14 = Utf8 [Ljava/lang/String;
#15 = Utf8 obj
#16 = Utf8 Ljava/lang/Object;
#17 = Utf8 SourceFile
#18 = Utf8 HelloJVM.java
#19 = NameAndType #4:#5 // "<init>":()V
#20 = Utf8 java/lang/Object
#21 = Utf8 com/zqlite/jvm/HelloJVM
{
public com.zqlite.jvm.HelloJVM();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zqlite/jvm/HelloJVM;
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: return
LineNumberTable:
line 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
8 1 1 obj Ljava/lang/Object;
}
SourceFile: "HelloJVM.java"
暫時無需理解上面的所有內(nèi)容痰憎,我們只關(guān)注 new 字節(jié)碼部分。
找到
0: new #2
可以看到 new 指令碼的參數(shù)是 #2攀涵,接著通過參數(shù) #2 在常量池(Constant pool)中找到 #2 對應的內(nèi)容:
#2 = Class #20 // java/lang/Object
這里說明了 #2 對應的是一個類铣耘,類符號引用需要到 #20 中找,在看 #20 :
#20 = Utf8 java/lang/Object
這里的 #20 是一個字符串類型以故,其內(nèi)容是 ava/lang/Object蜗细,表示 Object 類的符號引用即全限定名。接著 JVM 就會去尋找并加載怒详、解析和初始化這個類炉媒。
二、當在第一步確定了目標類以后昆烁,JVM 就會為這個類的新生對象在堆上面分配內(nèi)存吊骤。對象所需要的內(nèi)存大小在第一步的類加載后已經(jīng)確定了,所以為對象分配內(nèi)存也就等同于把一塊確定大小的內(nèi)存從 Java 堆中劃分出來静尼。
三白粉、分配完內(nèi)存后,JVM 需要將分配到的內(nèi)存空間都初始化為零值(不包括對象頭)茅郎。
這里需要注意蜗元,由于 JVM 會對對象的內(nèi)存空間做初始化操作,所以在類中類似的定義一個 int i 系冗,其默認值就是整型的零值 0 奕扣。Java 類變量也正因如此允許不賦初始值。但在類方法中掌敬,如果這樣定義一個 i惯豆,在接下來的代碼中如果使用 i 的話,會有報錯提示 i 沒有初始化值奔害,
四楷兽、當初始化完以后,JVM 就要對對象做必要的處理华临,比如這個對象是哪個類的實例芯杀、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的 GC 分代年齡表的信息揭厚。這些信息都會放在對象頭中却特。
五、上面的工作都完成之后筛圆,從 JVM 的角度來看裂明,一個新的對象已經(jīng)產(chǎn)生了,但從 Java 程序的角度來看太援,對象創(chuàng)建才剛開始闽晦。<init> 方法還沒有執(zhí)行,所有的字段還都為零提岔。所以一般在 new 指令碼后都會跟 invokespecial 指令來調(diào)用 <init> 方法仙蛉。此處的 <init> 即我們通常說的構(gòu)造方法。
對象的內(nèi)存布局
在上小節(jié)介紹了對象在 JVM 中的創(chuàng)建過程碱蒙,這節(jié)簡單介紹下對象的內(nèi)存布局捅儒。
對象的內(nèi)存布局可以分三個區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充振亮。
其中對象頭包括兩部分信息,第一部分用于存儲對象自身的運行時數(shù)據(jù)鞭莽,如哈希碼坊秸、GC 分代年齡、鎖狀態(tài)標志澎怒、線程持有的鎖等褒搔。另外一部分則是類型指針,用于指向它的類元數(shù)據(jù)喷面。JVM 可以通過這個指針來確定對象是屬于哪個類的星瘾。另外需要注意的是如果對象是數(shù)組,那么對象頭中還需要記錄數(shù)組的長度惧辈。
實例數(shù)據(jù)部分是對象真正存儲的有效信息琳状,也是在程序代碼中所定義的各種類型字段的內(nèi)容,包括從父類繼承下來的和自己定義的盒齿。
最后一部分并不是必須的念逞,它僅僅起到了占位符的作用。有些虛擬機要求對象的起始地址必須是 8 字節(jié)的整數(shù)倍边翁,換句話說就是對象大小必須是 8 字節(jié)的整數(shù)倍翎承。所以當不足整數(shù)倍的時候,就需要進行對齊填充處理符匾。
下面這張圖簡單表示了對象在內(nèi)存中的布局:
Class 文件
計算機只認識 0 和 1 叨咖,這是常識。所以我們編寫的程序都需要經(jīng)過編譯器翻譯成有 0 和 1 構(gòu)成的二進制格式才能被計算機執(zhí)行。但在近十幾年內(nèi)甸各,虛擬機以及大量建立在虛擬機上的程序語言如雨后春筍般出現(xiàn)并蓬勃發(fā)展垛贤,由此二進制本地機器碼已不再是唯一的選擇,越來越多的程序語言選擇了與操作系統(tǒng)和機器指令集無關(guān)的痴晦、平臺中立的格式作為程序編譯后的存儲格式南吮。我們這里的 Class 文件就是其中一種。
我們通常都把 Java 和 JVM 有意識無意識的聯(lián)系在一起誊酌,但 JVM 其實并不關(guān)心 Java部凑。什么意思呢?JVM 面向的是 Class 格式的文件碧浊,它并不關(guān)系 Class 的來源是何種語言涂邀。如 Clojure、Groovy箱锐、Jruby比勉、Jython、Scala 等都可以被編譯成 Class 文件驹止,從而在 JVM 上運行浩聋。
Class 內(nèi)部結(jié)構(gòu)
Class 文件是一組以 8 位字節(jié)為基礎單位的二進制流,各個數(shù)據(jù)項目嚴格按照順序緊湊地排列在 Class 文件中臊恋,其中沒有任何分隔符衣洁,這使得整個 Class 文件中存儲的內(nèi)容幾乎全部都是程序運行必須的數(shù)據(jù),沒有空隙存在抖仅。
Class 文件的完整格式見下表:
其中類型中的 u4坊夫,u2以及沒有這上圖出現(xiàn)的 u1,u8都表示無符號數(shù)撤卢。其中 u1 表示 1 個字節(jié)环凿,u2 表示 2 個字節(jié),以此類推放吩。名稱則說明該位置數(shù)據(jù)的作用智听,如 constant_pool 表示常量池。數(shù)量則說明 Class 文件中屎慢,不同名稱的數(shù)據(jù)塊的個數(shù)瞭稼。如 constant_pool 的個數(shù)取決于 constant_pool_count,而 constant_pool_count 則是占用兩個字節(jié)的無符號數(shù)腻惠。
不管是多復雜的 Class 文件环肘,其內(nèi)部數(shù)據(jù)結(jié)構(gòu)必然是按照上表來排列的,由于其內(nèi)部各類型的數(shù)據(jù)比較復雜集灌,這里也不展開講悔雹。這一節(jié)大家只要知道 Class 文件內(nèi)部有這些數(shù)據(jù)就可以了复哆,如果需要查看 Class 文件,可以使用反編譯工具 javap 腌零。具體命令如下:
javap -v ClassFile
字節(jié)碼指令介紹
JVM 中的指令由一個字節(jié)長度的梯找、代表著某種特定操作含義的數(shù)字(操作碼,字節(jié)碼)以及跟隨其后的零至多個代表此操作所需參數(shù)(操作數(shù))構(gòu)成益涧。由于 JVM 采用面向操作數(shù)棧而不是寄存器的結(jié)構(gòu)锈锤,所以大多數(shù)的指令都不包含操作數(shù),只有一個操作碼闲询。由于指令長度是 8 位的無符號數(shù)久免,所以 JVM 指令最多 256 條。
大部分指令從其助記符就能知道它所操作的數(shù)據(jù)類型扭弧,比如 iload 指令用于從局部變量表加載 int 型的數(shù)據(jù)到操作數(shù)棧阎姥,而 fload 指令加載的則是 float 類型的數(shù)據(jù)。
大部分與數(shù)據(jù)相關(guān)的字節(jié)碼指令鸽捻,其助記符都有特殊的字符來表示其服務的數(shù)據(jù)類型:i 代表對 int 型數(shù)據(jù)的操作呼巴,l 代表 long,s 代表 short御蒲,b 代表 byte衣赶,c 代表 char,f 代表 float厚满,d 代表 double屑埋,a 代表 reference。
如需查看所有 JVM 指令痰滋,點擊此鏈接:http://17b84ff5.wiz03.com/share/s/0nK4_R1FrArb2bpd-P3HNDCf0YLy5c0dYAqj2-_XjE3Jsh_A
JVM 與 Class 文件
前面分開講了 JVM 與 Class 文件,相信大家對它們有了一定的了解续崖。Java 程序之所以可以運行起來敲街,離不開這兩位的緊密配合,下面我要給大家介紹的就是關(guān)于它倆的一些相關(guān)知識严望。
類加載
Class 文件從被 JVM 加載到內(nèi)存多艇,到卸載出內(nèi)存為止,它的整個生命周期包括:加載像吻、驗證峻黍、準備、解析拨匆、初始化姆涩、使用和卸載咸包。其中驗證滋将、準備硼瓣、解析三個部分統(tǒng)稱為連接,這七個階段發(fā)生順序如下圖:
接下來我們簡單了解一下這幾個過程
- 加載:在類的加載階段 JVM 主要做了下面三件事:
- 通過類的全限定名來獲取定義此類的二進制字節(jié)流柠衅。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
- 在內(nèi)存中生成一個代表這個類的 java.lang.Class 對象蒋荚,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口适贸。
這里需要注意第一點,它只說明了通過全限定名獲取定義此類的二進制字節(jié)流察署,但是沒有具體說從哪獲取闷游,怎么獲取。正是有了這么一個開放的入口贴汪,才有了現(xiàn)在 Java 的各種有趣的玩法脐往。
驗證:這一階段的目的是為了確保 Class 文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全嘶是。
準備:為類變量(static 變量)分配內(nèi)存并設置變量的初始值钙勃。
解析:JVM 將常量池內(nèi)的符號引用替換為直接引用。
初始化:前面幾個階段都是 JVM 主導和控制的聂喇,到了這一步開始辖源,才真正開始執(zhí)行 Java 程序代碼。在準備階段 JVM 為類變量設置了初始值希太,而在初始化階段克饶,JVM 會調(diào)用類構(gòu)造器 <clinit>() 方法,即我們平時寫的 static{ ... } 這部分代碼和靜態(tài)變量賦值操作的集合誊辉。
使用:這部分是我們開發(fā)者最熟悉的矾湃,即我們所寫的程序運行的過程。
卸載:當 JVM 確定某個類永久不需要的時候堕澄,就會執(zhí)行類卸載邀跃,將其在內(nèi)存中占用的空間全部釋放。
類加載器
前面介紹 JVM 加載類的時候說到蛙紫,JVM 對于從哪加載二進制字節(jié)流是對外開放的拍屑,即這部分是在 JVM 外部實現(xiàn)的。而用于實現(xiàn)類加載的代碼模塊稱為“類加載器”坑傅。類加載器可以說是 Java 語言的一項創(chuàng)新僵驰,也是 Java 語言流行的重要原因之一,它起初是為 Java Applet 而開發(fā)出開的唁毒。雖然目前 Java Applet 技術(shù)基本已經(jīng)“死了”蒜茴,但類加載器卻在類層次劃分、OSGi浆西、熱部署粉私、代碼加密等領(lǐng)域大放異彩,成為了 Java 體系中一塊重要的基石近零。
下面我用代碼展示如何自定義一個類加載器毡鉴,從網(wǎng)絡上加載一個類崔泵,然后調(diào)用其 toString 方法。其實現(xiàn)很簡單猪瞬,如下:
package com.zqlite.jvm;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class HelloJVM {
public static void main(String ...args){
MyNetClassLoader classLoader = new MyNetClassLoader();
try {
Class<?> clazz = classLoader.findClass("http://7xprgn.com1.z0.glb.clouddn.com/RemoteClass.class");
Object o = clazz.newInstance();
System.out.print(o.toString());
} catch (ClassNotFoundException |IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
private static class MyNetClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
URL url = new URL(name);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
InputStream inputStream = connection.getInputStream();
byte[] b = new byte[inputStream.available()];
inputStream.read(b);
return defineClass("com.zqlite.jvm.RemoteClass",b,0,b.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
}
}
首先我們自定義一個 MyNetClassLoader 類來繼承 ClassLoader 憎瘸,然后重寫它的 findClass 方法,此方法會在系統(tǒng)找不到指定類的時候調(diào)用陈瘦。在 findClass 中我們做的事情很簡單幌甘,從網(wǎng)絡上獲取類的二進制字節(jié)流,然后讀入數(shù)組痊项,最后通過 defineClass 方法锅风,將其轉(zhuǎn)為 class 對象并返回。
然后通過 class 對象的 newInstance 方法獲得一個對應的實例對象鞍泉,即 RemoteClass 的對象皱埠,然后輸入其 toString 返回的內(nèi)容。具體輸出什么大家可以在本地跑了試試咖驮。
基于棧的解釋器執(zhí)行過程
在 JVM 的內(nèi)存區(qū)域劃分的時候講到過虛擬機棧這個區(qū)域边器,它是線程私有區(qū)域,用于描述 Java 方法的執(zhí)行模型托修。每個方法在執(zhí)行同時會創(chuàng)建一個棧幀用于存儲局部變量表忘巧、操作數(shù)、動態(tài)鏈接睦刃、方法出口等信息砚嘴。接下來我就用一個實際的例子來介紹在 JVM 中,一個方法是如何被解釋執(zhí)行的涩拙。借此機會也可以更進一步了解虛擬機棧相關(guān)知識际长。
運行時棧幀結(jié)構(gòu)
棧幀是用于支持 JVM 進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機棧的棧元素兴泥。每一個方法從調(diào)用到完成也颤,都對應著一個棧幀在虛擬機棧中的入棧和出棧操作。
每個棧幀都包括局部變量表郁轻、操作數(shù)、動態(tài)連接文留、方法返回地址和一些額外的附加信信息好唯。并且在程序進行編譯的時候,棧幀需要多大的局部變量表燥翅,多深的操作數(shù)棧都是已經(jīng)確定的骑篙,因此一個棧幀需要分配多少內(nèi)存也是確定不變的。JVM 中線程森书、虛擬機棧靶端、棧幀的典型結(jié)構(gòu)圖如下:
解釋器執(zhí)行過程實例
本節(jié)中谎势,我準備了如下一段代碼:
package com.zqlite.jvm;
public class HelloJVM {
public static void main(String ...args){
calc();
}
public static int calc(){
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
}
從 Java 的語言角度來講,這段代碼沒有任何解釋的必要杨名,我們接下來用 javap 來看看他的字節(jié)碼指令脏榆,如下:
Classfile /Users/scott/workspace/jvm/out/production/jvm/com/zqlite/jvm/HelloJVM.class
Last modified 2017-11-30; size 549 bytes
MD5 checksum 1bf313c415146d6070a30e8addc83a8c
Compiled from "HelloJVM.java"
public class com.zqlite.jvm.HelloJVM
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#24 // java/lang/Object."<init>":()V
#2 = Methodref #3.#25 // com/zqlite/jvm/HelloJVM.calc:()I
#3 = Class #26 // com/zqlite/jvm/HelloJVM
#4 = Class #27 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 Lcom/zqlite/jvm/HelloJVM;
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 args
#15 = Utf8 [Ljava/lang/String;
#16 = Utf8 calc
#17 = Utf8 ()I
#18 = Utf8 a
#19 = Utf8 I
#20 = Utf8 b
#21 = Utf8 c
#22 = Utf8 SourceFile
#23 = Utf8 HelloJVM.java
#24 = NameAndType #5:#6 // "<init>":()V
#25 = NameAndType #16:#17 // calc:()I
#26 = Utf8 com/zqlite/jvm/HelloJVM
#27 = Utf8 java/lang/Object
{
public com.zqlite.jvm.HelloJVM();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zqlite/jvm/HelloJVM;
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=1, locals=1, args_size=1
0: invokestatic #2 // Method calc:()I
3: pop
4: return
LineNumberTable:
line 6: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 args [Ljava/lang/String;
public static int calc();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=0
0: bipush 100
2: istore_0
3: sipush 200
6: istore_1
7: sipush 300
10: istore_2
11: iload_0
12: iload_1
13: iadd
14: iload_2
15: imul
16: ireturn
LineNumberTable:
line 10: 0
line 11: 3
line 12: 7
line 13: 11
LocalVariableTable:
Start Length Slot Name Signature
3 14 0 a I
7 10 1 b I
11 6 2 c I
}
SourceFile: "HelloJVM.java"
我們把注意點放在 calc 方法上,其中
descriptor: ()I
表示這個方法的方法描述符是 ()I台谍。了解 JNI 的小伙伴對方法描述符應該不會陌生须喂,這里我不多介紹,想了解更多可以參考此處趁蕊。
接著的
flags: ACC_PUBLIC, ACC_STATIC
表示這個是一個 public static 修飾的方法坞生。
然后 Code 部分是重點,其中 stack = 2 說明操作數(shù)棧的深度為 2 掷伙,locals = 3 說明本地變量表有三個變量是己,args_size = 0 說明此方法沒有參數(shù)。
接下來先跳過下面的字節(jié)碼任柜,來看到 LocalVariableTable卒废,這就是所謂的本地變量表了,表中有三行數(shù)據(jù)乘盼,分別是 a升熊,b,c 和代碼中定義的變量名一致绸栅。且他們的簽名都是 I 级野,表示是 Int 型整數(shù)。
接下來粹胯,我通過下面 7 幅圖來講解下解釋執(zhí)行這個方法的過程和此過程中局部變量表和操作數(shù)棧的變化情況蓖柔。
上圖是執(zhí)行偏移地址為 0 的指令的情況。
上圖是執(zhí)行偏移地址為 2 的指令的情況风纠。
上圖是執(zhí)行偏移地址為 11 的指令的情況况鸣。
上圖是執(zhí)行偏移地址為 12 的指令的情況。
上圖是執(zhí)行偏移地址為 13 的指令的情況竹观。
上圖是執(zhí)行偏移地址為 14 的指令的情況镐捧。
上圖是執(zhí)行偏移地址為 16 的指令的情況。
方法的執(zhí)行模型到這就結(jié)束了臭增,請大家記住這僅僅是一種概念模型懂酱,虛擬機最終會對執(zhí)行過程做一些優(yōu)化來提升性能,實際的執(zhí)行過程與概念模型差距可能會很大誊抛。之所以會有這種差距是因為虛擬機的執(zhí)行引擎和即時編譯器都會對輸入的字節(jié)碼進行優(yōu)化列牺。
總結(jié)
這篇文章我主要帶著大家簡單了解了一下 JVM 的內(nèi)層劃分、對象的創(chuàng)建拗窃、字節(jié)碼結(jié)構(gòu)瞎领、字節(jié)碼指令和類加載與解釋執(zhí)行這幾部分的相關(guān)內(nèi)容泌辫。由于篇幅有限,如 GC九默,編譯優(yōu)化和 JVM內(nèi)部實現(xiàn)并發(fā)等內(nèi)容沒有介紹震放,如果大家感興趣可以閱讀《深入理解 Java 虛擬機-JVM 高級特性與最佳實踐》。本文也是參考此書總結(jié)而來荤西。