前
Java字节码结构看过很多遍,单总是记不太清,看的博客也是五花八门,这里还是自己总结下。
声明:本篇博客大部分位其他博客内容,只供自己总结使用,不是原创内容
结构
字节码
Java字节码是Java跨平台,也就是所谓“一次编译,到处运行“的根本,Java的运行容器,也就是JVM,它不是跨平台的,win和linux的JVM实现都不一样,真正跨平台的是字节码文件,因为所有字节码是同一份,不同平台的JVM虽然实现不同,但是大家都遵循着一个规范,就是JVM规范,对于字节码的识别是相同的。
而字节码文件(.class)并不是Java独有的,也可以理解为它适合JVM绑定的,因为不只是Java代码可以编译成字节码文件,例如scala,groovy,kotlin文件都可以编译成字节码文件,这也是这些代码可以在一个项目里存在的原因,使用不同的编译器,不同的代码文件最后都编译成同一份字节码文件。
获取字节码
Java中获取字节码很简单,使用javac xxx.java 即可
打开字节码文件一般以16进制的形式打开(vscode用hexdunp插件)
一般用idea打开的字节码文件都是反编译以后的Java代码,也可以用javap反编译,结果更清晰一点
这边复制一份网上的字节码文件
java代码是 Main.java
public class Main {
private int m;
public int inc() {
return m + 1;
}
}
字节码 Main.class
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: CA FE BA BE 00 00 00 34 00 13 0A 00 04 00 0F 09 J~:>...4........
00000010: 00 03 00 10 07 00 11 07 00 12 01 00 01 6D 01 00 .............m..
00000020: 01 49 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 .I...<init>...()
00000030: 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E V...Code...LineN
00000040: 75 6D 62 65 72 54 61 62 6C 65 01 00 03 69 6E 63 umberTable...inc
00000050: 01 00 03 28 29 49 01 00 0A 53 6F 75 72 63 65 46 ...()I...SourceF
00000060: 69 6C 65 01 00 09 4D 61 69 6E 2E 6A 61 76 61 0C ile...Main.java.
00000070: 00 07 00 08 0C 00 05 00 06 01 00 0A 6F 74 68 65 ............othe
00000080: 72 2F 4D 61 69 6E 01 00 10 6A 61 76 61 2F 6C 61 r/Main...java/la
00000090: 6E 67 2F 4F 62 6A 65 63 74 00 21 00 03 00 04 00 ng/Object.!.....
000000a0: 00 00 01 00 02 00 05 00 06 00 00 00 02 00 01 00 ................
000000b0: 07 00 08 00 01 00 09 00 00 00 1D 00 01 00 01 00 ................
000000c0: 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00 ...*7..1........
000000d0: 00 06 00 01 00 00 00 08 00 01 00 0B 00 0C 00 01 ................
000000e0: 00 09 00 00 00 1F 00 02 00 01 00 00 00 07 2A B4 ..............*4
000000f0: 00 02 04 60 AC 00 00 00 01 00 0A 00 00 00 06 00 ...`,...........
00000100: 01 00 00 00 0D 00 01 00 0D 00 00 00 02 00 0E ...............
javap反编译 javap -verbose -p Main.class
Classfile /Users/xxx/xxx/xxxx/xxx/main/java/xxx/Main.class
Last modified 2022-2-25; size 271 bytes
MD5 checksum 81e29186872af47b63bf14bd01d386b8
Compiled from "Main.java"
public class other.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // other/Main.m:I
#3 = Class #17 // other/Main
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 Main.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 other/Main
#18 = Utf8 java/lang/Object
{
private int m;
descriptor: I
flags: ACC_PRIVATE
public other.Main();
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 int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 13: 0
}
SourceFile: "Main.java"
详细结构
魔数
前8位是魔数,字节码的标志,都是CAFEBABE
版本号
接下来8位位版本号0000 0034
,前4位位副版本号,后四位位主版本号,(只知道java8是52,是有规律的)
常量池
接下来4位0013
是常量池中的常量数量,这个数量是从1开始计数的,所以一共有18个,
常量池中存放了(字面量)文字字符串,常量值,(符号引用)当前类的类名,字段名,方法名,字段和方法的描述符,对当前类的引用信息,当前类中对其他类的引用信息等。
常量池是一个类的结构索引,其他地方对对象的引用可以通过索引位置代替,常量池中的数据通过索引来访问,但是下标是从1开始的,如果索引了0,就代表不引用任何常量。
在字节码加载入虚拟机的链接阶段,内部分为校验,准备,解析,在解析阶段,会把符号引用转变为直接引用,也就是把常量池中的这些符号引用转化成为内存中的真实地址。
0013后面的到下一个类型前的内容都是常量池的内容,每一种常量类型的结构不同,占用字节也不同,解析也是通过tag先识别类型,然后读取对应长度的字节来解析的。
这边就不具体说了,可以参考一下表
类访问标记
接下来是类的访问标记,有个对应表
值得注意的是,这个字段用两个字节表示,例如例子中的javap后的反编译内容中访问标记是ACC_PUBLIC, ACC_SUPER,将二者相加,结果是0x0021,可以在字节码文件中找到
类索引,父类索引,接口索引
接下来的2个字节是类的索引,是一个无符号整数,值为常量池的索引,
再接下来2个字节是父类的索引,同上
再接下来2个字节是接口的计数
再接下来n*2个字节是接口的索引,n是接口的数量
字段表
接下来2个字节表示类的字段数量,包括实例字段和类字段,不包括方法内部的局部变量,同样不包括父类或者接口中的字段
再接下来就是字段的具体描述,每个字段用一个field_info结构体来描述,包含多个修饰符,例如:
- 访问标记 u2
- 字段名索引 u2
- 描述符索引 u2
- 属性计数器 u2
- 属性集合
其中修饰符都是布尔值,要么有,要么没有,字段名是索引,常量池中的
还有一个属性集合包含初始化值,注释信息等
方法表
还是先是两个字节表示方法数量
接下来是方法的具体描述,使用method_info来描述,结构也类似
- 访问标志 u2
- 字段名索引 u2
- 描述符索引 u2
- 属性计数器 u2
- 属性集合
属性表集合
属性表指class文件携带的辅助信息,比如该class文件的源文件的名称,以及注解信息
属性表的结构没有很严格,只要不重名,任何人实现的编译器都可以向属性表写入东西,但是java虚拟机会忽略掉不认识的
结构如下
- 属性名索引 u2
- 属性长度 u4
- 属性表 u1
属性表详情就不介绍了,后面有需要再说。