简单认识 java 字节码 --- bytecode
我们在最初学习java的时候,都会使用记事本写一个hello world,然后使用javac编译成class文件,最后使用java命令执行编译后的文件。
进一步学习我们了解到java代码和java虚拟机是两回事,虚拟机能接受的是字节码,java代码编译后的东西是字节码,但字节码也可以是别的语言编译出来的。
通过对字节码的学习,我们可以进一步了解java代码的执行原理,了解各种语法糖的实现机制。当然实际我们常用的spring框架本身也是通过字节码增强技术实现各种功能。
现在我们进一步了解字节码相关的内容,编写了如下测试代码
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
Test hello = new Test("hello");
hello.method();
Test.staticMethod();
}
public static class Test {
private String text;
public Test(String text) {
this.text = text;
}
{
System.out.println("{}");
}
static {
System.out.println("static");
}
public static void staticMethod() {
System.out.println("staticMethod");
}
public void method() {
System.out.println(text);
}
}
}
此代码包含了入口点函数main,静态内部类Test,实例方法调用
,代码块,静态代码块,构造函数,默认构造函数。method
`,静态方法调用
`staticMethod
我们使用javac编译源码,得到了2个类的class文件
我们先使用javap查看内部类Test的字节码,因为内容比较长,我将分段贴出来并解释内容的含义。
PS E:\Data\Desktop\tencent2\untitled\out\production\untitled> javap -v '.\Main$Test.class'
Classfile /E:/Data/Desktop/tencent2/untitled/out/production/untitled/Main$Test.class
Last modified 2024年2月2日; size 757 bytes
SHA-256 checksum 4b8c20d77e222d8522b84daf68753d7c1786e78fc45b80bb2b5a3e4ebe93c7f4
Compiled from "Main.java"
此段输出了文件的基本内容,包含了文件的完整路径,修改日期,SHA-256值,对应的源文件名。
类声明信息
public class Main$Test
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #22 // Main$Test
super_class: #2 // java/lang/Object
interfaces: 0, fields: 1, methods: 4, attributes: 3
此段输出了这个类的基本属性信息。
属性 | 说明 |
---|---|
minor version | 参与编译的java次版本号 |
major version | 参与编译的java主版本号,值61表示这是jdk17编译的 |
flags | 类的访问修饰符,是public类型的,后面的 就涉及到java沉重的历史包袱了,可以忽略。 |
this_class | 类索引 |
super_class | 父类索引 |
interfaces | 接口计数 |
fields | 字段集合数 |
methods | 方法计数 |
attributes | 附加属性数 |
常量池
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 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // {}
#14 = Utf8 {}
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Fieldref #22.#23 // Main$Test.text:Ljava/lang/String;
#22 = Class #24 // Main$Test
#23 = NameAndType #25:#26 // text:Ljava/lang/String;
#24 = Utf8 Main$Test
#25 = Utf8 text
#26 = Utf8 Ljava/lang/String;
#27 = String #28 // staticMethod
#28 = Utf8 staticMethod
#29 = String #30 // static
#30 = Utf8 static
#31 = Utf8 Code
#32 = Utf8 LineNumberTable
#33 = Utf8 LocalVariableTable
#34 = Utf8 this
#35 = Utf8 LMain$Test;
#36 = Utf8 method
#37 = Utf8 <clinit>
#38 = Utf8 SourceFile
#39 = Utf8 Main.java
#40 = Utf8 NestHost
#41 = Class #42 // Main
#42 = Utf8 Main
#43 = Utf8 InnerClasses
#44 = Utf8 Test
此段内容声明了当前类的常量池,常量池中主要两种类型的常量
类型 | 说明 |
---|---|
字面量 | 字符串,final类型的常量值 |
符号引用 | 1. 类和接口的全限定名 2. 字段的名称和描述符 3. 方法的名称和描述符 |
表中的#开头的数字都是索引,通过索引找到对应的值比如#23是 NameAndType
类型,也就是变量名和类型,值是#25:#26
,我们继续找到#25和#26索引对应的值,合并后变成text:Ljava/lang/String;
,也就是字符串类型的变量text。#44是utf8类型,也就是个字符串,值是 Test
构造函数
{
public Main$Test(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #13 // String {}
9: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_0
13: aload_1
14: putfield #21 // Field text:Ljava/lang/String;
17: return
LineNumberTable:
line 10: 0
line 14: 4
line 11: 12
line 12: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 this LMain$Test;
0 18 1 text Ljava/lang/String;
再接下来就是方法表了,上面是构造函数的定义.我们可以看到,因为是内部类,构造函数的名字不再是当前类名,而是父类$子类
属性 | 说明 |
---|---|
descriptor | 方法描述 包含了入参和出参的定义,但只有类型声明 |
flags | 方法的访问修饰符 |
Code | 方法的代码块 |
LineNumberTable | 行号表,记录字节码和源码的行号关联,一般用于堆栈打印具体报错的源码位置 |
LocalVariableTable | 本地变量表,述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系 |
代码块
下面我们分析一下 code区域的内容。
代码 | 说明 |
---|---|
stack=2, locals=2, args_size=2 | 这串内容说明调用栈中变量数量,本地变量数量,和入参的数量 |
0: aload_0 | aload指令是将局部变量表(slot)中的指定变量压入操作数栈中,索引0是为固定的this |
1: invokespecial #1 | invokespecial指定是用于调用方法,此处是调用Object的init方法,init方法值对象的实例构造方法,还有个client方法是class类的构造器,对应的变量初始化,代码块,构造函数都会整合到这2个方法内 |
4: getstatic #7 | 获取静态字段.该指令需要一个操作数,该操作数是常量池中某个CONSTANT_Fieldref_info常量的索引。#7对应System.out |
7: ldc #13 | 将常量从运行时常量池压栈到操作数栈 #13 对应字符串{} |
9: invokevirtual #15 | 用于调用非私有实例方法 #15值println 方法,操作数栈置空 |
aload_0 | 重新将this压入操作数栈 |
aload_1 | 将字符串{} 压入操作数栈 |
14: putfield #21 | 用栈顶的值为指定的类的实例域赋值 当前栈顶就是{} #21是变量text |
return | return void |
方法调用指令 | 作用 |
---|---|
invokestatic | 用于调用静态方法 |
invokespecial | 用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的default方法 |
invokevirtual | 用于调用非私有实例方法 |
invokeinterface | 用于调用接口方法 |
invokedynamic | 用于调用动态方法 |
通过对构造函数的分析,我们发现构造函数的实际内容为
System.out.println("{}");
this.text = text;
编译的时候,把构造函数和代码块整合到了一起,且代码块先于构造函数执行。
静态方法
public static void staticMethod();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #27 // String staticMethod
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 20: 0
line 21: 8
这个静态方法相比上一个构造函数并没有新的内容产生,再code段可以看到存在2个操作数栈变量,一个是System.out,一个是字符串staticMethod
,但源码里没有单独声明变量,所以locals是0.
public void method();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #21 // Field text:Ljava/lang/String;
7: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
LineNumberTable:
line 23: 0
line 24: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this LMain$Test;
上述是内部类定义的普通方法,在这里讲一下LineNumberTable为辅助性代码,如下图,报错堆栈打印的类的行号就源自这个表
和LocalVariableTable
.
LineNumberTable为辅助性代码,如下图,报错堆栈打印的类的行号就源自这个表,
比如line 23: 0
这个0值得是字节码指令前面的数字,23是指这个指令对应在方法中的哪一行的代码
后面的line 24: 10
是找不到对应的源码位置的,因为返回值是void的方法可以不写return,所以原本应该是return的第24行在源码里是大括号,这也说明了不写return实际是语法糖,java支持,jvm是不支持的。
LocalVariableTable
也是辅助性表,当编译出来的字节码需要被另外的程序使用的使用,或者使用mybatis之类的框架的时候需要使用。说白了就是存变量的名字的,对于jvm而言变量的名字是非必要的,变量名仅仅在程序员编写和阅读代码的时候有意义,为了减少程序的体积,编译的时候会移出所有变量的名称。但有时候我们有需要使用这个变量名,比如mybatis中sql中的参数占位符和方法的参数名是绑定的,如果编译的时候移除了参数名则执行SQL的会提示找不到参数值,打印的错误提示可能会变成 存在参数p1,p2,但找不到参数xxx之类的东西。因为参数没有名字,就可能被指定成p1,p2,arg1,arg2之类的无意义的名字。
或者你的程序引入了一个jar,编写代码调用jar内的方法时,如果方法内不存在LocalVariableTable
,则参数名会显示成i1,s1之类的名字。
又又又所以,我们发布maven仓库的时候,会发布jar和src.jar,方便开发和调试。
静态代码块
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #29 // String static
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 17: 0
line 18: 8
}
因为没有静态构造函数,所以静态代码块被单独拿出来了。
SourceFile: "Main.java"
NestHost: class Main
InnerClasses:
public static #44= #22 of #41; // Test=class Main$Test of class Main
这段为输出的最后一点内容,描述了class文件对应的源文件名,NestHost表示顶层类,Test内部类,对应顶层类Main,InnerClasses是对这个内部类的扩展说明。
========================
下面我们看下Main类中的内容
PS E:\Data\Desktop\tencent2\untitled\out\production\untitled> javap -v '.\Main.class'
Classfile /E:/Data/Desktop/tencent2/untitled/out/production/untitled/Main.class
Last modified 2024年2月2日; size 711 bytes
SHA-256 checksum fcb8f440a309c2c7e3acbaef453486523d279f13ea7263e6fc2bf2a6f847724c
Compiled from "Main.java"
public class Main
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #33 // Main
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 3
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 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // Hello world!
#14 = Utf8 Hello world!
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // Main$Test
#22 = Utf8 Main$Test
#23 = String #24 // hello
#24 = Utf8 hello
#25 = Methodref #21.#26 // Main$Test."<init>":(Ljava/lang/String;)V
#26 = NameAndType #5:#20 // "<init>":(Ljava/lang/String;)V
#27 = Methodref #21.#28 // Main$Test.method:()V
#28 = NameAndType #29:#6 // method:()V
#29 = Utf8 method
#30 = Methodref #21.#31 // Main$Test.staticMethod:()V
#31 = NameAndType #32:#6 // staticMethod:()V
#32 = Utf8 staticMethod
#33 = Class #34 // Main
#34 = Utf8 Main
#35 = Utf8 Code
#36 = Utf8 LineNumberTable
#37 = Utf8 LocalVariableTable
#38 = Utf8 this
#39 = Utf8 LMain;
#40 = Utf8 main
#41 = Utf8 ([Ljava/lang/String;)V
#42 = Utf8 args
#43 = Utf8 [Ljava/lang/String;
#44 = Utf8 LMain$Test;
#45 = Utf8 SourceFile
#46 = Utf8 Main.java
#47 = Utf8 NestMembers
#48 = Utf8 InnerClasses
#49 = Utf8 Test
{
public Main();
descriptor: ()V
flags: (0x0001) 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 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LMain;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello world!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: new #21 // class Main$Test
11: dup
12: ldc #23 // String hello
14: invokespecial #25 // Method Main$Test."<init>":(Ljava/lang/String;)V
17: astore_1
18: aload_1
19: invokevirtual #27 // Method Main$Test.method:()V
22: invokestatic #30 // Method Main$Test.staticMethod:()V
25: return
LineNumberTable:
line 3: 0
line 4: 8
line 5: 18
line 6: 22
line 7: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 args [Ljava/lang/String;
18 8 1 hello LMain$Test;
}
SourceFile: "Main.java"
NestMembers:
Main$Test
InnerClasses:
public static #49= #21 of #33; // Test=class Main$Test of class Main
下面针对main方法讲一下不一样的内容:
descriptor: ([Ljava/lang/String;)V
此段描述了main方法的入参字符串数组和没有输出参数。其中括号内的内容描述输入参数,参数使用分号分隔,括号后面的输出参数类型。
输入参数[Ljava/lang/String;
开头的方括号表示这是数组类型,后面的L指的是class,java/lang/String
指的就是String类型的完整包名,使用斜杠分隔。
返回类型V
就是void。
接下来讲一下源码中创建实例和调用方法的字节码
源码
Test hello = new Test("hello");
hello.method();
Test.staticMethod();
字节码
8: new #21 // class Main$Test
11: dup
12: ldc #23 // String hello
14: invokespecial #25 // Method Main$Test."<init>":(Ljava/lang/String;)V
17: astore_1
18: aload_1
19: invokevirtual #27 // Method Main$Test.method:()V
22: invokestatic #30 // Method Main$Test.staticMethod:()V
- 首先,new指令创建内部类Test的实例,并将其引用值压入栈顶,
- 下一个指令dup用于复制一份栈顶的数据压入栈顶,为什么要复制一份呢? 因为创建对象实例需要调用构造函数啦。invokespecial执行一次消耗一个操作数,这种就只剩一个引用在操作数栈了。
- 下一个指指令ldc 读取#23的指压入栈顶,此时操作数栈为 ["hello",this]
- astore_1指令 讲栈顶的值存入局部变量1,这个1是谁?看下LocalVariableTable中slot值是1的哪行,发现是刚才new出来的
hello
- aload_1 从局部变量读取并放入栈顶 也就是把hello读取出来,马上就要操作它了。
- invokevirtual 在这里是调用实例非私有方法,#27指的是方法
Main$Test.method
- invokestatic是调用静态方法 #30对应
Main$Test.staticMethod