從1+1=2來理解Java字節碼

背景

前不久《深刻理解Java虛擬機》第三版發佈了,趕忙買來看了看新版的內容,這本書更新了不少新版本虛擬機的內容,還對之前的部份內容進行了重構,仍是值得去看的。本着複習和鞏固的態度,我決定來編譯一個簡單的類文件來分析Java的字節碼內容,來幫助理解和鞏固Java字節碼知識,但願也對閱讀本文的你有所幫助。java

說明:本次採用的環境是OpenJdk12python

編譯「1+1」代碼

首先咱們須要寫個簡單的小程序,1+1的程序,學習就要從最簡單的1+1開始,代碼以下:小程序

package top.luozhou.test;

/**
 * @description:
 * @author: luozhou
 * @create: 2019-12-25 21:28
 **/
public class TestJava {
    public static void main(String[] args) {
        int a=1+1;
        System.out.println(a);
    }
}

複製代碼

寫好java類文件後,首先執行命令javac TestJava.java 編譯類文件,生成TestJava.class。 而後執行反編譯命令javap -verbose TestJava,字節碼結果顯示以下:bash

Compiled from "TestJava.java"
public class top.luozhou.test.TestJava
  minor version: 0
  major version: 56
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref #5.#14 // java/lang/Object."<init>":()V
   #2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
   #4 = Class #19 // top/luozhou/test/TestJava
   #5 = Class #20 // java/lang/Object
   #6 = Utf8 <init>
   #7 = Utf8 ()V
   #8 = Utf8 Code
   #9 = Utf8 LineNumberTable
  #10 = Utf8 main
  #11 = Utf8 ([Ljava/lang/String;)V
  #12 = Utf8 SourceFile
  #13 = Utf8 TestJava.java
  #14 = NameAndType #6:#7 // "<init>":()V
  #15 = Class #21 // java/lang/System
  #16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
  #17 = Class #24 // java/io/PrintStream
  #18 = NameAndType #25:#26 // println:(I)V
  #19 = Utf8 top/luozhou/test/TestJava
  #20 = Utf8 java/lang/Object
  #21 = Utf8 java/lang/System
  #22 = Utf8 out
  #23 = Utf8 Ljava/io/PrintStream;
  #24 = Utf8 java/io/PrintStream
  #25 = Utf8 println
  #26 = Utf8 (I)V
{
  public top.luozhou.test.TestJava();
    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 8: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_2
         1: istore_1
         2: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
         5: iload_1
         6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 10: 0
        line 11: 2
        line 12: 9
}
複製代碼

解析字節碼

1.基礎信息學習

上述結果刪除了部分不影響解析的冗餘信息,接下來咱們便來解析字節碼的結果。優化

minor version: 0 次版本號,爲0表示未使用
 major version: 56 主版本號,56表示jdk12,表示只能運行在jdk12版本以及以後的虛擬機中
複製代碼
flags: ACC_PUBLIC, ACC_SUPER
複製代碼

ACC_PUBLIC:這就是一個是不是public類型的訪問標誌。ui

ACC_SUPER: 這個falg是爲了解決經過 invokespecial 指令調用 super 方法的問題。能夠將它理解成 Java 1.0.2 的一個缺陷補丁,只有經過這樣它才能正確找到 super 類方法。從 Java 1.0.2 開始,編譯器始終會在字節碼中生成 ACC_SUPER 訪問標識。感興趣的同窗能夠點擊這裏來了解更多。this

2.常量池編碼

接下來,咱們將要分析常量池,你也能夠對照上面總體的字節碼來理解。spa

#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
複製代碼

這是一個方法引用,這裏的#5表示索引值,而後咱們能夠發現索引值爲5的字節碼以下

#5 = Class #20 // java/lang/Object
複製代碼

它表示這是一個Object類,同理#14指向的是一個"<init>":()V表示引用的是初始化方法。

#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
複製代碼

上面這段表示是一個字段引用,一樣引用了#15#16,實際上引用的就是java/lang/System類中的PrintStream對象。其餘的常量池分析思路是同樣的,鑑於篇幅我就不一一說明了,只列下其中的幾個關鍵類型和信息。

NameAndType:這個表示是名稱和類型的常量表,能夠指向方法名稱或者字段的索引,在上面的字節碼中都是表示的實際的方法。

Utf8咱們常用的是字符編碼,可是這個不是隻有字符編碼的意思,它表示一種字符編碼是Utf8的字符串。它是虛擬機中最經常使用的表結構,你能夠理解爲它能夠描述方法,字段,類等信息。 好比:

