從棧幀看字節碼是如何在 JVM 中進行流轉的

工具介紹

工欲善其事,必先利其器。先介紹兩個分析字節碼的小工具。html

javap

第一個小工具是 javap,javap 是 JDK 自帶的反解析工具。它的做用是將 .class 字節碼文件解析成可讀的文件格式。
在使用 javap 時我通常會添加 -v 參數,儘可能多打印一些信息。同時,我也會使用 -p 參數,打印一些私有的字段和方法。使用起來大概是這樣:
javap -p -v HelloWorld
在 Stack Overflow 上有一個很是有意思的問題:我在某個類中增長一行註釋以後,爲何兩次生成的 .class 文件,它們的 MD5 是不同的?java

這是由於在 javac 中能夠指定一些額外的內容輸出到字節碼。常常用的有git

  • javac -g:lines 強制生成 LineNumberTable。
  • javac -g:vars  強制生成 LocalVariableTable。
  • javac -g 生成全部的 debug 信息。
jclasslib

若是你不太習慣使用命令行的操做,還可使用 jclasslib,jclasslib 是一個圖形化的工具,可以更加直觀的查看字節碼中的內容。它還分門別類的對類中的各個部分進行了整理,很是的人性化。同時,它還提供了 Idea 的插件,你能夠從 plugins 中搜索到它。github

若是你在其中看不到一些諸如 LocalVariableTable 的信息,記得在編譯代碼的時候加上咱們上面提到的這些參數。數組

jclasslib 的下載地址:https://github.com/ingokegel/...數據結構

類加載和對象建立的時機

接下來,咱們來看一個稍微複雜的例子,來具體看一下類加載和對象建立的過程。oracle

首先,咱們寫一個最簡單的 Java 程序 A.java。它有一個公共方法 test,還有一個靜態成員變量和動態成員變量。
class B {
    private int a = 1234;jvm

    static long C = 1111;函數

    public long test(long num) {
        long ret = this.a + num + C;
        return ret;
    }
}工具

public class A {
    private B b = new B();

    public static void main(String[] args) {
        A a = new A();
        long num = 4321 ;

        long ret = a.b.test(num);

        System.out.println(ret);
    }
}
類的初始化發生在類加載階段,那對象都有哪些建立方式呢?除了咱們經常使用的 new,還有下面這些方式:

  • 使用 Class 的 newInstance 方法。
  • 使用 Constructor 類的 newInstance 方法。
  • 反序列化。
  • 使用 Object 的 clone 方法。

其中,後面兩種方式沒有調用到構造函數。

當虛擬機遇到一條 new 指令時,首先會檢查這個指令的參數可否在常量池中定位一個符號引用。而後檢查這個符號引用的類字節碼是否加載、解析和初始化。若是沒有,將執行對應的類加載過程。

拿咱們上面的代碼來講,執行 A 代碼,在調用 private B b = new B() 時,就會觸發 B 類的加載。
CgpOIF4ezuOAK_6bAACFY5oeX-Y174.jpg
A 和 B 會被加載到元空間的方法區,進入 main 方法後,將會交給執行引擎執行。這個執行過程是在棧上完成的,其中有幾個重要的區域,包括虛擬機棧、程序計數器等。

查看字節碼

命令行查看字節碼

使用下面的命令編譯源代碼 A.java。若是你用的是 Idea,能夠直接將參數追加在 VM options 裏面。
javac -g:lines -g:vars A.java
這將強制生成 LineNumberTable 和 LocalVariableTable。

而後使用 javap 命令查看 A 和 B 的字節碼。
javap -p -v A
javap -p -v B
這個命令,不只會輸出行號、本地變量表信息、反編譯彙編代碼,還會輸出當前類用到的常量池等信息。

注意 javap 中的以下字樣。

<1>

1: invokespecial #1   // Method java/lang/Object."<init>":()V
能夠看到對象的初始化,首先是調用了 Object 類的初始化方法。注意這裏是 <init> 而不是 <cinit>。

<2>

