引言
在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程序运行如飞。敬请期待!