JVM 概覽

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 程序設(shè)計語言。Java 的技術(shù)體系從傳統(tǒng)意義上來看有以下幾個:

  1. Java 程序設(shè)計語言
  2. 各硬件平臺上的 Java 虛擬機(jī)
  3. Class 文件格式
  4. Java API 類庫
  5. 其他商業(yè)機(jī)構(gòu)或開源社區(qū)的第三方 Java 類庫

其中 1、4、5 大家應(yīng)該比較熟悉,在編程中都能直接接觸。2、3 對于大家來說可能陌生了些,但不夸張的講,Java 能有如今的活力,其功臣正是 2 和 3。所以接下來的內(nèi)容將圍繞這兩點展開,讓你從更深的層次了解你所熟悉的 Java。

JVM

JVM(Java Virtual Machine)就是上面所說的 Java 虛擬機(jī),其作用是加載與運行 Class 文件。有句話大家一定不陌生,“一次編寫,到處運行”。簡單解釋一下,因為在 Class 文件和硬件平臺中間隔著一個 JVM,由 JVM 負(fù)責(zé)加載和執(zhí)行 Class 文件,這樣平臺的差異性就留給了 JVM 去考慮,而不是 Class 文件。也正因如此,Class 文件的格式可以真正做到平臺無關(guān)性。

JVM 中的 Java 內(nèi)存區(qū)域劃分

對于從事 C、C++ 的開發(fā)人員來說,在內(nèi)存管理領(lǐng)域他們是擁有最高權(quán)力的“皇帝”,但又是從事最底層基礎(chǔ)工作的“勞動人民”。他們即擁有與內(nèi)存直接打交道的權(quán)利,又要負(fù)責(zé)維護(hù)每一個對象的生命周期。

對于 Java 開發(fā)人員來說就輕松多了,在 JVM 自動內(nèi)存管理機(jī)制的幫助下,不再需要維護(hù)每一個對象的生命周期,內(nèi)存控制的權(quán)利由開發(fā)人員轉(zhuǎn)移到了 JVM 。

因為 JVM 需要管理 Java 運行期間的各種對象的生命周期,所以它在執(zhí)行 Java 程序的時候會把它所管轄的內(nèi)存分成若干個不同的數(shù)據(jù)區(qū)域,具體如下圖:

image

對以上幾個區(qū)域做下簡單的介紹:

  • 方法區(qū):線程共享區(qū)域,用于存儲被虛擬機(jī)加載的類信息、常量、靜態(tài)變量等數(shù)據(jù)。
  • 堆:線程共享區(qū)域,存放類實例以及數(shù)組。我們平時聽到的 GC(垃圾回收)就是在堆上進(jìn)行的。
  • 虛擬機(jī)棧:線程私有區(qū)域,用于描述 Java 方法的執(zhí)行模型。每個方法在執(zhí)行同時會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)、動態(tài)鏈接、方法出口等信息。
  • 本地方法棧:線程私有區(qū)域,與虛擬機(jī)棧作用類似,區(qū)別在于本地方法棧執(zhí)行的是 Native 方法。
  • 程序計數(shù)器:線程私有區(qū)域,用于指向當(dāng)前線程下一條需要執(zhí)行的字節(jié)碼指令。

除了以上幾個主要區(qū)域外,另外還要介紹一個區(qū)域:運行時常量池。它是方法區(qū)的一部分,用于存放編譯期生成的字面量和符號引用。字面量好理解,比如:

String str = "a";

上面的 a 就是字面量。而符號引用則是一些字符串,用于給虛擬機(jī)定位類或類方法等。

面試的時候有時會遇到這樣的問題,Java 中每次聲明并初始化一個 String 對象都會在堆上面分配內(nèi)存嗎?這里的答案當(dāng)然是否,因為有可能新聲明的對象已經(jīng)在常量池中存在了,這時候 JVM 會將新對象的引用指向常量池中對應(yīng)的值而不是在堆重新分配。

對象在 JVM 中的創(chuàng)建過程

在 Java 中創(chuàng)建對象很簡單,一個簡單的 new 關(guān)鍵字就可以。但在虛擬機(jī)中,對象的創(chuàng)建過程是怎樣的呢?我們來一起了解一下。

在 JVM 中,創(chuàng)建對象的字節(jié)碼指令是 new,當(dāng) JVM 執(zhí)行引擎遇到 new 指令后會進(jìn)行如下五步操作:

一、檢查這個指令的參數(shù)能否在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經(jīng)加載、解析和初始化。

下面我們看一下:

package com.zqlite.jvm;

public class HelloJVM {

    public static void main(String ...args){
        Object obj = new Object();
    }
}