#2 = Fieldref           #6.#27         // B.a:I
它其實直接拼接了 #13 和 #14 的內容。
#6 = Class             #29           // B
#27 = NameAndType       #8:#9         // a:I
...
#8 = Utf8               a
#9 = Utf8               I

<3>

你會注意到 :I 這樣特殊的字符。它們也是有意義的,若是你常用 jmap 這種命令,應該不會陌生。大致包括:

  • B 基本類型 byte
  • C 基本類型 char
  • D 基本類型 double
  • F 基本類型 float
  • I 基本類型 int
  • J 基本類型 long
  • S 基本類型 short
  • Z 基本類型 boolean
  • V 特殊類型 void
  • L 對象類型,以分號結尾,如 Ljava/lang/Object;
  • [Ljava/lang/String; 數組類型,每一位使用一個前置的"["字符來描述

咱們注意到 code 區域,有很是多的二進制指令。若是你接觸過彙編語言,會發現它們之間其實有必定的類似性。但這些二進制指令,並非操做系統可以認識的,它們是提供給 JVM 運行的源材料。

可視化查看字節碼

接下來,咱們就可使用更加直觀的工具 jclasslib,來查看字節碼中的具體內容了。

咱們以 B.class 文件爲例,來查看它的內容。

<1>

首先,咱們可以看到 Constant Pool(常量池),這些內容,就存放於咱們的 Metaspace 區域,屬於非堆。
Cgq2xl4ezeKAWB30AADZXqT3TjQ870.jpg
常量池包含 .class 文件常量池、運行時常量池、String 常量池等部分,大可能是一些靜態內容。

<2>

接下來,能夠看到兩個默認的 <init> 和 <cinit> 方法。如下截圖是 test 方法的 code 區域,比命令行版的更加直觀。
CgpOIF4ezeKAVmSnAACExsXdgtg544.jpg

<3>

繼續往下看,咱們看到了 LocalVariableTable 的三個變量。其中,slot 0 指向的是 this 關鍵字。該屬性的做用是描述幀棧中局部變量與源碼中定義的變量之間的關係。若是沒有這些信息,那麼在 IDE 中引用這個方法時,將沒法獲取到方法名,取而代之的則是 arg0 這樣的變量名。
Cgq2xl4ezeKASWJHAAB5Ptt1JsM137.jpg
本地變量表的 slot 是能夠複用的。注意一個有意思的地方,index 的最大值爲 3,證實了本地變量表同時最多可以存放 4 個變量。

另外,咱們觀察到還有 LineNumberTable 等選項。該屬性的做用是描述源碼行號與字節碼行號(字節碼偏移量)之間的對應關係,有了這些信息,在 debug 時,就可以獲取到發生異常的源代碼行號。

test 函數執行過程

Code 區域介紹

test 函數同時使用了成員變量 a、靜態變量 C,以及輸入參數 num。咱們此時說的函數執行,內存其實就是在虛擬機棧上分配的。下面這些內容,就是 test 方法的字節碼。
public long test(long);
   descriptor: (J)J
   flags: ACC_PUBLIC
   Code:
     stack=4, locals=5, args_size=2
        0: aload_0
        1: getfield      #2                  // Field a:I
        4: i2l
        5: lload_1
        6: ladd
        7: getstatic     #3                  // Field C:J
       10: ladd
       11: lstore_3
       12: lload_3
       13: lreturn
     LineNumberTable:
       line 13: 0
       line 14: 12
     LocalVariableTable:
       Start  Length  Slot  Name   Signature
           0      14     0  this   LB;
           0      14     1   num   J
          12       2     3   ret   J
咱們介紹一下比較重要的 3 三個數值。
<1>
首先,注意 stack 字樣,它此時的數值爲 4,代表了 test 方法的最大操做數棧深度爲 4。JVM 運行時,會根據這個數值,來分配棧幀中操做棧的深度。
<2>
相對應的,locals 變量存儲了局部變量的存儲空間。它的單位是 Slot(槽),能夠被重用。其中存放的內容,包括:

  • this
  • 方法參數
  • 異常處理器的參數
  • 方法體中定義的局部變量

<3>
args_size 就比較好理解。它指的是方法的參數個數,由於每一個方法都有一個隱藏參數 this,因此這裏的數字是 2。

字節碼執行過程

咱們稍微回顧一下 JVM 運行時的相關內容。main 線程會擁有兩個主要的運行時區域:Java 虛擬機棧和程序計數器。其中,虛擬機棧中的每一項內容叫做棧幀,棧幀中包含四項內容:局部變量報表、操做數棧、動態連接和完成出口。
咱們的字節碼指令,就是靠操做這些數據結構運行的。下面咱們看一下具體的字節碼指令。
CgpOIF4ezeKAHVCXAABv7rzSgXE896.jpg
(1)0: aload_0
把第 1 個引用型局部變量推到操做數棧,這裏的意思是把 this 裝載到了操做數棧中。
對於 static 方法,aload_0 表示對方法的第一個參數的操做。
CgpOIF4w-GGAA6DnAAEtqWkdOnE696.jpg
2)1: getfield      #2
將棧頂的指定的對象的第 2 個實例域(Field)的值,壓入棧頂。#2 就是指的咱們的成員變量 a。

