1、基本介绍:
- .java文件通过javac编译后将得到一个.class文件,比如编写一个简单的ByteCodeDemo类,编译后生成ByteCodeDemo.class文件,打开后是一堆十六进制数,由十部分按照固定的顺序组成。
- JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性(因为寄存器指令集往往和硬件挂钩),但缺点在于,要完成同样的操作,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈)。另外,由于栈是在内存实现的,而寄存器是在CPU的高速缓存区,相较而言,基于栈的速度要慢很多,这也是为了跨平台性而做出的牺牲。
// ByteCodeDemo.java文件:
public class ByteCodeDemo {
private int a = 1;
public int add() {
int b = a + 5;
return b;
}
}
// ByteCodeDemo.class文件:
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 0161 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0361 6464
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0011 4279 7465 436f 6465 4465
6d6f 2e6a 6176 610c 0007 0008 0c00 0500
0601 0015 636f 6d2f 6465 6d6f 2f42 7974
6543 6f64 6544 656d 6f01 0010 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 0021 0003
0004 0000 0001 0002 0005 0006 0000 0002
0001 0007 0008 0001 0009 0000 0026 0002
0001 0000 000a 2ab7 0001 2a04 b500 02b1
0000 0001 000a 0000 000a 0002 0000 0009
0004 000a 0001 000b 000c 0001 0009 0000
0025 0002 0002 0000 0009 2ab4 0002 0860
3c1b ac00 0000 0100 0a00 0000 0a00 0200
0000 0c00 0700 0d00 0100 0d00 0000 0200
0e
2、class文件结构:


2.1、魔数:
- 前4个字节表示的是魔数,对应我们Demo的是 0XCAFE BABE。什么是魔数?魔数是用来区分文件类型的一种标志,一般都是用文件的前几个字节来表示。比如0XCAFE BABE表示的是class文件,那么有人会问,文件类型可以通过文件名后缀来判断啊?是的,但是文件名是可以修改的(包括后缀),那么为了保证文件的安全性,讲文件类型写在文件内部来保证不被篡改。
2.2、版本号:
- 我们识别了文件类型之后,接下来要知道版本号。版本号含主版本号和次版本号,都是各占2个字节。在此Demo种为0X0000 0034。其中前面的0000是次版本号,后面的0034是主版本号。通过进制转换得到的是次版本号为0,主版本号为52。
- 从oracle官方网站我们能够知道,52对应的正式jdk1.8,而其次版本为0,所以该文件的版本为1.8.0。如果需要验证,可以在用java --version命令输出版本号,或者修改编译目标版本–target重新编译,查看编译后的字节码文件版本号是否做了相应的修改。
2.3、常量池:
- 紧接着主版本号之后的就是常量池入口。常量池是Class文件中的资源仓库,在接下来的内容中我们会发现很多地方会涉及,如Class Name,Interfaces等。常量池中主要存储2大类常量:字面量和符号引用。字面量如文本字符串,java中声明为final的常量值等等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符。
- 该项存放了类中各种文字字符串、类名、方法名和接口名称、final 变量以及对外部类的引用信息等常量。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用,因此它在 Java 的动态链接中起到了核心的作用。常量池的大小平均占到了整个类大小的 60% 左右。
- 这个静态常量池有2+n个字节,前两个字节表示的是常量池大小。
2.4、Access_Flag 访问标志:
- 访问标志信息包括该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final。
2.5、类索引:
- 访问标志后的两个字节,描述的是当前类的全限定名。
- 这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。
2.6、父类索引:
- 当前类名后的两个字节,描述父类的全限定名。
- 同上,保存的也是常量池中的索引值。
2.7、接口索引:
- 一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。
- 这个接口有2+n个字节,前两个字节表示的是接口数量,紧接着的n个字节是所有接口名称的字符串常量的索引值。
2.8、字段表集合:
- 字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。
- fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。
- 字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。
2.9、方法表Methods:
- 该项对类或接口中声明的方法进行了细致的描述。例如方法的名称、参数和返回值类型等。
- 需要注意的是,methods 列表里仅存放了本类或本接口中的方法,并不包括从超类和父接口继承而来的方法。
- 第一部分为两个字节描述方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性。
2.10、附加属性表Class attributes:
- 该项存放了在该文件中类或接口所定义的属性的基本信息。
3、反编译class文件:
- 对class文件运行javap命令,
javap -v ByteCodeDemo.class。 - 反编译后看到的是十六进制操作码所对应的JVM指令操作码。
{
public com.demo.ByteCodeDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
LineNumberTable:
line 9: 0
line 10: 4
public int add();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: iconst_5
5: iadd
6: istore_1
7: iload_1
8: ireturn
LineNumberTable:
line 12: 0
line 13: 7
}
参考链接: https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
版权声明:本文为www1575066083原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。