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

  1. Java 程序設計語言
  2. 各硬件平臺上的 Java 虛擬機
  3. Class 文件格式
  4. Java API 類庫
  5. 其他商業(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ū)域,具體如下圖:

image

對以上幾個區(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)存中的布局:

image

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 文件的完整格式見下表:

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ù)腻惠。

不管是多復雜的 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ā)生順序如下圖:

image

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

  • 加載:在類的加載階段 JVM 主要做了下面三件事:
  1. 通過類的全限定名來獲取定義此類的二進制字節(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ù)的訪問入口适贸。

這里需要注意第一點,它只說明了通過全限定名獲取定義此類的二進制字節(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)圖如下:

image

解釋器執(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ù)棧的變化情況蓖柔。

image

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

image

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

image

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

image

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

image

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

image

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

image

上圖是執(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é)而來荤西。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末澜搅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子邪锌,更是在濱河造成了極大的恐慌勉躺,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件觅丰,死亡現(xiàn)場離奇詭異饵溅,居然都是意外死亡,警方通過查閱死者的電腦和手機妇萄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門蜕企,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人冠句,你說我怎么就攤上這事轻掩。” “怎么了懦底?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵唇牧,是天一觀的道長。 經(jīng)常有香客問我聚唐,道長丐重,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任杆查,我火速辦了婚禮扮惦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘亲桦。我一直安慰自己崖蜜,他們只是感情好,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布客峭。 她就那樣靜靜地躺著豫领,像睡著了一般。 火紅的嫁衣襯著肌膚如雪桃笙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天沙绝,我揣著相機與錄音搏明,去河邊找鬼鼠锈。 笑死,一個胖子當著我的面吹牛星著,可吹牛的內(nèi)容都是我干的购笆。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼虚循,長吁一口氣:“原來是場噩夢啊……” “哼同欠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起横缔,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤铺遂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后茎刚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體襟锐,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年膛锭,在試婚紗的時候發(fā)現(xiàn)自己被綠了粮坞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡初狰,死狀恐怖莫杈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奢入,我是刑警寧澤筝闹,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站俊马,受9級特大地震影響丁存,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜柴我,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一解寝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧艘儒,春花似錦聋伦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至翻斟,卻和暖如春逾礁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背访惜。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工嘹履, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留腻扇,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓砾嫉,卻偏偏與公主長得像幼苛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子焕刮,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

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