JVM-Class文件结构
用hex editor可以打开.class文件,
或者vim -b xxx.class 然后进入末行模式, :%!xxd 则可以看到 cafe babe......
Jdk8 00 00 00 34 相当于十进制52, 看下面 major version: 52 代表使用的jdk8编译的。
mac下面可以下载010 editor 工具查看。很好用。
警告: 二进制文件Test包含club.wujingjian.stack.Test
Classfile /Users/apple/coding/uaa/datastructure/target/classes/club/wujingjian/stack/Test.class
Last modified 2021-3-11; size 2148 bytes
MD5 checksum 5552637dd3404991c0d739a0a71a4f73
Compiled from "Test.java"
public class club.wujingjian.stack.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #31.#69 // java/lang/Object."<init>":()V
#2 = Class #70 // java/util/ArrayList
#3 = Methodref #2.#69 // java/util/ArrayList."<init>":()V
#4 = String #71 // +
#5 = InterfaceMethodref #72.#73 // java/util/List.add:(Ljava/lang/Object;)Z
#6 = String #74 // -
#7 = String #75 // *
#8 = String #76 // /
#9 = String #77 // ,
#10 = Methodref #78.#79 // java/lang/String.split:(Ljava/lang/String;)[Ljava/lang/String;
在class文件中会包含:
魔数 副版本号 主版本号 常量池计数器 常量池数据区(cp_info) 访问标志 类索引 父类索引 接口计数器 等等
常量池计数器是从1开始计数的,而不是从0开始(第0项空出来有特殊考虑,为了满足某些指向常量池的索引值的数据在特定的情况下表达"不引用任何一个常量池"的意思,这种情况下可以将索引值设置为0来表示),如果常量池计数器 constant_pool_count = 22, 则后面的常量池项(cp_info) 的个数就位21,
class常量池
编译期间生成的,存储于class文件中。
运行时常量池和字符串常量池 是 存储在JVM内存中的, 他们的数据来源于 class常量池。
常量池位置:
class文件中前四个字节是 ca fe ba be
接下来4个字节是 jdk 版本号
接下来两个字节是 常量池计数器
接下来是就是 常量池数据区了。
常量池内部是如何组织的
cp_info 常量池项
constant_pool_count: 常量池计数器
常量池项的结构
常量池项: 包含字面量 (literal) 和 符号引用 (symbolic reference)
字面量又包含: 文本字符串、被声明为final的常量值、基本数据类型的值、其他
符号引用包含: 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符
cp_info {
u1 tag;
u1 info [];
}
tag的值来确定某个常量池项标识什么类型的字面量
Tag值 | 标识的字面量 | 更细化的结构 |
---|---|---|
1 | 用于表示字符串常量的值 | CONSTANT_Utf8_info |
3 | 表示4字节(int)的数值常量 | CONSTANT_Integer_info |
4 | 表示4字节(Float)的数值常量 | CONSTANT_Float_info |
5 | 表示8字节的(Long)的数值常量 | CONSTANT_Long_info |
6 | 表示8字节(Double)的数值常量 | CONSTANT_Double_info |
7 | 表示类或接口的完全限定名 | CONSTANT_Class_info |
8 | 用于表示java.lang.String类型的常量对象 | CONSTANT_String_info |
9 | 表示类中的字段 | CONSTANT_Fieldref_info |
10 | 表示类中的方法 | CONSTANT_Methodref_info |
11 | 表示类所实现的接口的方法 | CONSTANT_InterfaceMethodref_info |
12 | 表示字段或方法的名称和类型 | CONSTANT_NameAndType_info |
15 | 表示方法句柄 | CONSTANT_MethodHandle_info |
16 | 表示方法类型 | CONSTANT_MethodType_info |
18 | 用于表示invokedynamic 指令所使用到的引导方法(Bootstrap Method)、引导方法使用到动态调用名称(Dynamic Invocation Name) 、参数和请求返回类型、以及可以选择性的附加被称为静态参数(Static Arguments)的常量序列 | CONSTANT_InvokeDynamic_info |
Eg: int float类型在常量池中的定义
public class IntAndFloatTest {
private final int a = 10;
private final int b = 10;
private float c = 11f;
private float d = 11f;
private float e = 11f;
}
使用 javap -v IntAndFloatTest 查看,虽然定义了5个变量,但是常量值 Constant Pool中只有一个常量10 和一个常量11f.
Constant pool:
Xxxxxx
constant #8=int 10;
constant #23 = float 11.0f;
代码中用到int 类型10的地方,会使用常量池的指针值#8定位到第#8 个常量池项(cp_info),即值为10的结构体CONSTANT_Integer_info; 11f也同理。
String类型字符串常量在常量池中表示和存储?
CONSTANT_String_info{
u1 tag =8;
u2 string_index;
}
结构体中占用2个字节的string_index值指向了某个CONSTANT_Utf8_info结构体,即:string_index 的值是某个CONSTANT_Utf8_info结构体在常量池中的索引
Eg:
public class StringTest {
private String s1 = "JVM原理";
private String s2 = "JVM原理";
private String s3 = "JVM原理";
private String s4 = "JVM原理";
}
使用javap -v StringTest查看常量池信息
$ javap -v StringTest
警告: 二进制文件StringTest包含club.StringTest
Classfile /Users/apple/coding/uaa/datastructure/target/classes/club/StringTest.class
Last modified 2021-4-20; size 433 bytes
MD5 checksum dfb662b4ca52463103405204a6f35449
Compiled from "StringTest.java"
public class club.StringTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#23 // java/lang/Object."<init>":()V
#2 = String #24 // JVM原理
#3 = Fieldref #7.#25 // club/StringTest.s1:Ljava/lang/String;
#4 = Fieldref #7.#26 // club/StringTest.s2:Ljava/lang/String;
#5 = Fieldref #7.#27 // club/StringTest.s3:Ljava/lang/String;
#6 = Fieldref #7.#28 // club/StringTest.s4:Ljava/lang/String;
#7 = Class #29 // club/StringTest
#8 = Class #30 // java/lang/Object
#9 = Utf8 s1
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 s2
#12 = Utf8 s3
#13 = Utf8 s4
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 Lclub/StringTest;
#21 = Utf8 SourceFile
#22 = Utf8 StringTest.java
#23 = NameAndType #14:#15 // "<init>":()V
#24 = Utf8 JVM原理
#25 = NameAndType #9:#10 // s1:Ljava/lang/String;
#26 = NameAndType #11:#10 // s2:Ljava/lang/String;
#27 = NameAndType #12:#10 // s3:Ljava/lang/String;
#28 = NameAndType #13:#10 // s4:Ljava/lang/String;
#29 = Utf8 club/StringTest
#30 = Utf8 java/lang/Object
{
public club.StringTest();
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: ldc #2 // String JVM原理
7: putfield #3 // Field s1:Ljava/lang/String;
10: aload_0
11: ldc #2 // String JVM原理
13: putfield #4 // Field s2:Ljava/lang/String;
16: aload_0
17: ldc #2 // String JVM原理
19: putfield #5 // Field s3:Ljava/lang/String;
22: aload_0
23: ldc #2 // String JVM原理
25: putfield #6 // Field s4:Ljava/lang/String;
28: return
LineNumberTable:
line 3: 0
line 4: 4
line 5: 10
line 6: 16
line 7: 22
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lclub/StringTest;
}
SourceFile: "StringTest.java"
#2 = String #24 // JVM原理 这个是CONSTANT_String_info
#24 = Utf8 JVM原理 这个是CONSTANT_UTF8_INFO
类文件中定义的类名和各种使用到的类在常量池中是怎样被组织和存储的?
JVM会将某个java类中所有使用到了的类的完全限定名 以 二进制形式的完全限定名 封装成constant_class_info结构体中,然后将其放置到常量池中。
CONSTANT_CLASS_info {
u1 tag=7;
u2 name_index;
}
例如:
package club;
import java.util.Date;
public class ClassTest {
private Date date = new Date();
}
javap -v ClassTest
$ javap -v ClassTest
警告: 二进制文件ClassTest包含club.ClassTest
Classfile /Users/apple/coding/uaa/datastructure/target/classes/club/ClassTest.class
Last modified 2021-4-20; size 346 bytes
MD5 checksum 541dacbfcc6b1b077533700a290f3ed8
Compiled from "ClassTest.java"
public class club.ClassTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // java/util/Date
#3 = Methodref #2.#18 // java/util/Date."<init>":()V
#4 = Fieldref #5.#20 // club/ClassTest.date:Ljava/util/Date;
#5 = Class #21 // club/ClassTest
#6 = Class #22 // java/lang/Object
#7 = Utf8 date
#8 = Utf8 Ljava/util/Date;
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lclub/ClassTest;
#16 = Utf8 SourceFile
#17 = Utf8 ClassTest.java
#18 = NameAndType #9:#10 // "<init>":()V
#19 = Utf8 java/util/Date
#20 = NameAndType #7:#8 // date:Ljava/util/Date;
#21 = Utf8 club/ClassTest
#22 = Utf8 java/lang/Object
{
public club.ClassTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/util/Date
8: dup
9: invokespecial #3 // Method java/util/Date."<init>":()V
12: putfield #4 // Field date:Ljava/util/Date;
15: return
LineNumberTable:
line 5: 0
line 6: 4
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lclub/ClassTest;
}
SourceFile: "ClassTest.java"
上述 #2 #5 #6 这三行都是 =Class 分别是 Date类,ClassTest本身 ,以及Object类(所有类都继承自Object),
其中#5这一行代表ClassTest类 它的结构体CONSTANT_CLASS_INFO中 ,它在常量池中年的位置是#5,它的name_index值是#21,它(#21)指向了常量池的第21个常量池项,这第21项里面存储了以utf8编码的"club/ClassTest"字符串。
注:
任何Class文件中至少有两个CONSTANT_CLASS_info常量池项(因为有它本身,以及直接父类,如果没有声明直接父类那就是Object类,) 如果类声明实现了某些接口,那么接口的信息也会生产对应的CONSTANT_CLASS_INFO常量池项。
只有真正使用到的类才会编译到class的常量池中,如果只定义而未使用则不会编译到class文件中,比如:
package club.jvm.structure;
import java.util.Date;
public class Person extends Animal {
private Date date;
public Person() {
Date date1;
}
}
上述代码中虽然定义时候用到了Date类,但是并未使用,所以javap -v Person时候看常量池中并没有Date类的Constant_class_info信息。依然只有两个CONSTANT_CLASS_INFO(Person以及Animal)
哪些字面量能够进入常量池中?
- final类型的8种基本类型的值都会进入常量池
- 非final类型的(包括static的)8种基本类型的值,只有double、float、long的值会进入常量池
- 常量池中包含的字符串类型字面量(双引号引起来的字符串值)
Class文件中的引用和特殊字符串
符号引用
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能够无歧义定位到目标即可。
例如,在class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。
符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存。在编译时,java类并不知道引用的实际地址,因此只能使用符号引用来代替。
直接引用
直接引用可以是:
- 直接指向目标的指针(比如,指向"类型"【class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
- 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
- 一个能间接定位到目标的句柄
直接引用和虚拟机的布局相关,同一个符号的引用在不同的虚拟机实例上翻译出来的=直接引用=一般不会相同,如果有了直接引用,那引用的目标必定已经被加载入内存中了。
引用替换的时机
符号引用替换为直接引用的操作发生在类加载过程(加载 - > 连接(验证、准备、解析) -> 初始化)中的=解析阶段=,会将符号引用替换为对应的直接引用,放入=运行时常量池=中。
特殊字符串字面量
特殊字符串包括三种:类的全限定名,字段和方法的描述符,特殊方法的方法名。
类的全限定名
Object类,在源文件中的全限定名是java.lang.Object, class文件中的全限定名将"."替换为"/",变为java/lang/Object .(源文件中的一个类的名字,在class文件中是用全限定名表述的)
描述符
各类型的描述符
对于字段的数据类型,描述符主要以下几种
- 基本数据类型(byte, char, double, float, int, long, short, boolean): 除long 和boolean ,其他基本数据类型的描述符用对应单词的大写首字母标识。long 用
J
表示, boolean 用Z
表示。 - Void 描述符是
V
. - 对象类型: 描述符用字符
L
加上对象的全限定名表示,如 String 类型的描述符为 Ljava/lang/String; - 数组类型: 每增加一个维度则在对应的字符描述符前增加一个
[
, 如一位数组int []
的描述符为[I
,二维数组String [][]
的描述符为[[Ljava/lang/String;
.
数据类型 | 描述符 |
---|---|
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
boolean | Z |
特殊类型void | V |
对象类型 | "L" + 类型的全限定名 + ";"。 如 Ljava/lang/String; 表示 String类型 |
数组类型 | 若干个 "["+ 数组中元素类型的对应字符串,如一维数组 int[] 的描述符为 [I , 二维数组 String[][] 的描述符为 [[Ljava/lang/String; |
字段描述符
字段的描述符就是字段的类型所对应的字符或字符串
eg: int i 中,字段i的描述符是 I
Object o中,字段o的描述符是 Ljava/lang/Object;
double [][]d
中, 字段d的描述符就是 [[D
方法描述符
方法描述符包含所有的参数类型列表和方法返回值。格式为:
(参数1类型 参数2类型 参数3类型...)返回值类型
不管参数类型还是返回值类型,都使用对应字符和对应字符串来表示,并且参数类别用小括号括起来,各参数直接没有空格,参数列表和返回值类型之间也没有空格
方法描述符示例
方法描述符 | 方法声明 |
---|---|
()I | int getSize() |
()Ijava/lang/String | String toString() |
([Ljava/lang/String)V | void main(String[] args) |
()V | void wait() |
(JI)V | void wait(long timeout, int nanos) |
(ZILjava/lang/String;II)Z | boolean regionMatches(boolean ignoreCase, int toOffset, String ther, int offset, int len) |
([BII)I | int read(byte[] b, int off, int len) |
()[[Ljava/lang/Object | Object [][] getObjectArray() |
特殊方法的方法名
特殊方法指: 类的构造方法 和 类型初始化方法(==静态初始化块==)
静态初始化块,在class文件中是以一个方法标识的,这个方法同样有方法描述符和方法名,具体如下:
- 类的构造方法的方法名使用字符串
<init>
表示 - 静态初始化方法的方法名使用字符串
<clinit>
表示 - 除了这两种特殊的方法外,其他普通方法的方法名,和源文件中的方法名相同。
总结:
- 方法和字段的描述符中,不包括字段名和方法名,字段描述符中只包括字段类型,方法描述符中只包括参数列表和返回值类型
- 无论method()是静态方法还是实例方法,它的方法描述符都是相同的。尽管实例方法除了传递自身定义的参数,还需要额外传递参数this,但是这一点不是由方法描述符来表达的。参数this的传递,是由java虚拟机实现在调用实例方法所使用的指令中实现的隐式传递。
javap命令分析java指令
javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区
(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
当然这些信息中,有些信息(如本地变量表、指令和代码行偏移量映射表、常量池中方法的参数名称等
等)需要在使用javac编译成class文件时,指定参数才能输出,比如,你直接javac xx.java,就不
会在生成对应的局部变量表等信息,如果你使用javac -g xx.java就可以生成所有相关信息了。如果
你使用的eclipse,则默认情况下,eclipse在编译时会帮你生成局部变量表、指令和代码行偏移量映
射表等信息的。
通过反编译生成的汇编代码,我们可以深入的了解java代码的工作机制。比如我们可以查看i++;这行
代码实际运行时是先获取变量i的值,然后将这个值加1,最后再将加1后的值赋值给变量i。
通过局部变量表,我们可以查看局部变量的作用域范围、所在槽位等信息,甚至可以看到槽位复用等信
息。
$ javap -help
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
一般常用的是-v -l -c三个选项。
javap -v classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用
到的常量池等信息。
javap -l 会输出行号和本地变量表信息。
javap -c 会对当前class字节码进行反编译生成汇编代码
查看汇编代码时,需要知道里面的jvm指令,可以参考官方文档:
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
另外通过jclasslib工具也可以看到上面这些信息,而且是可视化的,效果更好一些
总结
- 通过javap命令可以查看一个java类反汇编、常量池、变量表、指令代码行号表等等信息。
- 平常,我们比较关注的是java类中每个方法的反汇编中的指令操作过程,这些指令都是顺序执行
的,可以参考官方文档查看每个指令的含义,很简单:
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.areturn
- 通过对前面两个例子代码反汇编中各个指令操作的分析,可以发现,一个方法的执行通常会涉及下
面几块内存的操作:
(1)java栈:局部变量表、操作数栈。这些操作基本上都值操作。
(2)java堆:通过对象的地址引用去操作。
(3)常量池。
(4)其他如帧数据区、方法区(jdk1.8之前,常量池也在方法区)等部分,测试中没有显示出来,这
里说明一下。
在做值相关操作时:
一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可
能是指,可能是对象的引用)被压入操作数栈。
一个指令,也可以从操作数数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系
统调用等等操作。
十六进制转字符串:http://www.bejson.com/convert/ox2str/
进制转换网址(十六进制转十进制):http://tool.oschina.net/hexconvert/