22. java虛擬機(jī)總結(jié)-從棧幀看字節(jié)碼 (五)

怎么查看字節(jié)碼文件?
字節(jié)碼文件長(zhǎng)什么樣子?
對(duì)象初始化之后,具體的字節(jié)碼又是怎么執(zhí)行的?

查看字節(jié)碼的工具

javap

javap 是 JDK 自帶的反解析工具。它的作用是將 .class 字節(jié)碼文件解析成可讀的文件格式

// -v 參數(shù),盡量多打印一些信息, -p 參數(shù),打印一些私有的字段和方法
javap -p -v HelloWorld

問題:某個(gè)類中增加一行注釋之后,兩次生成的 .class 文件,它們的 MD5 是不一樣的?答案是否定的
我們知道生成.class的是javac命令,javac 中可以指定一些額外的內(nèi)容輸出到字節(jié)碼。經(jīng)常用的有

    javac -g:lines 強(qiáng)制生成 LineNumberTable。
    javac -g:vars  強(qiáng)制生成 LocalVariableTable。
    javac -g 生成所有的 debug 信息。
jclasslib

jclasslib 是一個(gè)圖形化的工具,能夠更加直觀的查看字節(jié)碼中的內(nèi)容。jclasslib 的下載地址:https://github.com/ingokegel/jclasslib。同時(shí),在Android studio 或者 Idea 中都可以通過插件安裝,可以從 plugins 中搜索

There is a plugin for IntelliJ IDEA that can be installed via the plugin manager. The action in the "View menu" can be invoked when a Java, Groovy or Kotlin file is open in the editor.
ij_action.png

下面通過代碼,來看一下類加載和對(duì)象創(chuàng)建的過程

class B {
    private int a = 1234;

    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);
    }
}

我們知道對(duì)象的創(chuàng)建方式有以下這幾種:

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

其中,后面兩種方式?jīng)]有調(diào)用到構(gòu)造函數(shù)。

當(dāng)虛擬機(jī)遇到一條 new 指令時(shí),首先會(huì)檢查這個(gè)指令的參數(shù)能否在常量池中定位一個(gè)符號(hào)引用。然后檢查這個(gè)符號(hào)引用的類字節(jié)碼是否加載、解析和初始化。如果沒有,將執(zhí)行對(duì)應(yīng)的類加載過程。

在上邊的代碼中,在調(diào)用 private B b = new B() 時(shí),就會(huì)觸發(fā) B 類的加載


無名.jpg

A 和 B 會(huì)被加載到元空間的方法區(qū),進(jìn)入 main 方法后,將會(huì)交給執(zhí)行引擎執(zhí)行。這個(gè)執(zhí)行過程是在棧上完成的,其中有幾個(gè)重要的區(qū)域,包括虛擬機(jī)棧、程序計(jì)數(shù)器等。接下來我們?cè)敿?xì)看一下虛擬機(jī)棧上的執(zhí)行過程

查看字節(jié)碼

使用如下命令將Java文件編譯成class文件然后解析成操作碼

javac -g:lines -g:vars A.java

javap -p -v A
javap -p -v B
renzm:src renzm$ javap -p -v A
Classfile /Users/renzm/IdeaProjects/untitled/src/A.class
  Last modified 2020-7-12; size 619 bytes
  MD5 checksum 4d0a145212532396e853d26eea3d02d4
