简介
类文件:又称字节码文件,java源代码编译之后产生的文件,是基于字节码的二进制文件,jvm通过运行类文件来执行java程序。
字节码:类文件中的基本数据单位,一个字节码占一个字节。
类文件的基本结构
类文件中存储的数据:类文件相当于jvm的指令集,文件中存储了两种数据,虚拟机指令和操作数
- 虚拟机指令:因为一个字节可以表示256个数字,所以Java支持256个虚拟机指令,目前已使用200多个,虚拟机指令相当于CPU的操作码。
- 操作数:虚拟机指令操作的数据,操作数是字节码指令的一部分,用于指定指令的具体操作对象,例如变量、常量或方法调用的目标。
类文件的基本结构:
text
ClassFile {
u4 magic; // 类文件的魔数,每个类型的文件都有自己的魔数
u2 minor_version; // 次版本
u2 major_version; // 主版本
u2 constant_pool_count; // 常量池中数据项的个数
cp_info constant_pool; // 常量池
u2 this_class; // 当前类的全路径
u2 super_class; // 当前类的父类的全路径
u2 interfaces_count; // 接口数量
u2 interfaces; // 接口的路径
u2 fields_count; // 字段数量
field_info fields; // 字段
u2 methods_count; // 方法数量
method_info methods; // 方法
u2 attributes_count; // 属性数量
attribute_info attributes; // 属性
}
内容讲解:u1、u2、u4、u8:分别代表1个字节、2个字节、4个字节、8个字节。在这里,u4、u2等内容,表示它对应的数据项占多少个字节,它们本身并不存在于文件中
类文件中的各数据项严格按照定义进行排列,中间没有分隔符和填充,如果遇到8位以上的数据项,按照高位在前的方式拆分成若干个字节存储
前置容量:类文件不采用分隔符的方式分隔数据,数据项的顺序是被严格限定的,因此当需要描述多个同类数据项的时候,采用前置容量计数器的方式。
查看类文件的十六进制形式
类文件是一种二进制文件,使用普通的文本编辑器只能看到乱码,使用十六进制的形式来查看,则可以看到原始的二进制的内容。
方法:
- 在Linux平台下,使用xxd命令,得到十六进制形式的类文件
- 使用vim名, 打开字节码文件,输入命令
:%!xxd
案例:字节码文件的内容是一个最简单的Hello World
text
00000000: cafe babe 0000 0034 0022 0a00 0600 1409 .......4."......
00000010: 0015 0016 0800 170a 0018 0019 0700 1a07 ................
00000020: 001b 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 124c 6f63 umberTable...Loc
00000050: 616c 5661 7269 6162 6c65 5461 626c 6501 alVariableTable.
00000060: 0004 7468 6973 0100 1c4c 6f72 672f 7779 ..this...Lorg/wy
00000070: 6a2f 6a76 6d2f 7031 302f 4865 6c6c 6f57 j/jvm/p10/HelloW
00000080: 6f72 6c64 3b01 0004 6d61 696e 0100 1628 orld;...main...(
00000090: 5b4c 6a61 7661 2f6c 616e 672f 5374 7269 [Ljava/lang/Stri
000000a0: 6e67 3b29 5601 0004 6172 6773 0100 135b ng;)V...args...[
000000b0: 4c6a 6176 612f 6c61 6e67 2f53 7472 696e Ljava/lang/Strin
000000c0: 673b 0100 0a53 6f75 7263 6546 696c 6501 g;...SourceFile.
000000d0: 000f 4865 6c6c 6f57 6f72 6c64 2e6a 6176 ..HelloWorld.jav
000000e0: 610c 0007 0008 0700 1c0c 001d 001e 0100 a...............
000000f0: 0a48 656c 6c6f 576f 726c 6407 001f 0c00 .HelloWorld.....
00000100: 2000 2101 001a 6f72 672f 7779 6a2f 6a76 .!...org/wyj/jv
00000110: 6d2f 7031 302f 4865 6c6c 6f57 6f72 6c64 m/p10/HelloWorld
00000120: 0100 106a 6176 612f 6c61 6e67 2f4f 626a ...java/lang/Obj
00000130: 6563 7401 0010 6a61 7661 2f6c 616e 672f ect...java/lang/
00000140: 5379 7374 656d 0100 036f 7574 0100 154c System...out...L
00000150: 6a61 7661 2f69 6f2f 5072 696e 7453 7472 java/io/PrintStr
00000160: 6561 6d3b 0100 136a 6176 612f 696f 2f50 eam;...java/io/P
00000170: 7269 6e74 5374 7265 616d 0100 0770 7269 rintStream...pri
00000180: 6e74 6c6e 0100 1528 4c6a 6176 612f 6c61 ntln...(Ljava/la
00000190: 6e67 2f53 7472 696e 673b 2956 0021 0005 ng/String;)V.!..
000001a0: 0006 0000 0000 0002 0001 0007 0008 0001 ................
000001b0: 0009 0000 002f 0001 0001 0000 0005 2ab7 ...../........*.
000001c0: 0001 b100 0000 0200 0a00 0000 0600 0100 ................
000001d0: 0000 0600 0b00 0000 0c00 0100 0000 0500 ................
000001e0: 0c00 0d00 0000 0900 0e00 0f00 0100 0900 ................
000001f0: 0000 3700 0200 0100 0000 09b2 0002 1203 ..7.............
00000200: b600 04b1 0000 0002 000a 0000 000a 0002 ................
00000210: 0000 0008 0008 0009 000b 0000 000c 0001 ................
00000220: 0000 0009 0010 0011 0000 0001 0012 0000 ................
00000230: 0002 0013 ....
结果解读:
- 整体结构:除第一列外,每个字符代表4比特数据,因为每4比特数据组成一个十六进制字符,所以,每两个字符代表一个字节,一行有16个字节
- 第一列:地址偏移量,它是十六进制的
部分解读十六进制内容:
-
magic:魔数,1-4 字节,cafe babe。所有的文件都有自己的特定类型,魔数用来表示一个文件究竟是什么文件。虚拟机在读取到魔数后认为该文件是一个字节码文件。
-
minor_version:次要版本,5-6 字节,0000,换算成十进制是0
-
major_version:主版本,7-8字节,0034,换算成十进制是52,代表jdk8。通过版本号,虚拟机能够检测是否可兼容该类文件
使用javap命令反编译源代码
案例:javap -v 字节码文件
,字节码文件的内容是一个最简单的Hello World
打印结果:
text
Classfile /Users/work/project/java/learn-jvm/out/production/jvm-learn/org/wyj/jvm/p10/HelloWorld.class
Last modified 2024-3-10; size 564 bytes
MD5 checksum cc6a38615b9ded3b89bb45327bb537e3
Compiled from "HelloWorld.java"
public class org.wyj.jvm.p10.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // HelloWorld
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // org/wyj/jvm/p10/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/wyj/jvm/p10/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 HelloWorld
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 org/wyj/jvm/p10/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public org.wyj.jvm.p10.HelloWorld();
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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/wyj/jvm/p10/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String HelloWorld
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 8: 0
line 9: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
javap反编译结果解读:
-
第一部分:Classfile,类文件的具体位置、上次修改时间、校验和、源文件位置
-
第二部分:类的签名、版本号
-
第三部分:Constant pool,常量池
-
第四部分:类中的方法,方法中的内容:
-
stack:代表操作数栈的深度
-
locals:代表局部变量表的长度
-
args_size:方法的参数个数
-
接下来是 字节码行号 、字节码指令 、操作数
-
LineNumberTable:源码的行号和字节码行号的对应关系
-
LocalVariableTable:局部变量表
-
start:字节码行号,在这一行,变量开始生效
-
length:长度,经过多少字节码行,变量的作用域结束
-
slot:变量的槽位,slot可以重复使用
-
name:变量的名称
-
Signature:变量的数据类型
-
-
数据类型的表示:基本数据类型使用一个特殊字符来表示,引用数据类型,在类的全路径前加 L,数组类型,如果是一维数组,加 [,如果是二维数组,加 [[。
类型的表示:
- byte:B,字节
- char:C,字符
- double:D
- float:F
- int:I
- long:J
- short:S
- boolean:Z
- void:V
- 类对象:L类的全路径
- 一维数组:[类的全路径,例如,字符串数组 [Ljava/lang/String
- 二维数组:[[类的全路径
<clinit>
方法和<init>
方法
clinit方法:静态代码在字节码指令中的名称。在类初始化时调用,只会被调用一次,用于初始化类。它会依据顺序来收集类中的静态代码块和有初始化语句的静态变量,在初始化类时执行
init方法:构造方法在字节码指令中的名称。每个构造方法在执行时,都会收集类中的普通代码块和有初始化语句的成员变量,然后把这些代码放在构造方法的开头执行,注意,代码块在构造方法之前执行
Q&A
a++ + ++a的执行过程
源代码:
java
int a = 1;
int b = a++ + ++a - a--; // b = 1 a = 2
总结:这个表达式的计算顺序是从左到右,结果是 1 + 3 - 3 = 1,a = 2,具体计算过程是:a先参与运算,然后++,所以是1 + ++a - a--,++a时,此时a是2,先加加,后参与运算,所以是1 + 3 - a--,a减减也类似,先参与运算,后减减,所以是 1 + 3 - 3 ,此时a减减后是2,表达式的值是1。
这是一道面试题,当时觉得没人会这么写代码,但是真的被面试到了,发现自己不会。这道题更加详细的原理需要结合字节码指令来分析,和局部变量表、操作数栈相关,a++是先把数据加载到操作数栈,然后再把加加结果写回到局部变量表,基本是这样,但大概原理是上面描述到的。