问题:
1、是不是只有Java编译器才能完成Java到class字节码文件的编译过程?
不是,像Java/JRuby/Groovy等程序都可以通过自己的编译器转成字节码(class)文件,然后交给JVM。
2、什么是u1、u2、u4、u8。
- u1:一个子字节。
- u2:两个子字节。
- u4:四个子字节。
- u8:八个子字节。
Class文件组成内容:
虚拟机指令、符号表、其它复制信息(这三个都由两种数据结构组成,无符号数、表)。
无符号数:数值。
表:xxx_info。
Class文件格式:
u4 u2 u2 u2 cp_info u2 u2 u2 u2 u2 u2 field_info u2 method_info u2 attribute_info
解释:
1、魔数:
每个Class文件的头四个字节数为魔数,唯一作用是确定这个文件是否是能被虚拟机接受的Class文件。
目前版本的值都是固定的,0xCAFEBABE(咖啡宝贝)。
2、紧接着魔数后面的4个字节就是Class文件的版本号:5-6字节,次版本号;7-8字节主版本号。
3、Class文件版本号后面紧随的是常量池入口。
常量池主要分为两大类:
- 字面量:与Java中的常量概念类似;int a = 3,字面量就是等号右边的东西。
- 符号引用:符号引用包含三类常量。
- 类和接口的全限名称:org.springframework.xxxBean。
- 字段名称和描述符:private/protected/public。
- 方法的名称和描述符:private/protected/public。
常量池:
常量池结构表:
访问标志:
分析示例:
public class Test extends Thread implements Serializable { public int count(int sum) { int i = sum = 1; return i; } }
以上代码生成class文件用二进制格式打开后如下(部分):
前置说明,address 00000000记为00,00000010记为10
1、[00, 0]至[00, 7]如上所述为魔数和版本号,[00, 8]至[00, 9]为常量池计数器,换算下来为22,但因为常量池数量为常量池计数器-1,所以常量池的数量是21。
2、然后我们来看看常量池的数据:
从[00, a]开始,首先0a的10进制为10,对照上表是CONSTANT_Methodref_info。
我们将其列为:
tag = 10 index = 00 03 >>> 00 03 index = 13 07 >>> 19 07
以此类推,[00, f]:class_info
tag = 07
index = 00 14 >>> 00 20
[00, f]:class_info
tag = 07
index = 00 15 >>> 00 21
3、但其实我们不用一步步的分析,可以直接通过javap -v Test.class命令就可以了。
根据构造函数中invokespecial #1的入口分析后得出如下:
第一步:#1,然后我们找到常量池的#1,其中会先到#3再到#19。
第二步:#3,转到#21。
第三步:#19,转到#5,#6,以此类推。
其中我们可以看到<init>,这是对象实例的方法;如果是<clinit>则是类和接口初始化,如静态代码块的加载。
4、接下来就是访问标志、类索引、父类索引、接口计数器等等,它们都对应两个字节。
- 访问标志:[110, 5],[110, 6]两个字节,也就是00 21。因访问标志为public所以对应标志为0x0001,计算公式 >>> 访问标志1 | 访问标志2 | 访问标志n | ACC_SUPER的值。
- public class >>> 0x0001 | 0x0020 >>> 0x0021,所以对应了00 21。
- public abstract class >>> 0x0001 | 0x0400 | 0x0020 >>> 0x0421,你再重新编译便会发现值为04 21。
- 以此类推,只要对照访问标志的字典表就好了。
- 类索引:[110, 7],[110, 8] >>> 00 02,对应常量表#2,com/jdr/maven/rabbitmq/helloworld/Test。
- 父类索引:[110, 9],[110, a] >>> 00 03,对应常量表#3,java/lang/Thread。
- 接口计数器:[110, b],[110, c] >>> 00 01,也就是一个接口。
- 内容太多就不一一分析了,只需按图索骥便可。