public class A
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#30        // java/lang/Object."<init>":()V
   #2 = Class              #31            // B
   #3 = Methodref          #2.#30         // B."<init>":()V
   #4 = Fieldref           #5.#32         // A.b:LB;
   #5 = Class              #33            // A
   #6 = Methodref          #5.#30         // A."<init>":()V
   #7 = Long               4321l
   #9 = Methodref          #2.#34         // B.test:(J)J
  #10 = Fieldref           #35.#36        // java/lang/System.out:Ljava/io/PrintStream;
  #11 = Methodref          #37.#38        // java/io/PrintStream.println:(J)V
  #12 = Class              #39            // java/lang/Object
  #13 = Utf8               b
  #14 = Utf8               LB;
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               LA;
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               a
  #27 = Utf8               num
  #28 = Utf8               J
  #29 = Utf8               ret
  #30 = NameAndType        #15:#16        // "<init>":()V
  #31 = Utf8               B
  #32 = NameAndType        #13:#14        // b:LB;
  #33 = Utf8               A
  #34 = NameAndType        #40:#41        // test:(J)J
  #35 = Class              #42            // java/lang/System
  #36 = NameAndType        #43:#44        // out:Ljava/io/PrintStream;
  #37 = Class              #45            // java/io/PrintStream
  #38 = NameAndType        #46:#47        // println:(J)V
  #39 = Utf8               java/lang/Object
  #40 = Utf8               test
  #41 = Utf8               (J)J
  #42 = Utf8               java/lang/System
  #43 = Utf8               out
  #44 = Utf8               Ljava/io/PrintStream;
  #45 = Utf8               java/io/PrintStream
  #46 = Utf8               println
  #47 = Utf8               (J)V
{
  private B b;
    descriptor: LB;
    flags: ACC_PRIVATE

  public A();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: new           #2                  // class B
         8: dup
         9: invokespecial #3                  // Method B."<init>":()V
        12: putfield      #4                  // Field b:LB;
        15: return
      LineNumberTable:
        line 15: 0
        line 16: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   LA;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=6, args_size=1
         0: new           #5                  // class A
         3: dup
         4: invokespecial #6                  // Method "<init>":()V
         7: astore_1
         8: ldc2_w        #7                  // long 4321l
        11: lstore_2
        12: aload_1
        13: getfield      #4                  // Field b:LB;
        16: lload_2
        17: invokevirtual #9                  // Method B.test:(J)J
        20: lstore        4
        22: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
        25: lload         4
        27: invokevirtual #11                 // Method java/io/PrintStream.println:(J)V
        30: return
      LineNumberTable:
        line 19: 0
        line 20: 8
        line 22: 12
        line 24: 22
        line 25: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1     a   LA;
           12      19     2   num   J
           22       9     4   ret   J
}


//////////////////////////////////////////////////////////////


renzm:src renzm$ javap -p -v B
Classfile /Users/renzm/IdeaProjects/untitled/src/B.class
  Last modified 2020-7-12; size 446 bytes
  MD5 checksum 1e8e7a73c9932d559c13e4ae08ee42d0
class B
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #6.#25         // B.a:I
   #3 = Fieldref           #6.#26         // B.C:J
   #4 = Long               1111l
   #6 = Class              #27            // B
   #7 = Class              #28            // java/lang/Object
   #8 = Utf8               a
   #9 = Utf8               I
  #10 = Utf8               C
  #11 = Utf8               J
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               LB;
  #19 = Utf8               test
  #20 = Utf8               (J)J
  #21 = Utf8               num
  #22 = Utf8               ret
  #23 = Utf8               <clinit>
  #24 = NameAndType        #12:#13        // "<init>":()V
  #25 = NameAndType        #8:#9          // a:I
  #26 = NameAndType        #10:#11        // C:J
  #27 = Utf8               B
  #28 = Utf8               java/lang/Object
{
  private int a;
    descriptor: I
    flags: ACC_PRIVATE

  static long C;
    descriptor: J
    flags: ACC_STATIC

  B();
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: sipush        1234
         8: putfield      #2                  // Field a:I
        11: return
      LineNumberTable:
        line 1: 0
        line 2: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   LB;

  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 7: 0
        line 8: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   LB;
            0      14     1   num   J
           12       2     3   ret   J

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: ldc2_w        #4                  // long 1111l
         3: putstatic     #3                  // Field C:J
         6: return
      LineNumberTable:
        line 4: 0
}

可視化查看字節(jié)碼

使用更加直觀的工具 jclasslib,以 B.class 文件為例,來查看它的內(nèi)容

首先,我們能夠看到 Constant Pool(常量池),這些內(nèi)容,就存放于我們的 Metaspace 區(qū)域,屬于非堆。常量池包含 .class 文件常量池、運(yùn)行時(shí)常量池、String 常量池等部分,大多是一些靜態(tài)內(nèi)容。


constant pool.png

接下來,可以看到兩個(gè)默認(rèn)的 <init> 和 <cinit> 方法。以下截圖是 test 方法的 code 區(qū)域,比命令行版的更加直觀。

methods.png