#2 = Fieldref           #6.#27         // B.a:I
...
#6 = Class             #29           // B
#27 = NameAndType       #8:#9         // a:I

Cgq2xl4w-HKABrhgAAEvNAmbGWY870.jpg
(3)i2l
將棧頂 int 類型的數據轉化爲 long 類型,這裏就涉及咱們的隱式類型轉換了。圖中的信息沒有變更,再也不詳解介紹。
(4)lload_1
將第一個局部變量入棧。也就是咱們的參數 num。這裏的 l 表示 long,一樣用於局部變量裝載。你會看到這個位置的局部變量,一開始就已經有值了。
CgpOIF4w-IuAOmp0AAEzFWM0gmc155.jpg
(5)ladd
把棧頂兩個 long 型數值出棧後相加,並將結果入棧。
Cgq2xl4w-KKAGhwcAAEtNkzwpcw021.jpg
(6)getstatic #3
根據偏移獲取靜態屬性的值,並把這個值 push 到操做數棧上。
Cgq2xl4w-MWAVt_ZAAE2NxokOfU153.jpg
(7)ladd
再次執行 ladd。
CgpOIF4w-NCAaU4rAAEtel-Iskk153.jpg
(8)lstore_3
把棧頂 long 型數值存入第 4 個局部變量。
還記得咱們上面的圖麼?slot 爲 4,索引爲 3 的就是 ret 變量。
CgpOIF4w-OWAPOn9AAE1Y2sXttM659.jpg
(9)lload_3
正好與上面相反。上面是變量存入,咱們如今要作的,就是把這個變量 ret,壓入虛擬機棧中。
CgpOIF4w-O6ARdRFAAE62GkvYGo689.jpg
(10)lreturn
從當前方法返回 long。
到此爲止,咱們的函數就完成了相加動做,執行成功了。JVM 爲咱們提供了很是豐富的字節碼指令。詳細的字節碼指令列表,能夠參考如下網址:
https://docs.oracle.com/javas...

注意點注意上面的第 8 步,咱們首先把變量存放到了變量報表,而後又拿出這個值,把它入棧。爲何會有這種畫蛇添足的操做?緣由就在於咱們定義了 ret 變量。JVM 不知道後面還會不會用到這個變量,因此只好傻瓜式的順序執行。爲了看到這些差別。你們能夠把咱們的程序稍微改動一下,直接返回這個值。public long test(long num) {       return this.a + num + C;}再次看下,對應的字節碼指令是否是簡單了不少?0: aload_01: getfield     #2                 // Field a:I4: i2l5: lload_16: ladd7: getstatic     #3                 // Field C:J10: ladd11: lreturn那咱們之後編寫程序時,是否是要儘可能少的定義成員變量?這是沒有必要的。棧的操做複雜度是 O(1),對咱們的程序性能幾乎沒有影響。日常的代碼編寫,仍是以可讀性做爲首要任務。

相關文章
相關標籤/搜索