然后用 javap 對這段代碼生成的 class 文件進(jìn)行反匯編處理,結(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 對應(yīng)的內(nèi)容:

#2 = Class              #20            // java/lang/Object

這里說明了 #2 對應(yīng)的是一個類,類符號引用需要到 #20 中找,在看 #20 :

#20 = Utf8               java/lang/Object

這里的 #20 是一個字符串類型,其內(nèi)容是 ava/lang/Object,表示 Object 類的符號引用即全限定名。接著 JVM 就會去尋找并加載、解析和初始化這個類。

二、當(dāng)在第一步確定了目標(biāo)類以后,JVM 就會為這個類的新生對象在堆上面分配內(nèi)存。對象所需要的內(nèi)存大小在第一步的類加載后已經(jīng)確定了,所以為對象分配內(nèi)存也就等同于把一塊確定大小的內(nèi)存從 Java 堆中劃分出來。

三、分配完內(nèi)存后,JVM 需要將分配到的內(nèi)存空間都初始化為零值(不包括對象頭)。

這里需要注意,由于 JVM 會對對象的內(nèi)存空間做初始化操作,所以在類中類似的定義一個 int i ,其默認(rèn)值就是整型的零值 0 。Java 類變量也正因如此允許不賦初始值。但在類方法中,如果這樣定義一個 i,在接下來的代碼中如果使用 i 的話,會有報錯提示 i 沒有初始化值,

四、當(dāng)初始化完以后,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)標(biāo)志、線程持有的鎖等。另外一部分則是類型指針,用于指向它的類元數(shù)據(jù)。JVM 可以通過這個指針來確定對象是屬于哪個類的。另外需要注意的是如果對象是數(shù)組,那么對象頭中還需要記錄數(shù)組的長度。

實例數(shù)據(jù)部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型字段的內(nèi)容,包括從父類繼承下來的和自己定義的。

最后一部分并不是必須的,它僅僅起到了占位符的作用。有些虛擬機(jī)要求對象的起始地址必須是 8 字節(jié)的整數(shù)倍,換句話說就是對象大小必須是 8 字節(jié)的整數(shù)倍。所以當(dāng)不足整數(shù)倍的時候,就需要進(jìn)行對齊填充處理。

下面這張圖簡單表示了對象在內(nèi)存中的布局:

image

Class 文件

計算機(jī)只認(rèn)識 0 和 1 ,這是常識。所以我們編寫的程序都需要經(jīng)過編譯器翻譯成有 0 和 1 構(gòu)成的二進(jìn)制格式才能被計算機(jī)執(zhí)行。但在近十幾年內(nèi),虛擬機(jī)以及大量建立在虛擬機(jī)上的程序語言如雨后春筍般出現(xiàn)并蓬勃發(fā)展,由此二進(jìn)制本地機(jī)器碼已不再是唯一的選擇,越來越多的程序語言選擇了與操作系統(tǒng)和機(jī)器指令集無關(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é)為基礎(chǔ)單位的二進(jìn)制流,各個數(shù)據(jù)項目嚴(yán)格按照順序緊湊地排列在 Class 文件中,其中沒有任何分隔符,這使得整個 Class 文件中存儲的內(nèi)容幾乎全部都是程序運行必須的數(shù)據(jù),沒有空隙存在。

Class 文件的完整格式見下表:

image

其中類型中的 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ù)。

不管是多復(fù)雜的 Class 文件,其內(nèi)部數(shù)據(jù)結(jié)構(gòu)必然是按照上表來排列的,由于其內(nèi)部各類型的數(shù)據(jù)比較復(fù)雜,這里也不展開講。這一節(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é)碼指令,其助記符都有特殊的字符來表示其服務(wù)的數(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)存為止,它的整個生命周期包括:加載、驗證、準(zhǔn)備、解析、初始化、使用和卸載。其中驗證、準(zhǔn)備、解析三個部分統(tǒng)稱為連接,這七個階段發(fā)生順序如下圖:

image

接下來我們簡單了解一下這幾個過程

  • 加載:在類的加載階段 JVM 主要做了下面三件事:
  1. 通過類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。
  2. 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
  3. 在內(nèi)存中生成一個代表這個類的 java.lang.Class 對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。

