引言
在Java的世界里,JVM(Java虛擬機)是我們程序運行的心臟。而字節碼,作為JVM的血液,攜帶著程序的執行指令。今天,我們將深入探索Java字節碼的奧秘,一窺JVM如何將人類可讀的代碼轉化為機器可執行的指令。
一、JVM 知識回顧
JVM是一個可以執行Java字節碼的虛擬計算機,它包括類加載器、運行時數據區、執行引擎等核心組件。JVM架構保證了Java程序的平臺獨立性,實現了“一次編寫,到處運行”的理念。
關于Java虛擬機的工作原理、作用和應用場景, 感興趣的朋友請前往查看:Java虛擬機揭秘-底層驅動力,性能保障!
二、Class文件結構
Class文件是JVM執行字節碼的載體,它以8位字節為基礎單位的二進制流形式存在。Class文件的結構包括魔數、版本號、常量池、字段表集合、方法表集合等。
下面以一個例子逐步講解字節碼。
//Test.java
public class Test {private int a;public int num() {return a + 1;}
}
通過執行以下命令, 可以在當前所在路徑下生成一個Test.class文件。
javac Test.java
以文本形式打開Test.class文件,內容如下:
cafe babe 0000 003d 0013 0a00 0200 0307
0004 0c00 0500 0601 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 063c 696e
6974 3e01 0003 2829 5609 0008 0009 0700
0a0c 000b 000c 0100 0454 6573 7401 0001
6101 0001 4901 0004 436f 6465 0100 0f4c
696e 654e 756d 6265 7254 6162 6c65 0100
036e 756d 0100 0328 2949 0100 0a53 6f75
7263 6546 696c 6501 0009 5465 7374 2e6a
6176 6100 2100 0800 0200 0000 0100 0200
0b00 0c00 0000 0200 0100 0500 0600 0100
0d00 0000 1d00 0100 0100 0000 052a b700
01b1 0000 0001 000e 0000 0006 0001 0000
0002 0001 000f 0010 0001 000d 0000 001f
0002 0001 0000 0007 2ab4 0007 0460 ac00
0000 0100 0e00 0000 0600 0100 0000 0700
0100 1100 0000 0200 12
-
文件開頭的4個字節(“cafe babe”)稱之為
魔數
,唯有以"cafe babe"開頭的class文件方可被虛擬機所接受,這4個字節就是字節碼文件的身份識別。 -
0000是編譯器jdk版本的次版本號0
三、反編譯字節碼文件
使用 javap
命令反編譯 Java 字節碼文件是一個查看 .class
文件內部結構和字節碼指令的簡單方法。
用法: javap <options> <classes>
其中<options>
選項包括:
-help --help -? 輸出此用法消息-version 版本信息-v -verbose 輸出附加信息-l 輸出行號和本地變量表-public 僅顯示公共類和成員-protected 顯示受保護的/公共類和成員-package 顯示程序包/受保護的/公共類和成員 (默認)-p -private 顯示所有類和成員-c 對代碼進行反匯編-s 輸出內部類型簽名-sysinfo 顯示正在處理的類的系統信息 (路徑, 大小, 日期, MD5 散列)-constants 顯示最終常量-classpath <path> 指定查找用戶類文件的位置-cp <path> 指定查找用戶類文件的位置-bootclasspath <path> 覆蓋引導類文件的位置
以下是如何使用 javap
命令的基本步驟:
- 第一步,編譯 Java 源文件: 首先,你需要有一個
.class
文件。如果你有一個.java
源文件,可以使用javac
命令將其編譯成.class
文件。例如:
javac Test.java
這將在當前目錄下生成一個名為 Test.class
的字節碼文件。
-
第二步,使用 javap 命令: 打開命令行工具(在 Windows 上是 CMD 或 PowerShell,在 macOS 或 Linux 上是 Terminal),然后使用
javap
命令查看.class
文件的內容。可以使用:
javap -c Test
-
第三步,查看輸出: 執行
javap
命令后,你將看到.class
文件的詳細信息,包括類定義、字段、方法以及它們的字節碼指令。
- 第四步,輸出到文件: 如果你想要將
javap
的輸出保存到文件中,可以使用重定向操作符>
。
javap -c Test > output.txt
這將把 Test.class
文件的字節碼指令輸出到 output.txt
文件中。
請注意,javap
主要用于查看字節碼指令和類的結構,而不是將 .class
文件完全反編譯回 Java 源代碼。
如果你需要將 .class
文件反編譯為更接近原始源代碼的形式,可能需要使用其他專門的反編譯工具。
四、字節碼文件.class
詳細解讀
執行以下命令,可查看子節碼文件內容:
javap -verbose -p Test.class
Last modified 2024年5月26日; size 265 bytesSHA-256 checksum aeba5b65f486bc4f6ee16ec2073e4fec2053987d53cdaed540343c6230966095Compiled from "Test.java"
public class Testminor version: 0major version: 61flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #8 // Testsuper_class: #2 // java/lang/Objectinterfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:#1 = Methodref #2.#3 // java/lang/Object."<init>":()V#2 = Class #4 // java/lang/Object#3 = NameAndType #5:#6 // "<init>":()V#4 = Utf8 java/lang/Object#5 = Utf8 <init>#6 = Utf8 ()V#7 = Fieldref #8.#9 // Test.a:I#8 = Class #10 // Test#9 = NameAndType #11:#12 // a:I#10 = Utf8 Test#11 = Utf8 a#12 = Utf8 I#13 = Utf8 Code#14 = Utf8 LineNumberTable#15 = Utf8 num#16 = Utf8 ()I#17 = Utf8 SourceFile#18 = Utf8 Test.java
{private int a;descriptor: Iflags: (0x0002) ACC_PRIVATEpublic Test();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 2: 0public int num();descriptor: ()Iflags: (0x0001) ACC_PUBLICCode:stack=2, locals=1, args_size=10: aload_01: getfield #7 // Field a:I4: iconst_15: iadd6: ireturnLineNumberTable:line 7: 0
}
SourceFile: "Test.java"
以上信息是使用 javap
命令反編譯 Test.class
文件后得到的輸出結果。它展示了 .class
文件的內部結構和相關信息。
下面是對輸出結果的詳細解析:
(1)、文件元信息
-
Last modified
:文件最后修改時間。 -
size
:文件大小。 -
SHA-256 checksum
:文件的 SHA-256 校驗和。
(2)、編譯信息
-
Compiled from "Test.java"
:表明Test.class
文件是由Test.java
源文件編譯而來。
(3)、類定義
-
public class Test
:定義了一個名為Test
的公共類。
(4)、版本信息
-
minor version: 0
和major version: 61
:表示這個.class
文件的 Java 版本信息,主版本號是 61,這對應于 Java 17。
(5)、訪問標志
-
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
:類的標志,ACC_PUBLIC
表示這個類是公開的,ACC_SUPER
是一個默認標志,表示這個類不是final
的。
訪問標志的含義如下:
標志名稱 標志值 含義 ACC_PUBLIC 0x0001 是否為Public類型 ACC_FINAL 0x0010 是否被聲明為final,只有類可以設置 ACC_SUPER 0x0020 是否允許使用invokespecial字節碼指令的新語義. ACC_INTERFACE 0x0200 標志這是一個接口 ACC_ABSTRACT 0x0400 是否為abstract類型,對于接口或者抽象類來說,次標志值為真,其他類型為假 ACC_SYNTHETIC 0x1000 標志這個類并非由用戶代碼產生 ACC_ANNOTATION 0x2000 標志這是一個注解 ACC_ENUM 0x4000 標志這是一個枚舉
(6)、常量池
Constant pool
-
常量池可以理解成Class文件中的資源倉庫,包含了類文件中的各種常量引用,例如方法引用、類名、字段名等。
-
常量池主要存放的是兩大類常量:字面量(Literal)和符號引用(Symbolic References)。
-
字面量類似于java中的常量概念,如文本字符串,final常量等。
-
符號引用則屬于編譯原理方面的概念,包括以下三種:
-
類和接口的全限定名(Fully Qualified Name)
-
字段的名稱和描述符號(Descriptor)
-
方法的名稱和描述符
-
JVM是在加載Class文件的時候才進行的動態鏈接,也就是說這些字段和方法符號引用只有在運行期轉換后才能獲得真正的內存入口地址。當虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建或運行時解析并翻譯到具體的內存地址中。
例如:
-
#1 = Methodref #2.#3
:引用了java/lang/Object
類的無參構造方法<init>()V
。 -
#7 = Fieldref #8.#9
:引用了Test
類中的字段a
。
(7)、字段
private int a;
:定義了一個名為a
的私有整型字段。
(8)、構造方法
-
public Test();
:定義了一個公共的無參構造方法,用于實例化Test
類的對象。 -
Code
:構造方法的字節碼指令,這里調用了父類Object
的構造方法。
Code:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 2: 0
code內的主要屬性為:
-
stack: 最大操作數棧,JVM運行時會根據這個值來分配棧幀(Frame)中的操作棧深度,此處為1。
-
locals: 局部變量所需的存儲空間,單位為Slot, Slot是虛擬機為局部變量分配內存時所使用的最小單位,為4個字節大小。方法參數(包括實例方法中的隱藏參數this),顯示異常處理器的參數(try catch中的catch塊所定義的異常),方法體中定義的局部變量都需要使用局部變量表來存放。值得一提的是,locals的大小并不一定等于所有局部變量所占的Slot之和,因為局部變量中的Slot是可以重用的。
-
args_size: 方法參數的個數,這里是1,因為每個實例方法都會有一個隱藏參數this.
-
LineNumberTable: 該屬性的作用是描述源碼行號與字節碼行號(字節碼偏移量)之間的對應關系。可以使用 -g:none 或-g:lines選項來取消或要求生成這項信息,如果選擇不生成LineNumberTable,當程序運行異常時將無法獲取到發生異常的源碼行號,也無法按照源碼的行數來調試程序。
-
(9)、方法
-
public int num();
:定義了一個名為num
的公共方法,返回類型為int
。 -
Code
:num
方法的字節碼指令,它讀取字段a
的值,加 1,然后返回結果。
-
descriptor: I
類型為I, I即是int類型,關于字節碼的類型對應如下:
標識字符 | 含義 |
---|---|
B | 基本類型byte |
C | 基本類型char |
D | 基本類型double |
F | 基本類型float |
I | 基本類型int |
J | 基本類型long |
S | 基本類型short |
Z | 基本類型boolean |
V | 特殊類型void |
L | 對象類型,以分號結尾,如Ljava/lang/Object; |
(10)、行號表
-
LineNumberTable
:提供了源代碼行號和字節碼指令之間的映射。
(11)、源文件
SourceFile: "Test.java"
:指示這個.class
文件是由Test.java
源文件編譯而來的。
這個輸出結果提供了 Test.class
文件的詳細內部結構,包括字段、方法、訪問控制、版本信息等。通過這些信息,可以更好地理解 Java 類的編譯過程和字節碼的細節。
五、分析try-catch-finally
public class TestCode {public int foo() {int x;try {x = 1;return x;} catch (Exception e) {x = 2;return x;} finally {x = 3;}}
}
執行命令:
javac TestCode.java
javap -verbose -p TestCode.class
內容如下:
Last modified 2024年5月26日; size 418 bytesSHA-256 checksum 0d58e986cce436cf5dd634bcfabb39b75afaddaa863d596c1e874f3571832952Compiled from "TestCode.java"
public class TestCodeminor version: 0major version: 61flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #9 // TestCodesuper_class: #2 // java/lang/Objectinterfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:#1 = Methodref #2.#3 // java/lang/Object."<init>":()V#2 = Class #4 // java/lang/Object#3 = NameAndType #5:#6 // "<init>":()V#4 = Utf8 java/lang/Object#5 = Utf8 <init>#6 = Utf8 ()V#7 = Class #8 // java/lang/Exception#8 = Utf8 java/lang/Exception#9 = Class #10 // TestCode#10 = Utf8 TestCode#11 = Utf8 Code#12 = Utf8 LineNumberTable#13 = Utf8 foo#14 = Utf8 ()I#15 = Utf8 StackMapTable#16 = Class #17 // java/lang/Throwable#17 = Utf8 java/lang/Throwable#18 = Utf8 SourceFile#19 = Utf8 TestCode.java
{public TestCode();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0public int foo();descriptor: ()Iflags: (0x0001) ACC_PUBLICCode:stack=1, locals=5, args_size=10: iconst_11: istore_12: iload_13: istore_24: iconst_35: istore_16: iload_27: ireturn8: astore_29: iconst_210: istore_111: iload_112: istore_313: iconst_314: istore_115: iload_316: ireturn17: astore 419: iconst_320: istore_121: aload 423: athrowException table:from to target type0 4 8 Class java/lang/Exception0 4 17 any8 13 17 any17 19 17 anyLineNumberTable:line 5: 0line 6: 2line 11: 4line 6: 6line 7: 8line 8: 9line 9: 11line 11: 13line 9: 15line 11: 17line 12: 21StackMapTable: number_of_entries = 2frame_type = 72 /* same_locals_1_stack_item */stack = [ class java/lang/Exception ]frame_type = 72 /* same_locals_1_stack_item */stack = [ class java/lang/Throwable ]
}
SourceFile: "TestCode.java"
我們重點解讀名為 foo
的 Java 方法的字節碼信息。
下面是對這個輸出的詳細解析:
- 方法簽名:
public int foo();
:定義了一個名為foo
的公共方法,它返回一個整數值。
- 方法描述符:
descriptor: ()I
:表示這個方法沒有參數,并返回一個int
類型的值。
- 訪問標志:
flags: (0x0001) ACC_PUBLIC
:表示這個方法是公開的。
- 字節碼指令:
Code
:包含了實際執行的方法體的字節碼指令。stack=1, locals=5, args_size=1
:表示這個方法在執行時,操作數棧的最大深度是 1,局部變量表的大小是 5(包括this
指針和方法參數),參數大小是 1(對于實例方法,this
指針算作第一個參數)。
- 字節碼指令詳解:
iconst_1
:將整數 1 推送到棧上。istore_1
:將棧頂的整數值存儲到局部變量 1 中。iload_1
:從局部變量 1 中加載整數值到棧上。istore_2
:將棧頂的整數值存儲到局部變量 2 中。iconst_3
:將整數 3 推送到棧上。istore_1
:將棧頂的整數值存儲到局部變量 1 中。iload_2
:從局部變量 2 中加載整數值到棧上。ireturn
:將棧頂的整數值作為返回值結束方法。astore_2
:將棧頂的引用類型值存儲到局部變量 2 中。iconst_2
、istore_1
、istore_3
、aload
、athrow
:這些指令涉及到異常處理,athrow
指令會拋出異常。
- 異常表:
Exception table
:列出了方法中可能拋出的異常及其處理程序的位置。from
、to
、target
、type
:分別表示異常發生的起始指令、結束指令、跳轉目標指令和異常類型。
- 行號表:
LineNumberTable
:提供了源代碼行號和字節碼指令之間的映射,方便調試。
- StackMapTable:
StackMapTable
:在 JDK 7 及以后版本中,用于替代之前的Exceptions
屬性和LineNumberTable
,提供了更詳細的棧映射信息,用于支持新的異常表和行號信息。
- 源文件:
SourceFile
:指示這個.class
文件是由哪個.java
源文件編譯而來的。
這個輸出結果提供了 foo
方法的字節碼指令和相關元信息,包括方法的訪問權限、返回類型、局部變量和操作數棧的使用情況、異常處理以及源代碼行號映射等。通過這些信息,可以深入理解 Java 方法的執行細節和異常處理機制。
六、結語
字節碼不僅支持Java語言,還支持所有編譯到字節碼的JVM語言,如Groovy、Scala、Kotlin等。此外,字節碼層面的優化可以顯著提高程序性能。
字節碼是Java程序的靈魂,掌握了字節碼,就掌握了程序性能的鑰匙。本文深入探討了Java字節碼的內部結構和工作原理,然而,字節碼的世界遠比我們所見的要深邃。
在下一篇文章中,我們將揭開JVM調優的神秘面紗,探索如何通過字節碼優化讓Java程序運行如飛。敬請期待!