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

2021-01-07 咔咔侃技術

背景

前不久《深入理解Java虛擬機》第三版發布了,趕緊買來看了看新版的內容,這本書更新了很多新版本虛擬機的內容,還對以前的部分內容進行了重構,還是值得去看的。本著複習和鞏固的態度,我決定來編譯一個簡單的類文件來分析Java的字節碼內容,來幫助理解和鞏固Java字節碼知識,希望也對閱讀本文的你有所幫助。

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

編譯「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,字節碼結果顯示如下:

Compiled from "TestJava.java"public class top.luozhou.test.TestJava minor version: 0 major version: 56 flags: ACC_PUBLIC, ACC_SUPERConstant 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_SUPERACC_PUBLIC:這就是一個是否是public類型的訪問標誌。

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

2.常量池

接下來,我們將要分析常量池,你也可以對照上面整體的字節碼來理解。

#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: 0descriptor: ()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方法。

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給變量,而不是進行加法運算,從而優化了我們的代碼,提搞了執行效率。

轉自:https://my.oschina.net/luozhou/blog/3150191

相關焦點

  • 一文讓你明白 Java 字節碼
    Java號稱是一門「一次編譯到處運行」的語言,但是我們對這句話的理解深度又有多少呢?從我們寫的java文件到通過編譯器編譯成java字節碼文件(也就是.class文件),這個過程是java編譯過程;而我們的java虛擬機執行的就是字節碼文件。不論該字節碼文件來自何方,由哪種編譯器編譯,甚至是手寫字節碼文件,只要符合java虛擬機的規範,那麼它就能夠執行該字節碼文件。
  • 這一次,徹底弄懂 Java 字節碼文件!
    : "MyTest1.java」Java字節碼十六進位Mac作業系統下建議使用 Hex Fiend 工具查看 MyTest1.class 文件的十六進位格式。Java字節碼整體結構如下圖所示,以下圖示以不同緯度展示了字節碼結構中所包含的關鍵內容。Java字節碼整體結構圖:完整的Java字節碼結構圖:接下來結合十六進位格式的 class 文件,參照 Java字節碼文件來剖析下都包含了哪些內容。
  • Java默認構造方法在字節碼的實現
    接著8個字節」00 02 00 01 00 00 00 0A「分表表示Code結構操作數棧最大值、局部變量所需存儲空間、代碼長度,結果分別輸2、1、10;10表示接下來有10個字節碼指令。字節碼指令由一個字節組成,所以最多只能有256條指令,具體每個值對應操作指令在Java虛擬機規範可以查到。
  • 「JAVA」萬字長篇詳述字節碼對象與反射機制完成動態編程
    1.類的加載類的加載是指將類的class文件(字節碼文件)載入JVM內存中,並為之創建一個java.lang.Class對象,也就是字節碼對象。類的加載過程由類加載器(ClassLoader)完成,類加載器由JVM提供,我們稱之為系統類加載器,同時,我們也可以繼承ClassLoader類來提供自定義類加載器。
  • 深入理解JAVA虛擬機學習筆記19——字節碼指令4(其他指令)
    指令這塊的相關信息我們在前幾篇的文章中已經有了基本了解,這一篇不過多解釋了,將剩下的指令列出來,作為記錄(節選自《深入理解JAVA虛擬機》)。1、對象創建與訪問指令JVM對類實例和數組創建和操作使用了不同的字節碼指令,包括:創建類實例的指令:new創建數組的指令:newarray , anewarray , multianewarray訪問類欄位(static 欄位,或者稱為類變量)和實例欄位(非static 欄位,或者稱為實例變量)的指令:getfield , putfield ,
  • 字節碼層面理解try、catch、finally
    面試中經常有關於try、catch、finally相關的問題,今天從字節碼層面了解他們的運行流程。在這裡我們通過jclasslib查看編譯後的字節碼,並找到方法的字節碼指令,如下圖:右邊被圈中的就是test()方法執行的字節碼指令,字節碼指令較長,接下來一部分一部分的分析。
  • Java【IO系列】基礎篇—1. IO框架
    當時學習的節奏是,首先了解io的相關介紹,再查看io的使用示例,最後才是打開API參考文檔並參考文檔通過寫demo來學習。結果,當時是了解了API的使用方法;可是一段時間之後,只能零零碎碎的記得一些片段,再要自己講出個所以然來,已經非常之艱難。之所以造成這種困境,我想主要是:一,學習的時候,理解的不夠徹底,沒有真正理解它的思想和框架。二,加之後來使用的較少,加速了遺忘的速度。
  • Java動態字節技術之Javassist
    概述Javassist是一個開源的分析、編輯和創建Java字節碼的類庫,可以直接編輯和生成Java生成的字節碼。相對於bcel, asm等這些工具,開發者不需要了解虛擬機指令,就能動態改變類的結構,或者動態生成類。javassist簡單易用, 快速。
  • Java典型面試題 ——談談你對Java平臺的理解?
    我們開發的 Java 的原始碼,首先通過 Javac 編譯成為字節碼(bytecode),然後,在運行時,通過 Java 虛擬機(JVM)內嵌的解釋器將字節碼轉換成為最終的機器碼。這裡說的 Java 的編譯和 C/C++ 是有著不同的意義的,Javac 的編譯,編譯 Java 源碼生成「.class」文件裡面實際是字節碼,而不是可以直接執行的機器碼。Java 通過字節碼和 Java 虛擬機(JVM)這種跨平臺的抽象,屏蔽了作業系統和硬體的細節,這也是實現「一次編譯,到處執行」的基礎。
  • Java之字節輸入流InputStream的簡單介紹
    java.io.InputStream:字節輸入流,此抽象類是表示字節輸入流的所有類的超類。定義了所有子類共性的方法:1.int read()從輸入流中讀取數據的下一個字節。java.io.FileInputStream extends InputStream,FileInputStream:文件字節輸入流,作用:把硬碟文件中的數據,讀取到內存中使用。
  • JVM之用Java解析class文件
    《深入理解java虛擬機》中花了一整個章節來講解Class文件,可是看完後,一直都還是迷迷糊糊,似懂非懂。正好前段時間看見一本書很不錯:《自己動手寫Java虛擬機》,作者利用go語言實現了一個簡單的JVM,雖然沒有完整實現JVM的所有功能,但是對於一些對JVM稍感興趣的人來說,可讀性還是很高的。
  • Java反射機制深入詳解
    1 Person p1 = new Person(); 2 //下面的這三種方式都可以得到字節碼 3 CLass c1 = Date.class(); 4 p1.getClass(); 5 //若存在則加載,否則新建,往往使用第三種,類的名字在寫源程序時不需要知道,到運行時再傳遞過來 6 Class.forName("java.lang.String");CLass.forName
  • Java程式設計師必備基礎:Java代碼是怎麼運行的?
    最近複習了深入理解Java虛擬機,做了一下總結,希望對大家有幫助,如果有不正確的地方,歡迎提出,感激不盡。java源文件編譯為class字節碼 類加載器把字節碼加載到虛擬機的方法區。
  • JAVA字節碼的應用實例,長見識了
    雖然許多字節碼很簡單,但上面的上半行中的字節很複雜且具體到Java。字節碼的長度是一到五字節,因此它們的名字Java助記符使用前綴是32位是整數,A為參考地址,S為16位整數(短),和乙8位字節。我們使用是對於一個16位常量的8位常數和II6。
  • 你有真正理解 Java 的類加載機制嗎?|原力計劃
    在我們代碼中,我們只知道有一個構造方法,但實際上Java代碼編譯成字節碼之後,最開始是沒有構造方法的概念的,只有類初始化方法 和 對象初始化方法 。這個時候我們就不得不深入理解了!那麼這兩個方法是怎麼來的呢?
  • Java編譯期與運行期,別傻傻分不清楚!
    想通過這篇文章來分析分析Java的執行流程,或者換句話說想聊聊Java的編譯期與運行期的流程。1. 開門見山2.5 字節碼生成字節碼生成是Javac編譯過程的最後一個階段,在Javac源碼裡面由com.sun.tools.javac. jvm.Gen類來完成。字節碼生成階段前面各個步驟所生成的信息(語法樹、符號表)轉化成字節碼寫到磁碟中,主要工作就是把語法樹和符號表加工成字節碼文件。3. 運行期間都做了什麼?
  • 深入JAVA 字節碼驗證 for 循環中 list.size()是否會重複調用?
    最近有朋友討論說for循環裡不要用list.size(),這樣會導致每次都調用一次list.size()接口會耗性能,建議我們在外面使用一個變量來存儲這個list.size()的值再放到循環裡面,真的是這樣嗎?
  • java class loader
    當你需要用java語言進行開發時,了解java類加載如何工作是很有幫助的。對類加載過程有基本的理解可以幫助Java程式設計師處理多種ClassLoader相關的異常。類加載器委派模型 java類的加載由類加載器(CL)來執行,CL負責將類載入到JVM中。簡單的應用程式可以利用java平臺自帶的類加載工具來進行載入,而更複雜的應用傾向於定義自己的CL。java中所有的CL被組織成樹結構。
  • Java 數據持久化系列之 HikariCP
    但是在這個基礎之上,HikariCP 優化並精簡了生成的字節碼,提高了性能。HikariCP 使用 Java 字節碼修改類庫 Javassist 來生成委託實現動態代理。動態代理的實現在 ProxyFactory 類。
  • 深入理解Java虛擬機:類加載機制
    三、類加載的過程1、加載加載(Loading)階段是整個類加載過程中的一個階段。加載階段需要完成的事通過一個類的全限定名來獲取定義此類的二進位字節流。將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。