前言
我们平时编写的java源文件,也就是.java文件在经过编译过后会成为jvm能识别的.class文件,也就是编译成了字节码文件,jvm的执行引擎目前有两种执行的方式,字节码解释执行和模板解释执行,我们的通常的字节码文件要通过jvm(c++)解释成计算机能识别的硬编码,也就是汇编;而模板解释器是直接不通过C++代码进行解释执行,而是通过模板解释器直接解释成计算机能识别的硬编码,而这些被模板解释器解释成的硬编码也是热点代码,热点代码具有热点代码缓存区,这个也是jvm调优的一部分,但是jvm有默认的热点代码缓存大小,如果不是太懂最好不要调整这个值的大小;关于执行引擎的解释器后面的笔记再行记录,这篇笔记主要记录下我们通过手动的方式是如何解析字节码文件的,进而了解字节码文件是什么样的形式进行存储。
字节码文件原貌
字节码文件即.java文件通过javac命令生成的.class文件 我们在编译器比如Eclipse或者Idea中编写了java类,但是编译工具会自动给我们编译成.class文件,也就是字节码文件;
看看字节码文件的原貌
比如我这边有个java文件,如下:
public class ByteCode {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
}
}
然后我们在windows下面通过javac或者开发工具给我们编译出来的字节码文件如下:
// class version 52.0 (52)
// access flags 0x21
public class com/bml/jvm/ByteCode {
// compiled from: ByteCode.java
// access flags 0xA
private static I count
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/bml/jvm/ByteCode; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 9 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
GETSTATIC com/bml/jvm/ByteCode.count : I
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L1
LINENUMBER 10 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 6 L0
ICONST_1
PUTSTATIC com/bml/jvm/ByteCode.count : I
RETURN
MAXSTACK = 1
MAXLOCALS = 0
}
注意看上面的前两行字节码文件
// class version 52.0 (52)
// access flags 0x21
意思就是我们的编写的这个java类的版本号是52,52是jdk的那个版本呢? 我们来看一张图:
所以我们的class的大版本major是52,是jdk1.8的版本
然后access flags 0x21指的就是我们的class的访问权限,我们的class ByteCode肯定是public,因为java类文件已经在上面了,所以access_flags 0x21就是标示我们的ByteCode的访问权限时public,那么0x21是什么意思呢,我们再来看下面一张图:
所以0x21代表的就是0x0001?
看什么的private static int I,I是什么意思呢?I表示就是我们的int类型,具体看下图
byte ->B
char->C
double->D
…
在上面类似这种Ljava/lang.String就是代本的String类型的参数,如果是数组的就是[Ljava/lang/String;
比如我写一个方法的描述符:
([[Ljava/lang/String;, I, Ljava/bml/Test;)Ljava/lang/String;
String XXX(String[][] strArrs, int a, Test test)
我们再来看下字节码的16进制文件
手动解析字节码文件
我们先来看下字节码文件包含了那些部分在里面,我们如何解析:
看结构从上到下,左边的U2 U4代表就是本结构多少字节,我们都知道1字节代本2位;
所以字节码文件的前四个字节u4=cafebabe(小端模式),而计算机一般传输过程中会转出大端模式
如果是大端模式就是bebafdca
一般jvm在解析class字节码文件的时候,验证的时候就首先判断魔数是不是以cafebabe开头,如果不是就是一个非法的class文件
在上图中“!”表示不确定有,长度大小不确定,好比实现的接口数量,我们的类可能没有接口,如果解析出来没有那么这个域就不会存在
我们来解析一次:
魔数(u4):cafebabe
minor version(u2):0000(次版本号为0)
major version(主版本号u2):0034(十进制为52)
常量池大小(u2):0024(十进制36)
我们来看这个常量池大小,在idea中用jclasslib就可以看到
常量池解析
常量池列表:
是从01开始的到35,真实的常量池个数 = 字节码文件中的常量池个数 - 1
解析常量池就要借助一个表格来,常量池规则表:
我们这边解析几个常量池,36个太多了
规则:
1.看上表中每一项的tag都是u1,那么就代表着我们的后面的解析每次取1字节
2.取到1字节过后,看值是多少,然后找到所对应的常量
3.找到所对应的常量过后依次把常量池解析出来
从上表中的0024过后的第一字节是:0a
第一个常量:
tag(u1):0a(0a十进制是10,在常量池结构表中对应的是Constant_Methodref_info)
index(u2):0006(是一个索引,6代表指向的是第6个,一般在字节码中未#6)
name_and_type(u2):0017(十进制23,23代表指向的是第23个,一般在字节码中表示#23
我们来验证一下:
看下截图:
我们解析的时候可以根据idea生成的可视化和我们自己手动解析的对应起来就知道有没有解析对
在idea中如果我们点击#6或者#23会直接跳到对应的类型上去,#6 #23代表的就是引用。
第二个常量:
tag(u1):09(9代表的就是Constant_Fieldref_info)
class_info(u2):0018(十进制24,代表指向#24)
name_and_type_info(u2):0019(十进制25,代表指向#25)
我们再来验证一下:
完全正确,当然了,这边手动解析只是为了了解class的文件结构,真实的情况下都是写程序取解析的,比如字节码技术asm
access flag:0021(public,前面已经说过)如图:
this_class(u2):0005(十进制是5,也就是#5)看idea的截图如下:
super_class:(u2):0006(看上图一目了然)
interfacce_count(u2):0000(表示接口为0)
interfaces[]:因为interface_count为0,所以这个域就不会存在了
fields_count(u2):00 01(表示有一个字段属性)
解析字段属性
接下来是解析我们的具体字段属性,字段属性的解析规则如下:
fields_1(第一个属性):
access_flags(u2):00 0a(十进制为10,是private static)
看下图:
name_index(u2):00 07
descriptor_index(u2):00 08
attributes_count(u2):0000(代表属性个数ConstantValue为0)
attributes:如果属性的数量=0,这块区域在字节码文件中就不会出现。
方法解析
方法解析前先看我们的方法信息:
从上图中可以看出我们的类ByteCode中有三个方法,分别是、main、
init:字节码为我们的java类生成的构造方法
client:当我们的类中有静态属性或者静态代码块的时候会生成clinit方法
因为我们的ByteCode类中有静态属性存在,所以生成的方法有三个,默认构造方法、man方法和静态初始化方法
接下来是方法个数
methods_count(u2):00 03(代表我们有3个方法)
这里解析方法,我演示解析我们的方法
上图是方法解析的规则
第一个方法init
access_flags:00 01(public)
name_index:00 09(引用到常量池中第9个常量)
desc_index: 00 0A(引用到常量池中第10个常量)
attr_count: 00 01(有一个属性)
attrs(如果属性=0,字节码文件中就没有这个区域):
属性内容解析规则:
attribute_name_index:00 0B(引用到常量池中第11个常量)
attribute_length: 00 00 00 2F(十进制47,表示长度为47)
max_stack:00 01
max_locals:00 01
code_length:00 00 05
codecode_length:2a b7 00 01 b1(根据长度取字节数)
exception_length:00 00 (无申明异常)
attribute_count:00 02(代表我们的方法有两个属性,也就是局部变量表和LineNumberTable)
attribute[attribute_count]:
Code属性的属性
attr_name_index: 00 0C(引用常量池12) LineNumberTable
att_length:00 00 00 06(长度为6)
line_number_length: 00 01(有一个LineNumber表)
[
start_pc:00 00
line_number: 00 03(具体的代码行数为3)
]
根据ByteCode的局部LineNumberTable在idea中所示:
LineNumberTable也就是jvm为什么会很准确定义到我们的代码错误行数等原理
attr_name_index:00 0D(引用常量池13)LocalVariableTable 方法的局部变量表
attr_len:00 00 00 0C(变量表长度为12)
table_length:00 01(有一个局部变量表)
[
start_pc: 00 00
length: 00 05(长度为5)
name_index: 00 0E(引用的是常量表的14)
des_index: 00 0F(引用的是常量表的15)
index: 00 00
]
根据ByteCode的局部变量表在idea中所示:
对应到上面的手动解析一目了然
还有最后一个属性的解析,非常简单,我这边就不解析,规则如下:
最后如果我们在电脑上如何查看自己的class的文件字节码,如果查看二进制的,直接用ue把class文件打开即可;
如果是为了查看字节码还可以在class的所在目录执行:
javap -verbose ByteCode.class
Last modified 2020-8-7; size 602 bytes
MD5 checksum 367be1125b0a815faf617b48e78c0d20
Compiled from "ByteCode.java"
public class com.bml.jvm.ByteCode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Fieldref #5.#26 // com/bml/jvm/ByteCode.count:I
#4 = Methodref #27.#28 // java/io/PrintStream.println:(I)V
#5 = Class #29 // com/bml/jvm/ByteCode
#6 = Class #30 // java/lang/Object
#7 = Utf8 count
#8 = Utf8 I
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/bml/jvm/ByteCode;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 <clinit>
#21 = Utf8 SourceFile
#22 = Utf8 ByteCode.java
#23 = NameAndType #9:#10 // "<init>":()V
#24 = Class #31 // java/lang/System
#25 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#26 = NameAndType #7:#8 // count:I
#27 = Class #34 // java/io/PrintStream
#28 = NameAndType #35:#36 // println:(I)V
#29 = Utf8 com/bml/jvm/ByteCode
#30 = Utf8 java/lang/Object
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (I)V
{
public com.bml.jvm.ByteCode();
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
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/bml/jvm/ByteCode;
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: getstatic #3 // Field count:I
6: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 9: 0
line 10: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #3 // Field count:I
4: return
LineNumberTable:
line 6: 0
}
结束语
上面就是差不多一个类的手动解析的一个过程,当然了还有很多没有写上,比如有继承的该如何解析,接口的怎么解析,其实都是一样的道理,手动解析这个实在太麻烦,意在明白这个解析的过程
如果真的要解析class字节码文件,那么我们肯定根据规则去编写自己的应用程序取解析,去完成我们的需求,这边只是将字节码的结构和底层原理分析出来,根据这个思想和思路去解析我们的字节码文件;其实你不懂字节码文件也不影响你工作,不影响你写代码,但是知识的储备不就是为了丰富你的人生,精彩你的生活吗?不要永远守着那点知识“啃老”,要学习进步,不仅仅是为了能够生存,也为了能够让我们的人生更精彩。
其实了解我们的JVM对大家也是非常有用的,为什么呢?因为现在运行在jvm上的语言不仅仅是java了,很多语言都是自己实现了编译,让后在jvm上运行,所以jvm是相对永恒的,java可能是暂时的,所以我们了解下底层的一些知识点没有什么损失,也就浪费点业余时间而已。
java
groovy
kotlin
Scala的源文件 ,经过Scala的编译器,编译生成了.class文件
只要符合jvm规范的class文件都可以在jvm上运行,不仅仅是java
上图就是不同语言在jvm上运行,所以学好jvm真的很有必要