开篇
大家好,今天是2022年的第一天,在这里祝大家在新的一年里工作顺利,bug少少,也祝大家的技术更上一层楼,我们一起进步,加油!
今天给大家带来的是Class文件,上篇博客我们初步认识了JVM,当然,为了更好的学习后面的内容,熟悉class文件也是非常重要的。内容很多,干货满满,一定要看完哦。防止下次找不到也可以收藏哦。
class文件介绍
一、class里都存了什么数据
查看字节码文件,字节码我们是看不懂的,但是可以通过转成16进制,虽然也不是很看得懂,但可以从中找到一些有用的信息,如下效果:
在这里给大家推荐几个转换的网站:
在线HEX Editor: https://www.onlinehexeditor.com/
转换为16进制的
●十六进制转字符串: http://www.bejson.com/ convert/ox2st/
●十六进制转十进制: http://tool.oschina.net/hexconvert/
字节码文件结构示意图
大家可以参考下图去阅读上面的十六进制文档:
魔数:(咖啡宝贝)判断是否为Java所写的代码
主版本号与副版本号:(编译版本)主版本号是1.8,副版本号是1.8.几,代码如果编译是1.8,运行是不能低于1.8
常量池计数器:标记后面多少个字节是常量池区域
访问标志:例如public/private等访问权限,记录类的信息
类索引:(引用)怎样指向自己的类信息
父类索引:每一个类都有父类,
接口计数器:因为类可以是多实现,不确定的因素都有计数器
IDEA 中有一个叫jclasslib的插件,可以更清晰的帮我们看到一些信息,有兴趣的可以去装上
二、class常量池如何存储数据
1.常量池的常量有哪些
分为字面量和符号引用
2.常量池的里面是怎么组织的?
cp_ info:常量池项
constant_ pool. count: 常量池计算器
3.常量池项(Cp_ info) 的结构是什么?
JVM虚拟机规定了不同的tag值和不同类型的字面量对应关系如下:
所以根据cp_ info中 的tag不同的值,可以将cp_ info 更细化为以下结构体:
现在让我们看一下细化了的常量池的结构会是类似下图所示的样子:
4.如何存储int和float数据类型的常量?
public class IntAndFloatTest {
private final int a = 10;
private final int b = 10;
//private int c = 20;
private float c = 11f;
private float d = 11f;
private float e = 11f;
}
5.如何存储long和double数据类型的常量?
public class LongAndDoubleTest {
private long a = -6076574518398440533L;
private long b = -6076574518398440533L;
private long c = -6076574518398440533L;
private double d = 10.1234567890D;
private double e = 10.1234567890D;
private double f = 10.1234567890D;
}
通过javap -V 类名 看一下常量池中的信息,虽然代码中有三次 -6076574518398440533L,但是常量池中只有一个
6.如何存储String类型的字符串常量?
双引号引起来的值,会存储成 CONSTANT_String_info结构体中
真正的值存储在CONSTANT_Utf8_info中
public class StringTest {
private String s1 = "JVM原理";
private String s2 = "JVM原理";
private String s3 = "JVM原理";
private String s4 = "JVM原理";
}
7.如何存储Class引用类型?
JVM会将某个Java类中所有使用到了的类的完全限定名以二进制形式的完全限定名 封装成CONSTANT_Class_info结构体中
然后将其放置在常量池中
public class ClassTest {
private Date date =new Date();
}
如图所示:共有3个CONSTANT_Class_info结构体,表示Class Test用到的Class信息
public class Other{
private Date date;
public Other() {
Date da;
}
}
public class Other{
public Other() {
new Date();
}
}
这时候使用javap -v Other,可以查看到常量池中有表示java/util/Date的常量池项:
8.哪些字面量会进入常量池中?
测试代码:
public class Test{
private int int_num = 110;
private char char_num = 'a';
private short short_num = 120;
private float float_num = 130.0f;
private double double_num = 140.0;
private byte byte_num = 111;
private long long_num = 3333L;
private long long_delay_num;
private boolean boolean_flage = true;
public void init() {
this.long_delay_num = 5555L;
}
}
使用javap命令打印的结果如下:
结论:
- 【final修饰】的8种基本类型的值会进⼊常量池。
- 【⾮final类型】(包括static的)的8种基本类型的值,只有 【double、float、long】的值会进⼊常量池。
- 常量池中包含的字符串类型字⾯量(【双引号引起来的字符串值】)。
三、符号引用和直接引用
符号引用(class文件中)
在Java中,一个java类将会编译成一个class文件。在编译时,java类 并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
比如org. simple. People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能 使用符号org.simple.Language (假设是这个,当然实际中是由类似于CONSTANT_Class_ info的常量来表示的)来表示Language类的地址。
直接引用(运行时内存中)
直接引用可以是:
1.直接指向目标的指针(比如,指向“类型”[Class对象]、类变量、类方法的直接引用可能是指向方法区的指针)
2.相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
3.一个能间接定位到且标的句柄。
引用替换的时机
1、类加载的解析阶段(需要将一部分符号引用转换为直接引用)
符号引用替换为直接引用的操作发生在类加载过程(加载->连接(验证、准备、解析)->初始化)中的解析阶段,会将符号引用转换
(替换)为对应的直接引用,放入运行时常量池中。
2、运行期间(动态分配)
四、Class中的特殊字符串
类的全限定名
比如Object类,在源文件中的全限定名是java.lang.object。
而class文件中的全限定名是将点号替换成“/”,也就是java/lang/object。
描述符
各类型的描述符
对于字段的数据类型,其描述符主要有以下几种
8种基本数据类型:除long和boolean,其他都用对应单词的大写首字母表示。long 用J表示,boolean 用Z表示。
void:描述符是V。
●对象类型:描述符用字符L加上对象的全限定名表示,如String类型的描述符为Ljava/lang/String。
●数组类型:每增加一个维度则在对应的字段描述符前增加一个[,
如一维数组 int[]的描述符为[I,二维数组String[][ ]
的描述符为[ [Ljava/ lang/String。
字段描述符
字段的描述符就是字段的类型所对应的字符或字符串。
如:
int i 中, 字段i的描述符就是 I
Object o中, 字段o的描述符就是 Ljava/lang/Object;
double[][] d中, 字段d的描述符就是 [[D
方法描述符
方法的描述符比较复杂,包括所有参数的类型列表和方法返回值。它的格式是这样的:
(参数1类型 参数2类型 参数3类型 ...)返回值类型
注意事项:
不管是参数的类型还是返回值类型,都是使用对应字符和对应字符串来表示的,并且参数列表使用小括号括起来,并且各个参数类型之间没有空格,参数列表和返回值类型之间也没有空格。
方法描述符举例说明如下:
特殊方法的方法名
类的构造方法和类型初始化方法。
构造方法就不用多说了,至于类型的初始化方法,对应到源码中就是静态初始化块。也就是说,静态初始化块,在class文件中 是以
一个方法表述的,这个方法同样有方法描述符和方法名,具体如下:
●类的构造方法的方法名使用字符串 表示
●静态初始化方法的方法名使用字符串 表示。
●除了这两种特殊的方法外,其他普通方法的方法名,和源文件中的方法名相同。
五、JavaP命令
javap是jdk 自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代
码行偏移量映射表、常量池等等信息。
javap的用法格式:
javap <options> <classes>
其中classes就是你要反编译的class文件。
在命令行中直接输入javap或javap -help可以看到javap的options有如下选项:
-help --help -? 输出此⽤法消息
-version 版本信息,其实是当前javap所在jdk的版本信息,不是class在哪个jdk下⽣成的。
-v -verbose 输出附加信息(包括⾏号、本地变量表,反汇编等详细信息)
-l 输出⾏号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类 和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进⾏反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的系统信息 (路径, ⼤⼩, ⽇期, MD5 散列)
-constants 显示静态最终常量
-classpath <path> 指定查找⽤户类⽂件的位置
-bootclasspath <path> 覆盖引导类⽂件的位置
一般常用的是-v -l -c三个选项。
●javap -V classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,
还会输出当前类用到的常量池等信息。
●javap -l会输出行号和本地变量表信息。
●javap -c会对当前class字节码进行反编译生成汇编代码。