概述本文主要是基于 .class 文件,进行分析 .class 文件的内容。
这部分个人觉得主要是属于设计机构拓展的内容,大家可以一起来学习一下 Java 字节码的设计结构以及感受一下设计者的设计。
Java 提供 javap 命令可以分析字节码文件,我们可以使用 javap -verbose 命令分析一个字节码文件时, 将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息。
一个简单的 Java 代码
public class TestClass {
private int m;
public int inc() {
return ++m;
}
}
下图显示的是 Java 代码编译后 .class 文件的十六进制信息

bytecode_十六进制.png
为了方便对比我执行一下 javap -v TestClass
Classfile /../../TestClass.class
Last modified 2021-2-6; size 306 bytes
MD5 checksum eeba40cc40cc28ef4d416ff70d901561
Compiled from "TestClass.java"
public class cn.edu.cqvie.jvm.bytecode.TestClass
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 // cn/edu/cqvie/jvm/bytecode/TestClass.m:I
#3 = Class #17 // cn/edu/cqvie/jvm/bytecode/TestClass
#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 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 cn/edu/cqvie/jvm/bytecode/TestClass
#18 = Utf8 java/lang/Object
{
public cn.edu.cqvie.jvm.bytecode.TestClass();
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 3: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field m:I
5: iconst_1
6: iadd
7: dup_x1
8: putfield #2 // Field m:I
11: ireturn
LineNumberTable:
line 8: 0
}
SourceFile: "TestClass.java"

bytecode_结构.png
魔数:所有的.class 字节码文件的前4个字节都是魔数,魔数为固定值: 0xCAFEBABE
版本信息,魔数之后的4个字节是版本信息,前两个字节表示 minor version (次版本号), 后2个字节表示major version (主版本号)。这里的版本号 00 00 00 34 换算成十进制表, 表示次版本号为0, 主版本号为 52. 所以该文件的版本号为 1.8.0。可以通过 java -version 来验证这一点。
➜ ~ java -version
java version "1.8.0_281"
Java(TM) SE Runtime Environment (build 1.8.0_281-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)
00 13 代表有 18 个常量。常量数组则紧跟着常量池数量之后。常量池数组与一般数组不同的是, 常量池数组中不同的元素的类型,结构都是不同的。长度当然也就不同;但是, 一种元素的第一种元素的第一个数据都是一个u1类型, 该字节是一个标识位,占据1个字节。JVM在解析常量池时,会更具这个u1 类型来获取元素的具体类型。值得注意的是:常量池中元素的个数 = 常量池数 -1 (其中0暂时不适用), 目的是满足某些常量池索引值的数据在特定情况下需要表达【不引用任何一个常量池】的含义:根本原因在于,索引为0也是一个常量(保留常量),只不过他不位于常量表中。这个常量就对应null值, 所以常量池的索引是从1开始而非0开始。
上面表中描述了11种数据类型的机构, 其实在jdk1.7之后又增加了3种(CONSTANT_MethodHandle_info, CONSTANT_MethodType_info 以及CONSTANT_InvokeDynami_info)。这样一共14种。
Class 字节码中有两种数据类型
0A 00 04 00 0F method_info 00 04 指向常量池中的常量的位置, 00 0F 指向的是 15 的位置。09 00 03 00 10 field_info 00 03 指向 3 的位置 00 10 (16 位置)07 00 11 class info 00 11 (17 位置)07 00 12 class info 00 12 (18 位置)01 00 01 6D utf8 长度为 1 内容为 6D 表示内容转换为 10 进制为 109 转换为 ASCII 码最后的结果为 m01 00 01 49 utf8 长度为 1 内容为 I01 00 06 3C 69 6E 69 74 3E utf8 长度为 8 内容为 <init>01 00 03 28 29 56 utf8 长度为 3 内容为:()V01 00 04 43 6F 64 65 utf8长度为 4 内容为 Code01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 utf8 长度为 15 内容为 LineNumberTable01 00 03 69 6E 63 utf8 长度为3 内容为 inc01 00 03 28 29 49 长度为 3 内容为 ()I01 00 0A 53 6F 75 72 63 65 46 69 6C 65 长度为 10 内容为 SourceFile01 00 0E 54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61 长度为 14 内容为 TestClass.java0C 00 07 00 08 NameAndType 内容 指向 7 位置。00 08 (8 位置)0C 00 05 00 06 NameAndType 内容 指向 5 位置。00 06 (6 位置)01 00 23 63 6E 2F 65 64 75 2F 63 71 76 69 65 2F 6A 76 6D 2F 62 79 74 65 63 6F 64 65 2F 54 65 73 74 43 6C 61 73 73 长度 35 内容 cn/edu/cqvie/jvm/bytecode/TestClass01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 长度 16 内容 java/lang/ObjectAccess_Flag 访问标志 访问标识信息包括该Class文件时类和接口是否被定义成了public,是否是 abstract, 如果是类,是否被申明为成final。通过扇面的源代码。
0x 00 21: 表示是0x0020 和0x0001的并集, 表示 ACC_PUBLIC 与 ACC_SUPER

