概览
上一篇文章中,讲了如何通过双亲委派机制查找到类文件。但是当你用文件编辑器(比如sublime打开),你就只能感叹一句
啥啥啥,这写的都是啥???
这篇文章就来讲,这些内容到底写的都是啥。也就是类载入这一步的解析过程
类文件的结构
结构总览
记得当初上java课的时候,老师曾经说过:类文件的信息很少,可以忽略不计。现在想想,里面的内容还真不少呢:
1.类文件基本信息:文件的魔数(后面会讲)、主版本、次版本
2.类常量池:用于存放类常量数据。注意,不是、不是、不是字符串常量池!,重要事情说三遍
3.类属性池:
(1)类的修饰符和名称
(2).类在常量池的索引:没错。类也作为一个常量的形式存放在常量池中
(3).类的父类:当然,java.lang.Object没有父类,基本数据类型也没有,这也是为啥泛型不能使用基本数据类型的原因
4.类接口池:存放类实现了哪些接口
5.类字段表:存放类的变量。获取类的字段就是通过字段表实现滴!
6.类方法表:存放与类方法相关的内容。所谓执行类的方法就是解析方法表里面的JVM指令并执行。这块是类文件的大头。
私以为类区别名方法区就是这个原因(当然现在叫元数据区了)
1.类基本信息
这块是.class文件最简单的地方了。只存放类的基本结构。
第一块是魔数(Magic Number)。所谓的魔数,在这里的作用就是用来判断文件的类型。.class文件的魔数为16进制的4个字节码:ca fe ba be,也就是咖啡宝贝,Java之父玩的什么梗,各位了解Java名称由来的大佬们懂的都懂,不细说。
主版本和次版本:各2个字节,用来校验jvm支不支持类文件格式,这里也略过。
2.类文件常量池
前言——类文件常量池与字符串常量池
再说一次,这不是字符串常量池!Java面试典中典的问题就是,运行过程中常量池大小是否固定。
如果是字符串常量池,毫无疑问是会变的,但类文件的常量池可不会(很明显嘛,类文件大小都固定好了)
因为:类文件常量池存放的是在编译过程中就确定好的常量,而不是运行过程中产生的变量。比如如下代码
public class A{
int i = 10;
int j = 114514;
public void method(){
String a = "abc";
String b = "b" + "cd";
for(int k = i; k < j - 114503; ++ k){ //行1
a += b; //行2
} //行3
}
}
(1).10 不会存放到类常量池里,虽然10在编译过程中已经确定,但10小于2个字节,出于优化考虑不会放入常量池,而是使用JVM字节码的方式进行赋值
(2).114514 会存放到类常量池里,因为在编译过程中已经确定,且超过了2字节,114503同理
(3)."abc" 编译过程中已经确定,会存放到类常量池
(4)."b" + "cd" 编译过程中优化为 "bcd",也就是编译过程中能确定数值,会放入类常量池里
(5).行1~3执行的代码,也就是 "abcbcd" 这是在执行过程中产生的字符串,因此不会放入类常量池,而是放入字符串常量池
(6).类A和方法method也使用到了,因此也会存放到类常量池。
所以,明白了吧。类文件常量池在编译的时候就确定了,因此大小不变;
字符串常量池存放的是在运行过程中产生的字符串,因此大小会变。
(1)常量池的结构
常量池两个字节(无符号)表示常量数量n,后面跟着n个类型各异的常量类型和信息(这些常量有自己结构,能和别人区分开来)。简而言之如下:
常量数量n(两个字节) 常量1类型,常量1、常量2类型,常量2、、...常量n类型,常量n
其中常量类型用一个字节表示(具体为1、3~12、15、16、18)
无符号2字节可以表示2^16 = 65536个常量,额,够用了吧,不会有哪个神仙的类能整个6万5千多个常量吧?不会吧?
(2)常量的类型
常量可以分为如下几类
类型1 字面量(UTF8):存放的内容为utf8字符串。
结构为 utf8长度n(2字节,长度2^16) 字节码1、字节码2、...、字节码n。
比如你代写了个String a = "Hello abc jvm",在常量池里就会对应生成一个内容为"Hello abc jvm"的utf8常量,.class存放为:utf8长度13,后面跟着13个字节码,表示Hello abc jvm。
注意,这不是String,这是UTF8! 类型8才是String,因为常量中可不单单String会引用UTF8,并且UTF8的长度远远小于String的长度。
没人会在java代码里写2^16这么长的字符串,所以设定为2^16。
但在运行过程中,String长度可能轻轻松松超过2^16,因此String的长度只受制于char数组的长度。是的,数值也是有长度上限的!!
类型2:额,没有类型2,别问我为啥,我也不知。
类型3 Integer:存放4字节有符号的整数数据,因此结构为4个字符。在常量池中以一个4字节数据的形式存放
类型4 Float:同上。不过4个字节表示的是float而已。
类型5 Long:8字节,表示8字节有符号的整数数据。结构为8个字符在常量池中以两个4字节数据的形式存放。
类型6 Double:同上,不过存放的是double而已。
类型7 类引用(ClassRef):存放类的符号引用,也就是类的名字。根据类的名字这个符号可以加载这个类。
结构为类名称索引(2字节),这个索引指向常量池中的一个字面量,这个字面量就是类的全限定类名。
比如存放类"Student"的符号引用,这个类在school包下,这样类的全限定名就是school/Student。
在类常量池中,会存在一个内容为school/Student的UTF8类型,假设这个UTF8的索引为12,那么这个类型会用两个字节存放12这个数值
类型8 字符串(String):结构也是2字节索引,这个索引指向的是String引用的UTF8。再说一遍,String不等于UTF8,能引用UTF8的类型很多
类型9 字段索引:所谓字段,也就是类的变量。结构为字段所属类的引用在常量池的索引(2字节),字段名称和属性在常量池的索引(2字节) 具体后面讲,记住,结构就是4字节
类型10 方法引用(MethodRef):和类引用类似,但是多了一个索引,指向方法名和参数列表
结构:4个字节。 2个字节代表方法所属类在常量池中的索引,2个字节代表方法名和参数列表在常量池中的索引(方法名和参数列表用UTF8代替)
参数列表的参数类型代号有点意思,这里讲一下。
[ : 代表数组
LXXX; : 代表是XXX对象,XXX是该对象的类的全限定名称,要用逗号分割
B : 代表byte
C : 代表char
I : 代表int
F : 代表float
D : 代表double
J : 代表long
Z : 代表boolean
举例:
main(String[] arg) --> main([Ljava/lang/String;)
method(int i,String j,byte[] f,long k, Object c) --> method(ILjava/lang/String;[BJLjava/lang/Object;)
类型11 接口方法(InterfaceMethodRef):同方法引用。我的理解是,这个是抽象类/接口里面的抽象方法
类型12 名字和属性索引(NameAndAttrRef):同,结构为4字节两个索引。这两个索引分别指向名字(UTF8)和属性说明(还是UTF8)
类型15 句柄(MethodHandle):结构为3个字节,用不到,跳过
类型16 标志方法:结构为2个字节,用不到,跳过
类型18 动态调用点:结构为4个字节,用不到,还跳过
总结
本来想一口气讲完类文件解析原理的,实在没想到才写完常量池,就这么多内容了。那就先写到这里吧
这篇文章主要讲解的内容为:
(1).class文件结构:.class文件的字节码主要分为哪几块
(2)类基础信息: 类文件的魔数、主次版本
(3)类文件常量池: 与字符串常量池的区别、常量池结构、常量类型和结构