操做java字节码,免不了要对字节码文件有一个详细的认识。本文主要记录学习小册 《JVM字节码从入门到精通》 的笔记,以供参考。java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
复制代码
打开命令行,将此文件经过 javac 命令编译成 jvm 能识别的 class 文件数组
javac Hello.java
复制代码
而后用 xxd 命令以 16 进制的方式查看这个 class 文件bash
xxd Hello.class
复制代码
16进制以下:数据结构
00000000: cafe babe 0000 0034 001d 0a00 0600 0f09 .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507 ................
00000020: 0016 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 046d 6169 umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67 n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 0a53 6f75 /String;)V...Sou
00000070: 7263 6546 696c 6501 000a 4865 6c6c 6f2e rceFile...Hello.
00000080: 6a61 7661 0c00 0700 0807 0017 0c00 1800 java............
00000090: 1901 000b 4865 6c6c 6f20 576f 726c 6407 ....Hello World.
000000a0: 001a 0c00 1b00 1c01 0021 636f 6d2f 7869 .........!com/xi
000000b0: 6173 6d2f 6173 6d64 656d 6f2f 636c 6173 asm/asmdemo/clas
000000c0: 7374 6573 742f 4865 6c6c 6f01 0010 6a61 stest/Hello...ja
000000d0: 7661 2f6c 616e 672f 4f62 6a65 6374 0100 va/lang/Object..
000000e0: 106a 6176 612f 6c61 6e67 2f53 7973 7465 .java/lang/Syste
000000f0: 6d01 0003 6f75 7401 0015 4c6a 6176 612f m...out...Ljava/
00000100: 696f 2f50 7269 6e74 5374 7265 616d 3b01 io/PrintStream;.
00000110: 0013 6a61 7661 2f69 6f2f 5072 696e 7453 ..java/io/PrintS
00000120: 7472 6561 6d01 0007 7072 696e 746c 6e01 tream...println.
00000130: 0015 284c 6a61 7661 2f6c 616e 672f 5374 ..(Ljava/lang/St
00000140: 7269 6e67 3b29 5600 2100 0500 0600 0000 ring;)V.!.......
00000150: 0000 0200 0100 0700 0800 0100 0900 0000 ................
00000160: 1d00 0100 0100 0000 052a b700 01b1 0000 .........*......
00000170: 0001 000a 0000 0006 0001 0000 0003 0009 ................
00000180: 000b 000c 0001 0009 0000 0025 0002 0001 ...........%....
00000190: 0000 0009 b200 0212 03b6 0004 b100 0000 ................
000001a0: 0100 0a00 0000 0a00 0200 0000 0500 0800 ................
000001b0: 0600 0100 0d00 0000 0200 0e ...........
复制代码
一个字节是8位,两个十六进制数表示一个字节。
jvm
不少文件都以魔数来进行文件类型的区分,class 文件的头四个字节称为魔数,是0xCAFEBABE,这个魔数是 jvm 识别 class文件的标识,虚拟机在加载class 文件前会先检查这四个字节,若是不是则拒绝加载。函数
class文件是二进制块,想直接看懂它比较难,javap 命令能够窥探 class 文件内部细节,其中 javap -c xxx 是用来对class文件进行反编译学习
xiasmdeMacBook-Pro:test xiasm$ javap -c Hello
警告: 二进制文件Hello包含com.xiasm.asmdemo.classtest.Hello
1 Compiled from "Hello.java"
2 public class com.xiasm.asmdemo.classtest.Hello {
3 public com.xiasm.asmdemo.classtest.Hello();
4 Code:
5 0: aload_0
6 1: invokespecial #1 // Method java/lang/Object."<init>":()V
7 4: return
8
9 public static void main(java.lang.String[]);
10 Code:
11 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
12 3: ldc #3 // String Hello World
13 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14 8: return
15 }
复制代码
上面代码前面的数字表示从方法开始算起的字节码偏移量
ui
代码前面的行号是我本身加上的,能够看到,3-7行虽然没有写Hello类的构造函数,可是编译器仍是为咱们自动加上了。this
到此,构造器函数就结束了,接下来是 main 函数:spa
理解 class 文件结构是理解字节码的基石,class 文件结构比较复杂。
Java 虚拟机规定义了 u一、u二、u4 三种数据结构来表示 一、二、4 字节无符号整数,相同类型的若干条数据集合用表(table)的形式来存储。表是一个变长的结构,由表明长度的表头(n)和 紧随着的 n 个数据项组成。class 文件采用相似 C 语言的结构体来存储数据,以下所示:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
复制代码
class文件由下面十个部分组成
常量池是class文件中最复杂的数据结构,它是一个变长度的数据项
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
复制代码
主要分为两部分,一个是常量池大小,一个是常量池的集合数据
Java 虚拟机目前一共定义了 14 种常量类型,这些常量名都以 "CONSTANT" 开头,以 "info" 结尾,以下表所示:
常量类型 | 常量值 |
---|---|
CONSTANT_Utf8_info | 1 |
CONSTANT_Integer_info | 3 |
CONSTANT_Float_info | 4 |
CONSTANT_Long_info | 5 |
CONSTANT_Double_info | 6 |
CONSTANT_Class_info | 7 |
CONSTANT_String_info | 8 |
CONSTANT_Fieldref_info | 9 |
CONSTANT_Methodref_info | 10 |
CONSTANT_InterfaceMethodref_info | 11 |
CONSTANT_NameAndType_info | 12 |
CONSTANT_MethodHandle_info | 15 |
CONSTANT_MethodType_info | 16 |
CONSTANT_InvokeDynamic_info | 18 |
每一个常量项都由两部分构成,表示类型的 tag 和表示 内容 的字节数组
cp_info {
u1 tag;
u1 info[];
}
复制代码
再看calss文件的十六进制表示
查看类文件的常量池,能够在 javap 命令上加上 -v 选项,下面是 Hello.class文件的常量池
xiasmdeMacBook-Pro:test xiasm$ javap -v Hello
警告: 二进制文件Hello包含com.xiasm.asmdemo.classtest.Hello
Classfile /Users/xiasm/Desktop/asm_test/Hello.class
Last modified 2020-1-16; size 443 bytes
MD5 checksum a9f2551fb88a0a34395ac7cf0a0eedd3
Compiled from "Hello.java"
public class com.xiasm.asmdemo.classtest.Hello
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello World
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // com/xiasm/asmdemo/classtest/Hello
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Hello.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello World
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 com/xiasm/asmdemo/classtest/Hello
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
//... 省略其余信息
SourceFile: "Hello.java"
复制代码
Hello.java文件里没有Long或Double类型的常量,因此n-1=28个常量项,没毛病。
常量池以后存储的是访问标记(Access flags),用来标识一个类是是否是final、abstract 等,由两个字节表示总共能够有 16 个标记位可供使用,目前只使用了其中的 8 个,以下:
这三个部分用来肯定类的继承关系,this_class 表示类索引,super_name 表示父类索引,interfaces 表示类或者接口的直接父接口。以 this_class 为例,它是一个两字节组成。
紧随接口索引表以后的是字段表(Fields),类中定义的字段会被存储到这个集合中,包括类中定义的静态和非静态的字段,不包括方法内部定义的变量,字段表也是一个变长结构,以下图所示:
每一个字段 field_info 的格式以下:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
复制代码
字段结构分为四部分:
在字段表后面的是方法表,类中定义的方法会被存储在这里,与前面介绍的字段表很相似,方法表也是一个变长结构
{
u2 methods_count;
method_info methods[methods_count];
}
复制代码
由表示方法个数的 methods_count 和对应个数的方法项集合组成,以下图所示:
方法 method_info 结构分为四部分:
在方法表以后的结构是 class 文件的最后一步部分属性表。属性出现的地方比较普遍,不止出如今字段和方法中,在顶层的 class 文件中也会出现。注意,此属性表存放的不是咱们理解的类里面的成员属性,而是class文件定义的属性,如 ConstantValue 属性、Code 属性等。
Java 虚拟机的指令由一个字节长度的操做码(opcode)和紧随其后的可选的操做数(operand)构成。“字节码”这个名字的由来也是由于操做码的长度用一个字节表示
<opcode> [<operand1>, <operand2>]
复制代码
好比将整型常量 100 压栈到栈顶的指令是bipush 100,其中 bipush 就是操做码,100 就是操做数。
由于操做码长度只有 1 个字节长度,这使得编译后的字节码文件很是小巧紧凑,但同时也直接限制了整个 JVM 操做码指令集的数量最多只能有 256 个,目前已经使用了 200+
字节码并非某种虚拟 CPU 的机器码,而是一种介于源码和机器码中间的一种抽象表示方法,不过字节码经过 JIT(Just in time)技术能够被进一步编译成机器码。