bytecode_访问标志.png
00 03 类名, 03 常量池位置 cn/edu/cqvie/jvm/bytecode/TestClass
00 04 父类名. 04 常量池位置 java/lang/Object
00 00 接口个数, 0 个, 接口个数最多 65535
00 01 字段的个数, 这里有一个

bytecode_字段表.png
字段表结构
field_info {
u2 access_flags; 00 02 Fieldref
u2 name_index; 00 05 (表示字段名称在常量池中的索引位置) m
u2 descriptor_index; 00 06 (描述符索引) I
u2 attributes_count; 00 00
attribute_info attributes[attributes_count]
}
00 02 表示有两个方法

bytecode_方法表.png
方法表结构
method_info {
u2 access_flags; 00 01 Methodref
u2 name_index; // 00 07 <init>
u2 descriptor_index; 00 08 // ()V
u2 attributes_count; 00 01 // 属性结构
attributes_info attributes[attributes_count]
}
方法属性结构
attribute_info {
u2 attribute_name_index; 00 09 // Code
u4 attribute_length; 00 00 00 1D 长度 29
u1 info[attribute_length];
...
}
00 09 00 00 1D 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 03 00
"Code" 表示下面是执行代码
JVM 预定义了一部分的attribute, 但是编译器自己也可以实现自己的attribute 写入class文件中, 供运行时使用。不同的attribute 通过attribute_name_index 来区分。

Code attribute 的作用是保存该放的的结构,如所对应的字节码
Code_attribute {
u2 attribute_name_index; // 00 09 ==> Code
u4 attribute_length; // 00 00 00 1D ==> 29
u2 max_stack; // 00 01 栈深度为 1 (栈帧中操作数栈的深度)
u4 code_length; // 00 00 00 05 指令的长度是多少
u1 code[code_lenght]; // 2A B7 00 01 B1 其中 2A
// 2A aload_0
// B7 invokespecial
// 00 #1
// 01 <java/lang/Object.<init>>
// B1 return
// 0 aload_0
// 1 invokespecial #1 <java/lang/Object.<init>>
// 4 return
u2 exception_table_length; // 00 00
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_lenght];
u2 attributes_count; // 00 01
attribute_info attributes[attributes_count];
}
LineNumberTable_attribute {
u2 attribute_name_index; //00 0A 常量池10号位置 LineNumberTable
u4 attribute_lenght; // 00 00 00 06 一共 6个长度
u2 line_number_table_length; // 00 01 映射的对数 1 对
line_number_info {
u2 start_pc; // 指令行号 00 00
u2 line_number; // 源码调试 00 03
}
line_number_table[line_number_table_length];
}
局部变量表 LocalVariableTable
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_lenght;
u2 local_variable_table_length;
{
u2 start_pc;
u2 line_number;
u2 name_index;
u2 descriptor_index;
u2 index;
}
}