摘要:虛擬機(jī)執(zhí)行程序的基礎(chǔ)是特定的二進(jìn)制指令集和運(yùn)行時(shí)棧幀二進(jìn)制指令集是虛擬機(jī)規(guī)定的一些指令,在編譯后二進(jìn)制字節(jié)碼的類方法里的字節(jié)碼就是這種指令,所以只要找到方法區(qū)里的類方法就可以依照這套指令集去執(zhí)行命令。
這篇文章的素材來(lái)自周志明的《深入理解Java虛擬機(jī)》。
作為Java開(kāi)發(fā)人員,一定程度了解JVM虛擬機(jī)的的運(yùn)作方式非常重要,本文就一些簡(jiǎn)單的虛擬機(jī)的相關(guān)概念和運(yùn)作機(jī)制展開(kāi)我自己的學(xué)習(xí)過(guò)程,是這個(gè)系列的第三篇。
虛擬機(jī)運(yùn)行活化的內(nèi)存數(shù)據(jù)中的指令:程序的執(zhí)行前面我們說(shuō)明了java源碼被編譯成為了二進(jìn)制字節(jié)碼,二進(jìn)制字節(jié)碼轉(zhuǎn)為內(nèi)存中方法區(qū)里存儲(chǔ)的活化對(duì)象,那么最重要的程序執(zhí)行就做好了基礎(chǔ):當(dāng)方法區(qū)里的字段和方法按照虛擬機(jī)規(guī)定的數(shù)據(jù)結(jié)構(gòu)排好,常量池中的符號(hào)引用數(shù)據(jù)在加載過(guò)程中最大限度地轉(zhuǎn)為了直接引用,那么這個(gè)時(shí)候虛擬機(jī)就可以在加載主類后創(chuàng)建新的線程按步執(zhí)行主類的main函數(shù)中的指令了。
java虛擬機(jī)執(zhí)行程序的基礎(chǔ)是特定的二進(jìn)制指令集和運(yùn)行時(shí)棧幀:
二進(jìn)制指令集是java虛擬機(jī)規(guī)定的一些指令,在編譯后二進(jìn)制字節(jié)碼的類方法里的字節(jié)碼就是這種指令,所以只要找到方法區(qū)里的類方法就可以依照這套指令集去執(zhí)行命令。
運(yùn)行時(shí)棧幀是虛擬機(jī)執(zhí)行的物理所在,在這個(gè)棧幀結(jié)構(gòu)上,方法的局部變量、操作數(shù)棧、動(dòng)態(tài)鏈接和返回地址依序排列,依照命令動(dòng)態(tài)變換棧幀上的數(shù)據(jù),最終完成所有的這個(gè)方法上的指令。
棧幀的進(jìn)一步劃分:
局部變量表:包括方法的參數(shù)和方法體內(nèi)部的局部變量都會(huì)存在這個(gè)表中。
操作數(shù)棧:操作數(shù)棧是一個(gè)運(yùn)行中間產(chǎn)生的操作數(shù)構(gòu)成的棧,這個(gè)棧的棧頂保存的就是當(dāng)前活躍的操作數(shù)。
動(dòng)態(tài)鏈接:我們之前提到這個(gè)方法中調(diào)用的方法和類在常量池中的符號(hào)引用轉(zhuǎn)換為的直接引用就保存在這里,只要訪問(wèn)到這些方法和類的時(shí)候就會(huì)根據(jù)動(dòng)態(tài)鏈接去直接引用所指的地址加載那些方法。
返回地址:程序正常結(jié)束恢復(fù)上一個(gè)棧幀的狀態(tài)的時(shí)候需要知道上一個(gè)指令的地址。
現(xiàn)在我們使用一個(gè)綜合實(shí)例來(lái)說(shuō)明運(yùn)行的整個(gè)過(guò)程:
源代碼如下,邏輯很簡(jiǎn)單:
public class TestDemo { public static int minus(int x){ return -x; } public static void main(String[] args) { int x = 5; int y = minus(x); } }
我們可以分析它的二進(jìn)制字節(jié)碼,當(dāng)然這里我們借助javap工具進(jìn)行分析:
jinhaoplus$ javap -verbose TestDemo Classfile /Users/jinhao/Desktop/TestDemo.class Last modified 2015-10-17; size 342 bytes MD5 checksum 4f37459aa1b3438b1608de788d43586d Compiled from "TestDemo.java" public class TestDemo minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#15 // java/lang/Object."":()V #2 = Methodref #3.#16 // TestDemo.minus:(I)I #3 = Class #17 // TestDemo #4 = Class #18 // java/lang/Object #5 = Utf8 #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 minus #10 = Utf8 (I)I #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 TestDemo.java #15 = NameAndType #5:#6 // " ":()V #16 = NameAndType #9:#10 // minus:(I)I #17 = Utf8 TestDemo #18 = Utf8 java/lang/Object { public TestDemo(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: return LineNumberTable: line 1: 0 public static int minus(int); descriptor: (I)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iload_0 1: ineg 2: ireturn LineNumberTable: line 3: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=3, args_size=1 0: iconst_5 1: istore_1 2: iload_1 3: invokestatic #2 // Method minus:(I)I 6: istore_2 7: return LineNumberTable: line 6: 0 line 7: 2 line 8: 7 } SourceFile: "TestDemo.java"
這個(gè)過(guò)程是從固化在class文件中的二進(jìn)制字節(jié)碼開(kāi)始,經(jīng)過(guò)加載器對(duì)當(dāng)前類的加載,虛擬機(jī)對(duì)二進(jìn)制碼的驗(yàn)證、準(zhǔn)備和一定的解析,進(jìn)入內(nèi)存中的方法區(qū),常量池中的符號(hào)引用一定程度上轉(zhuǎn)換為直接引用,使得字節(jié)碼通過(guò)結(jié)構(gòu)化的組織讓虛擬機(jī)了解類的每一塊的構(gòu)成,創(chuàng)建的線程申請(qǐng)到了虛擬機(jī)棧中的空間構(gòu)造出屬于這一線程的棧幀空間,執(zhí)行主類的main方法:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=3, args_size=1 0: iconst_5 1: istore_1 2: iload_1 3: invokestatic #2 // Method minus:(I)I 6: istore_2 7: return LineNumberTable: line 6: 0 line 7: 2 line 8: 7 }
首先檢查main的訪問(wèn)標(biāo)志、描述符描述的返回類型和參數(shù)列表,確定可以訪問(wèn)后進(jìn)入Code屬性表執(zhí)行命令,讀入棧深度建立符合要求的操作數(shù)棧,讀入局部變量大小建立符合要求的局部變量表,根據(jù)參數(shù)數(shù)向局部變量表中依序加入?yún)?shù)(第一個(gè)參數(shù)是引用當(dāng)前對(duì)象的this,所以空參數(shù)列表的參數(shù)數(shù)也是1),然后開(kāi)始根據(jù)命令正式執(zhí)行:
0: iconst_5
將整數(shù)5壓入棧頂
1: istore_1
將棧頂整數(shù)值存入局部變量表的slot1(slot0是參數(shù)this)
2: iload_1
將slot1壓入棧頂
3: invokestatic #2 // Method minus:(I)I
二進(jìn)制invokestatic方法用于調(diào)用靜態(tài)方法,參數(shù)是根據(jù)常量池中已經(jīng)轉(zhuǎn)換為直接引用的常量,意即minus函數(shù)在方法區(qū)中的地址,找到這個(gè)地址調(diào)用函數(shù),向其中加入的參數(shù)為棧頂?shù)闹?/p>
6: istore_2
將棧頂整數(shù)存入局部變量的slot2
7: return
將返回地址中存儲(chǔ)的PC地址返到PC,棧幀恢復(fù)到調(diào)用前
現(xiàn)在我們分析調(diào)用minus函數(shù)的時(shí)候進(jìn)入minus函數(shù)的過(guò)程:
public static int minus(int); descriptor: (I)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iload_0 1: ineg 2: ireturn LineNumberTable: line 3: 0
同樣的首先檢查minus函數(shù)的訪問(wèn)標(biāo)志、描述符描述的返回類型和參數(shù)列表,確定可以訪問(wèn)后進(jìn)入Code屬性表執(zhí)行命令,讀入棧深度建立符合要求的操作數(shù)棧,讀入局部變量大小建立符合要求的局部變量表,根據(jù)參數(shù)數(shù)向局部變量表中依序加入?yún)?shù),然后開(kāi)始根據(jù)命令正式執(zhí)行:
0: iload_0
將slot0壓入棧頂,也就是傳入的參數(shù)
1: ineg
將棧頂?shù)闹祻棾鋈∝?fù)后壓回棧頂
2: ireturn
將返回地址中存儲(chǔ)的PC地址返到PC,棧幀恢復(fù)到調(diào)用前
這個(gè)過(guò)程結(jié)束后對(duì)象的生命周期結(jié)束,因此開(kāi)始執(zhí)行GC回收內(nèi)存中的對(duì)象,包括堆中的類對(duì)應(yīng)的java.lang.Class對(duì)象,卸載方法區(qū)中的類。
方法的解析和分派上面這個(gè)例子中main方法里調(diào)用minus方法的時(shí)候是沒(méi)有二義性的,因?yàn)閺亩M(jìn)制字節(jié)碼里我們可以看到invokestatic方法調(diào)用的是minus方法的直接引用,也就說(shuō)在編譯期這個(gè)調(diào)用就已經(jīng)決定了。這個(gè)時(shí)候我們來(lái)說(shuō)說(shuō)方法調(diào)用,這個(gè)部分的內(nèi)容在前面的類加載時(shí)候提過(guò),在能夠唯一確定方法的直接引用的時(shí)候虛擬機(jī)會(huì)將常量表里的符號(hào)引用轉(zhuǎn)換為直接引用,這樣在運(yùn)行的時(shí)候就可以直接根據(jù)這個(gè)地址找到對(duì)應(yīng)的方法去執(zhí)行,這種時(shí)候的轉(zhuǎn)換才能叫做我們當(dāng)時(shí)提到的在連接過(guò)程中的解析。
但是如果方法是動(dòng)態(tài)綁定的,也就是說(shuō)在編譯期我們并不知道使用哪個(gè)方法(或者叫不知道使用方法的哪個(gè)版本),那么這個(gè)時(shí)候就需要在運(yùn)行時(shí)才能確定哪個(gè)版本的方法將被調(diào)用,這個(gè)時(shí)候才能將符號(hào)引用轉(zhuǎn)換為直接引用。這個(gè)問(wèn)題提到的多個(gè)版本的方法在java中的重載和多態(tài)重寫(xiě)問(wèn)題息息相關(guān)。
重載(override)
public class TestDemo { static class Human{ } static class Man extends Human{ } static class Woman extends Human{ } public void sayHello(Human human) { System.out.println("hello human"); } public void sayHello(Man man) { System.out.println("hello man"); } public void sayHello(Woman woman) { System.out.println("hello woman"); } public static void main(String[] args) { TestDemo demo = new TestDemo(); Human man = new Man(); Human woman = new Woman(); demo.sayHello(man); demo.sayHello(woman); } }
javap結(jié)果:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: new #7 // class TestDemo 3: dup 4: invokespecial #8 // Method "":()V 7: astore_1 8: new #9 // class TestDemo$Man 11: dup 12: invokespecial #10 // Method TestDemo$Man." ":()V 15: astore_2 16: new #11 // class TestDemo$Woman 19: dup 20: invokespecial #12 // Method TestDemo$Woman." ":()V 23: astore_3 24: aload_1 25: aload_2 26: invokevirtual #13 // Method sayHello:(LTestDemo$Human;)V 29: aload_1 30: aload_3 31: invokevirtual #13 // Method sayHello:(LTestDemo$Human;)V 34: return LineNumberTable: line 21: 0 line 22: 8 line 23: 16 line 24: 24 line 25: 29 line 26: 34
重寫(xiě)(overwrite)
public class TestDemo { static class Human{ public void sayHello() { System.out.println("hello human"); } } static class Man extends Human{ public void sayHello() { System.out.println("hello man"); } } static class Woman extends Human{ public void sayHello() { System.out.println("hello woman"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); } }
javap結(jié)果:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #2 // class TestDemo$Man 3: dup 4: invokespecial #3 // Method TestDemo$Man."":()V 7: astore_1 8: new #4 // class TestDemo$Woman 11: dup 12: invokespecial #5 // Method TestDemo$Woman." ":()V 15: astore_2 16: aload_1 17: invokevirtual #6 // Method TestDemo$Human.sayHello:()V 20: aload_2 21: invokevirtual #6 // Method TestDemo$Human.sayHello:()V 24: return LineNumberTable: line 20: 0 line 21: 8 line 22: 16 line 23: 20 line 24: 24
我們可以看出來(lái)無(wú)論是重載還是重寫(xiě),都是二進(jìn)制指令invokevirtual調(diào)用了sayHello方法來(lái)執(zhí)行的。
在重載中,程序調(diào)用的是參數(shù)實(shí)際類型不同的方法,但是虛擬機(jī)最終分派了相同外觀類型(靜態(tài)類型)的方法,這說(shuō)明在重載的過(guò)程中虛擬機(jī)在運(yùn)行的時(shí)候是只看參數(shù)的外觀類型(靜態(tài)類型)的,而這個(gè)外觀類型(靜態(tài)類型)是在編譯的時(shí)候就已經(jīng)確定的,和虛擬機(jī)沒(méi)有關(guān)系。這種依賴靜態(tài)類型來(lái)做方法的分配叫做靜態(tài)分派。
在重寫(xiě)中,程序調(diào)用的是不同實(shí)際類型的同名方法,虛擬機(jī)依據(jù)對(duì)象的實(shí)際類型去尋找是否有這個(gè)方法,如果有就執(zhí)行,如果沒(méi)有去父類里找,最終在實(shí)際類型里找到了這個(gè)方法,所以最終是在運(yùn)行期動(dòng)態(tài)分派了方法。在編譯的時(shí)候我們可以看到字節(jié)碼指示的方法都是一樣的符號(hào)引用,但是運(yùn)行期虛擬機(jī)能夠根據(jù)實(shí)際類型去確定出真正需要的直接引用。這種依賴實(shí)際類型來(lái)做方法的分配叫做動(dòng)態(tài)分派。得益于java虛擬機(jī)的動(dòng)態(tài)分派會(huì)在分派前確定對(duì)象的實(shí)際類型,面向?qū)ο蟮亩鄳B(tài)性才能體現(xiàn)出來(lái)。
對(duì)象的創(chuàng)建和堆內(nèi)存的分配前面我們提到的都是類在方法區(qū)中的內(nèi)存分配:
在方法區(qū)中有類的常量池,常量池中保存著類的很多信息的符號(hào)引用,很多符號(hào)引用還轉(zhuǎn)換為了直接引用以使在運(yùn)行的過(guò)程能夠訪問(wèn)到這些信息的真實(shí)地址。
那么創(chuàng)建出的對(duì)象是怎么在堆中分配空間的呢?
首先我們要明確對(duì)象中存儲(chǔ)的大部分的數(shù)據(jù)就是它對(duì)應(yīng)的非靜態(tài)字段和每個(gè)字段方法對(duì)應(yīng)的方法區(qū)中的地址,因?yàn)檫@些東西每個(gè)對(duì)象都是不一樣的,所以必須通過(guò)各自的堆空間存儲(chǔ)這些不一樣的數(shù)據(jù),而方法是所有同類對(duì)象共用的,因?yàn)榉椒ǖ拿钍且粯拥?,每個(gè)對(duì)象只是在各自的線程棧幀里提供各自的局部變量表和操作數(shù)棧就好。
這樣看來(lái),堆中存放的是真正“有個(gè)性”的屬于對(duì)象自己的變量,這些變量往往是最占空間的,而這些變量對(duì)應(yīng)的類字段的地址會(huì)找到位于方法區(qū)中,同樣的同類對(duì)象如果要執(zhí)行一個(gè)方法只需要在自己的棧幀里面創(chuàng)建局部變量表和操作數(shù)棧,然后根據(jù)方法對(duì)應(yīng)的方法區(qū)中的地址去尋找到方法體執(zhí)行其中的命令即可,這樣一來(lái)堆里面只存放有限的真正有意義的數(shù)據(jù)和地址,方法區(qū)里存放共用的字段和方法體,能最大程度地減小內(nèi)存開(kāi)銷。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/64695.html
摘要:虛擬機(jī)包括一套字節(jié)碼指令集一組寄存器一個(gè)棧一個(gè)垃圾回收堆和一個(gè)存儲(chǔ)方法域。而使用虛擬機(jī)是實(shí)現(xiàn)這一特點(diǎn)的關(guān)鍵。虛擬機(jī)在執(zhí)行字節(jié)碼時(shí),把字節(jié)碼解釋成具體平臺(tái)上的機(jī)器指令執(zhí)行。此內(nèi)存區(qū)域是唯一一個(gè)在虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何情況的區(qū)域。 1、 什么是JVM? JVM是Java Virtual Machine(Java虛擬機(jī))的縮寫(xiě),JVM是一種用于計(jì)算設(shè)備的規(guī)范,它是一個(gè)虛構(gòu)出來(lái)的計(jì)算機(jī),...
摘要:字節(jié)碼是程序的中間表示形式介于人類可讀的源碼和機(jī)器碼之間。在中一般是用編譯源文件變成字節(jié)碼,也就是我們的文件。字節(jié)碼的執(zhí)行操作,指的就是對(duì)當(dāng)前棧幀數(shù)據(jù)結(jié)構(gòu)進(jìn)行的操作。 0.寫(xiě)在前面 為什么會(huì)寫(xiě)這篇文章呢?主要是之前調(diào)研過(guò)日志脫敏相關(guān)的一些,具體可以參考LOG4j脫敏插件如何編寫(xiě)里面描述了日志脫敏插件編寫(xiě)方法: 直接在toString中修改代碼,這種方法很麻煩,效率低,需要修改每一個(gè)要...
摘要:由虛擬機(jī)加載的類,被加載到虛擬機(jī)內(nèi)存中之后,虛擬機(jī)會(huì)讀取并執(zhí)行它里面存在的字節(jié)碼指令。虛擬機(jī)中執(zhí)行字節(jié)碼指令的部分叫做執(zhí)行引擎。 什么是Java虛擬機(jī)? 作為一個(gè)Java程序員,我們每天都在寫(xiě)Java代碼,我們寫(xiě)的代碼都是在一個(gè)叫做Java虛擬機(jī)的東西上執(zhí)行的。但是如果要問(wèn)什么是虛擬機(jī),恐怕很多人就會(huì)模棱兩可了。在本文中,我會(huì)寫(xiě)下我對(duì)虛擬機(jī)的理解。因?yàn)槟芰λ?,可能有些地方描述的不夠?..
摘要:字節(jié)碼及使用什么是字節(jié)碼機(jī)器碼機(jī)器碼是可直接解讀的指令。字節(jié)碼的執(zhí)行操作,指的就是對(duì)當(dāng)前棧幀數(shù)據(jù)結(jié)構(gòu)進(jìn)行的操作。動(dòng)態(tài)鏈接每個(gè)棧幀指向運(yùn)行時(shí)常量池中該棧幀所屬的方法的引用,也就是字節(jié)碼的發(fā)放調(diào)用的引用。 字節(jié)碼及ASM使用 什么是字節(jié)碼? 機(jī)器碼機(jī)器碼(machine code)是CPU可直接解讀的指令。機(jī)器碼與硬件等有關(guān),不同的CPU架構(gòu)支持的硬件碼也不相同。 字節(jié)碼字節(jié)碼(byte...
摘要:我們都知道要運(yùn)行代碼就必須要有,也就是運(yùn)行時(shí)環(huán)境,中包含了程序的必需組件,包括虛擬機(jī)以及核心類庫(kù),然而運(yùn)行代碼則不需要額外的運(yùn)行時(shí)環(huán)境,只需要把代碼編譯成能識(shí)別的指令即可,也就是機(jī)器碼那為什么不直接像那樣而需要在虛擬機(jī)中運(yùn)行呢他在虛擬機(jī)中又 我們都知道要運(yùn)行Java代碼就必須要有JRE,也就是Java運(yùn)行時(shí)環(huán)境,JRE中包含了Java程序的必需組件,包括Java虛擬機(jī)以及Java核心類...
摘要:實(shí)現(xiàn)這個(gè)口號(hào)的就是可以運(yùn)行在不同平臺(tái)上的虛擬機(jī)和與平臺(tái)無(wú)關(guān)的字節(jié)碼。類加載過(guò)程加載加載是類加載的第一個(gè)階段,虛擬機(jī)要完成以下三個(gè)過(guò)程通過(guò)類的全限定名獲取定義此類的二進(jìn)制字節(jié)流。驗(yàn)證目的是確保文件字節(jié)流信息符合虛擬機(jī)的要求。 引言 我們知道java代碼編譯后生成的是字節(jié)碼,那虛擬機(jī)是如何加載這些class字節(jié)碼文件的呢?加載之后又是如何進(jìn)行方法調(diào)用的呢? 一 類文件結(jié)構(gòu) 無(wú)關(guān)性基石 ja...
閱讀 3525·2021-11-24 09:39
閱讀 787·2019-08-30 14:22
閱讀 3038·2019-08-30 13:13
閱讀 2321·2019-08-29 17:06
閱讀 2925·2019-08-29 16:22
閱讀 1262·2019-08-29 10:58
閱讀 2436·2019-08-26 13:47
閱讀 1635·2019-08-26 11:39