繼續(xù)往下看,我們看到了 LocalVariableTable 的三個(gè)變量。其中,slot 0 指向的是 this 關(guān)鍵字。該屬性的作用是描述幀棧中局部變量與源碼中定義的變量之間的關(guān)系。如果沒有這些信息,那么在 IDE 中引用這個(gè)方法時(shí),將無法獲取到方法名,取而代之的則是 arg0 這樣的變量名。


test.png

本地變量表的 slot 是可以復(fù)用的。注意一個(gè)有意思的地方,index 的最大值為 3,證明了本地變量表同時(shí)最多能夠存放 4 個(gè)變量。

另外,我們觀察到還有 LineNumberTable 等選項(xiàng)。該屬性的作用是描述源碼行號(hào)與字節(jié)碼行號(hào)(字節(jié)碼偏移量)之間的對(duì)應(yīng)關(guān)系,有了這些信息,在 debug 時(shí),就能夠獲取到發(fā)生異常的源代碼行號(hào)。

test 函數(shù)執(zhí)行過程
Code 區(qū)域介紹

test 函數(shù)同時(shí)使用了成員變量 a、靜態(tài)變量 C,以及輸入?yún)?shù) num。我們此時(shí)說的函數(shù)執(zhí)行,內(nèi)存其實(shí)就是在虛擬機(jī)棧上分配的。下面這些內(nèi)容,就是 test 方法的字節(jié)碼。

class B {
    private int a = 1234;
    static long C = 1111;
    public long test(long num) {
        long ret = this.a + num + C;
        return ret;
    }
}

 public long test(long);
    descriptor: (J)J
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=5, args_size=2
         //把第 1 個(gè)引用型局部變量推到操作數(shù)棧,這里的意思是把 this 裝載到了操作數(shù)棧中。
        //對(duì)于 static 方法,aload_0 表示對(duì)方法的第一個(gè)參數(shù)的操作。
         0: aload_0
        //將棧頂?shù)闹付ǖ膶?duì)象的第 2 個(gè)實(shí)例域(Field)的值,壓入棧頂。#2 就是指的我們的成員變量 a。
         1: getfield      #2                  // Field a:I
        //將棧頂 int 類型的數(shù)據(jù)轉(zhuǎn)化為 long 類型,這里就涉及我們的隱式類型轉(zhuǎn)換了。圖中的信息沒有變動(dòng),不再詳解介紹。
         4: i2l
        //將第一個(gè)局部變量入棧。也就是我們的參數(shù) num。這里的 l 表示 long,同樣用于局部變量裝載。你會(huì)看到這個(gè)位置的局部變量,一開始就已經(jīng)有值了。
         5: lload_1
         //把棧頂兩個(gè) long 型數(shù)值出棧后相加,并將結(jié)果入棧。
         6: ladd
         //根據(jù)偏移獲取靜態(tài)屬性的值,并把這個(gè)值 push 到操作數(shù)棧上。
         7: getstatic     #3                  // Field C:J
         //把棧頂兩個(gè) long 型數(shù)值出棧后相加,并將結(jié)果入棧。
        10: ladd
        //把棧頂 long 型數(shù)值存入第 4 個(gè)局部變量。
        11: lstore_3
        //正好與上面相反。上面是變量存入,我們現(xiàn)在要做的,就是把這個(gè)變量 ret,壓入虛擬機(jī)棧中。
        12: lload_3
        //返回棧頂?shù)?long
        13: lreturn
      LineNumberTable:
        line 7: 0
        line 8: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   LB;
            0      14     1   num   J
           12       2     3   ret   J

介紹一下比較重要的 3 三個(gè)數(shù)值。
1.首先,注意 stack 字樣,它此時(shí)的數(shù)值為 4,表明了 test 方法的最大操作數(shù)棧深度為 4。JVM 運(yùn)行時(shí),會(huì)根據(jù)這個(gè)數(shù)值,來分配棧幀中操作棧的深度。
2.相對(duì)應(yīng)的,locals 變量存儲(chǔ)了局部變量的存儲(chǔ)空間。它的單位是 Slot(槽),可以被重用。其中存放的內(nèi)容,包括:

this
方法參數(shù)
異常處理器的參數(shù)
方法體中定義的局部變量