#4 = Class #19 
#19 = Utf8 top/luozhou/test/TestJava
複製代碼

這裏表示#4這個索引下是一個類,而後指向的類是#19,#19是一個Utf8表,最終存放的是top/luozhou/test/TestJava,那麼這樣一鏈接起來就能夠知道#4位置引用的類是top/luozhou/test/TestJava了。

3.構造方法信息

接下來,咱們分析下構造方法的字節碼,咱們知道,一個類初始化的時候最早執行它的構造方法,若是你沒有寫構造方法,系統會默認給你添加一個無參的構造方法。

public top.luozhou.test.TestJava();
    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 8: 0
複製代碼

descriptor: ()V :表示這是一個沒有返回值的方法。

flags: ACC_PUBLIC:是公共方法。

stack=1, locals=1, args_size=1 :表示棧中的數量爲1,局部變量表中的變量爲1,調用參數也爲1。

這裏爲何都是1呢?這不是默認的構造方法嗎?哪來的參數?其實Java語言有一個潛規則:在任何實例方法裏面均可以經過this來訪問到此方法所屬的對象。而這種機制的實現就是經過Java編譯器在編譯的時候做爲入參傳入到方法中了,熟悉python語言的同窗確定會知道,在python中定義一個方法總會傳入一個self的參數,這也是傳入此實例的引用到方法內部,Java只是把這種機制後推到編譯階段完成而已。因此,這裏的1都是指this這個參數而已。

0: aload_0
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         4: return
    LineNumberTable:
        line 8: 0
複製代碼

通過上面這個分析對於這個構造方法表達的意思也就很清晰了。

aload_0:表示把局部變量表中的第一個變量加載到棧中,也就是this

invokespecial:直接調用初始化方法。

return:調用完畢方法結束。

LineNumberTable:這是一個行數的表,用來記錄字節碼的偏移量和代碼行數的映射關係。line 8: 0表示,源碼中第8行對應的就是偏移量0的字節碼,由於是默認的構造方法,因此這裏並沒有法直觀體現出來。

另外這裏會執行Object的構造方法是由於,Object是全部類的父類,子類的構造要先構造父類的構造方法。

4.main方法信息

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_2
         1: istore_1
         2: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
         5: iload_1
         6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 10: 0
        line 11: 2
        line 12: 9
複製代碼

有了以前構造方法的分析,咱們接下來分析main方法也會熟悉不少,重複的我就略過了,這裏重點分析code部分。

stack=2, locals=2, args_size=1:這裏的棧和局部變量表爲2,參數仍是爲1。這是爲何呢?由於main方法中聲明瞭一個變量a,因此局部變量表要加一個,棧也是,因此他們是2。那爲何args_size仍是1呢?你不是說默認會把this傳入的嗎?應該是2啊。注意:以前說的是在任何實例方法中,而這個main方法是一個靜態方法,靜態方法直接能夠經過類+方法名訪問,並不須要實例對象,因此這裏就不必傳入了

0: iconst_2:將int類型2推送到棧頂。

1: istore_1:將棧頂int類型數值存入第二個本地變量。

2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;:獲取PrintStream類。

5: iload_1: 把第二個int型本地變量推送到棧頂。

6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V:調用println方法。這裏的println方法就會把棧頂的元素做爲本身的入參來執行,最終也會輸出2。

9: return:調用完畢結束方法。

這裏的LineNumberTable是有源碼的,咱們能夠對照下我前面描述是否正確:

line 10: 0: 第10行表示0: iconst_2字節碼,這裏咱們發現編譯器直接給咱們計算好了把2推送到棧頂了。

line 11: 2:第11行源碼對應的是2: getstatic 獲取輸出的靜態類PrintStream

line 12: 9:12行源碼對應的是return,表示方法結束。

這裏我也畫了一個動態圖片來演示main方法執行的過程,但願可以幫助你理解:

總結

這篇文章我從1+1的的源碼編譯開始,分析了生成後的Java字節碼,包括類的基本信息,常量池,方法調用過程等,經過這些分析,咱們對Java字節碼有了比較基本的瞭解,也知道了Java編譯器會把優化手段經過編譯好的字節碼體現出來,好比咱們的1+1=2,字節碼字節賦值一個2給變量,而不是進行加法運算,從而優化了咱們的代碼,提搞了執行效率。

參考

  1. bugs.openjdk.java.net/browse/JDK-…
相關文章
相關標籤/搜索