這里需要注意第一點,它只說明了通過全限定名獲取定義此類的二進(jìn)制字節(jié)流,但是沒有具體說從哪獲取,怎么獲取。正是有了這么一個開放的入口,才有了現(xiàn)在 Java 的各種有趣的玩法。

  • 驗證:這一階段的目的是為了確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害虛擬機(jī)自身的安全。

  • 準(zhǔn)備:為類變量(static 變量)分配內(nèi)存并設(shè)置變量的初始值。

  • 解析:JVM 將常量池內(nèi)的符號引用替換為直接引用。

  • 初始化:前面幾個階段都是 JVM 主導(dǎo)和控制的,到了這一步開始,才真正開始執(zhí)行 Java 程序代碼。在準(zhǔn)備階段 JVM 為類變量設(shè)置了初始值,而在初始化階段,JVM 會調(diào)用類構(gòu)造器 <clinit>() 方法,即我們平時寫的 static{ ... } 這部分代碼和靜態(tài)變量賦值操作的集合。

  • 使用:這部分是我們開發(fā)者最熟悉的,即我們所寫的程序運行的過程。

  • 卸載:當(dāng) JVM 確定某個類永久不需要的時候,就會執(zhí)行類卸載,將其在內(nèi)存中占用的空間全部釋放。

類加載器

前面介紹 JVM 加載類的時候說到,JVM 對于從哪加載二進(jìn)制字節(jié)流是對外開放的,即這部分是在 JVM 外部實現(xiàn)的。而用于實現(xiàn)類加載的代碼模塊稱為“類加載器”。類加載器可以說是 Java 語言的一項創(chuàng)新,也是 Java 語言流行的重要原因之一,它起初是為 Java Applet 而開發(fā)出開的。雖然目前 Java Applet 技術(shù)基本已經(jīng)“死了”,但類加載器卻在類層次劃分、OSGi、熱部署、代碼加密等領(lǐng)域大放異彩,成為了 Java 體系中一塊重要的基石。

下面我用代碼展示如何自定義一個類加載器,從網(wǎng)絡(luò)上加載一個類,然后調(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)絡(luò)上獲取類的二進(jìn)制字節(jié)流,然后讀入數(shù)組,最后通過 defineClass 方法,將其轉(zhuǎn)為 class 對象并返回。

然后通過 class 對象的 newInstance 方法獲得一個對應(yīng)的實例對象,即 RemoteClass 的對象,然后輸入其 toString 返回的內(nèi)容。具體輸出什么大家可以在本地跑了試試。

基于棧的解釋器執(zhí)行過程

在 JVM 的內(nèi)存區(qū)域劃分的時候講到過虛擬機(jī)棧這個區(qū)域,它是線程私有區(qū)域,用于描述 Java 方法的執(zhí)行模型。每個方法在執(zhí)行同時會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)、動態(tài)鏈接、方法出口等信息。接下來我就用一個實際的例子來介紹在 JVM 中,一個方法是如何被解釋執(zhí)行的。借此機(jī)會也可以更進(jìn)一步了解虛擬機(jī)棧相關(guān)知識。

運行時棧幀結(jié)構(gòu)

棧幀是用于支持 JVM 進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)棧的棧元素。每一個方法從調(diào)用到完成,都對應(yīng)著一個棧幀在虛擬機(jī)棧中的入棧和出棧操作。

每個棧幀都包括局部變量表、操作數(shù)、動態(tài)連接、方法返回地址和一些額外的附加信信息。并且在程序進(jìn)行編譯的時候,棧幀需要多大的局部變量表,多深的操作數(shù)棧都是已經(jīng)確定的,因此一個棧幀需要分配多少內(nèi)存也是確定不變的。JVM 中線程、虛擬機(jī)棧、棧幀的典型結(jié)構(gòu)圖如下:

image

解釋器執(zhí)行過程實例

本節(jié)中,我準(zhǔn)備了如下一段代碼:

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 的小伙伴對方法描述符應(yīng)該不會陌生,這里我不多介紹,想了解更多可以參考此處

接著的

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ù)棧的變化情況。

image

上圖是執(zhí)行偏移地址為 0 的指令的情況。

image

上圖是執(zhí)行偏移地址為 2 的指令的情況。

image

上圖是執(zhí)行偏移地址為 11 的指令的情況。

image

上圖是執(zhí)行偏移地址為 12 的指令的情況。

image

上圖是執(zhí)行偏移地址為 13 的指令的情況。

image

上圖是執(zhí)行偏移地址為 14 的指令的情況。

image

上圖是執(zhí)行偏移地址為 16 的指令的情況。

方法的執(zhí)行模型到這就結(jié)束了,請大家記住這僅僅是一種概念模型,虛擬機(jī)最終會對執(zhí)行過程做一些優(yōu)化來提升性能,實際的執(zhí)行過程與概念模型差距可能會很大。之所以會有這種差距是因為虛擬機(jī)的執(zhí)行引擎和即時編譯器都會對輸入的字節(jié)碼進(jìn)行優(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 虛擬機(jī)-JVM 高級特性與最佳實踐》。本文也是參考此書總結(jié)而來。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內(nèi)容