3.args_size 就比較好理解。它指的是方法的參數(shù)個(gè)數(shù),因?yàn)槊總€(gè)方法都有一個(gè)隱藏參數(shù) this,所以這里的數(shù)字是 2。

字節(jié)碼執(zhí)行過程

回顧一下 JVM 運(yùn)行時(shí)的相關(guān)內(nèi)容。main 線程會(huì)擁有兩個(gè)主要的運(yùn)行時(shí)區(qū)域:Java 虛擬機(jī)棧和程序計(jì)數(shù)器。其中,虛擬機(jī)棧中的每一項(xiàng)內(nèi)容叫作棧幀,棧幀中包含四項(xiàng)內(nèi)容:局部變量報(bào)表、操作數(shù)棧、動(dòng)態(tài)鏈接和完成出口。
我們的字節(jié)碼指令,就是靠操作這些數(shù)據(jù)結(jié)構(gòu)運(yùn)行的。下面我們看一下具體的字節(jié)碼指令。


CgpOIF4ezeKAHVCXAABv7rzSgXE896.jpg
(1)0: aload_0

把第 1 個(gè)引用型局部變量推到操作數(shù)棧,這里的意思是把 this 裝載到了操作數(shù)棧中。
對(duì)于 static 方法,aload_0 表示對(duì)方法的第一個(gè)參數(shù)的操作。


CgpOIF4w-GGAA6DnAAEtqWkdOnE696.jpg
(2)1: getfield #2

將棧頂?shù)闹付ǖ膶?duì)象的第 2 個(gè)實(shí)例域(Field)的值,壓入棧頂。#2 就是指的我們的成員變量 a。


1.jpg
(3)i2l

將棧頂 int 類型的數(shù)據(jù)轉(zhuǎn)化為 long 類型,這里就涉及我們的隱式類型轉(zhuǎn)換了。圖中的信息沒有變動(dòng),不再詳解介紹。

(4)lload_1

將第一個(gè)局部變量入棧。也就是我們的參數(shù) num。這里的 l 表示 long,同樣用于局部變量裝載。你會(huì)看到這個(gè)位置的局部變量,一開始就已經(jīng)有值了。


2.jpg
(5)ladd

把棧頂兩個(gè) long 型數(shù)值出棧后相加,并將結(jié)果入棧。


4.jpg
(6)getstatic #3

根據(jù)偏移獲取靜態(tài)屬性的值,并把這個(gè)值 push 到操作數(shù)棧上。


3.jpg
(7)ladd

把棧頂兩個(gè) long 型數(shù)值出棧后相加,并將結(jié)果入棧。


5.jpg
(8)lstore_3

把棧頂 long 型數(shù)值存入第 4 個(gè)局部變量。還記得我們上面的圖么?slot 為 4,索引為 3 的就是 ret 變量。


6.jpg
(9)lload_3

正好與上面相反。上面是變量存入,我們現(xiàn)在要做的,就是把這個(gè)變量 ret,壓入虛擬機(jī)棧中。


7.jpg
(10)lreturn

從當(dāng)前方法返回 long。
到此為止,我們的函數(shù)就完成了相加動(dòng)作,執(zhí)行成功了。JVM 為我們提供了非常豐富的字節(jié)碼指令。詳細(xì)的字節(jié)碼指令列表,可以參考以下網(wǎng)址:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

注意點(diǎn)

注意上面的第 8 步,我們首先把變量存放到了變量報(bào)表,然后又拿出這個(gè)值,把它入棧。為什么會(huì)有這種多此一舉的操作?原因就在于我們定義了 ret 變量。JVM 不知道后面還會(huì)不會(huì)用到這個(gè)變量,所以只好傻瓜式的順序執(zhí)行。

為了看到這些差異。大家可以把我們的程序稍微改動(dòng)一下,直接返回這個(gè)值。

public long test(long num) {
       return this.a + num + C;
}

再次看下,對(duì)應(yīng)的字節(jié)碼指令是不是簡(jiǎn)單了很多?

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: lreturn
----------------------------------那我們以后編寫程序時(shí),是不是要盡量少的定義成員變量?--------------------------------------

這是沒有必要的。棧的操作復(fù)雜度是 O(1),對(duì)我們的程序性能幾乎沒有影響。平常的代碼編寫,還是以可讀性作為首要任務(wù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容