我的JVM之旅-ClassFile

欢迎访问我的个人博客 http://rayleung.xyz/

开篇

搞IT有几年了,也学了不少框架中间件之类的东西。东西越学越多,越感觉迷茫,后来突然觉醒,发现弄懂技术的基础原理和算法这些基本的东西,才是自己继续往下走的根本。
偶然发现有一本书教人写JVM,叫自己动手写Java虚拟机,于是萌生把学习Jvm的过程记录下来,目标是能自己写一个最简单的JVM,能运行Java程序。
要说JVM,首先先认识一下Class文件这东西。

什么是Class文件

什么是Class文件这个问题听上去很SB,搞Java的人有谁不知道Class文件
不过对于初学者来说,搞清楚这个问题却很有意义

我们写的程序会通过Java编译器编译成字节码(ByteCode),因为字节码使得应用与平台无关,能一次编译到处运行
这些字节码一般保存在Class文件里面(Java虚拟机规范并没有强制字节码一定要存放在文件里面),因此虚拟机是与Class文件打交道的,与你用什么编程语言来编写程序没什么关系。一般来说我们默认是用Java语言来写Java程序,不过其实只要特定语言的编译器能够生成符合JVM规范的字节码,都是能在JVM上运行的,例如Scala。
无关性基石

Class文件格式

就像我还没了解Class文件结构之前一样,看到Class文件会有一股陌生感。
于是乎如果有一个直观的工具能观察到Class文件内部结构,那是有多好。
嗯,的确有这样的工具 —– classpy(点击传送到GitHub)
用法很简单,双击jar文件,然后选择编译好的class文件打开,就能看到效果,这里就不演示怎样打开了。
另外我们也可以用javap这个工具来查看输出的常量表。

编写java样例程序

为了研究Class文件,我们首先编写一个Java文件然后编译成Class文件

public class HelloWorld {

    private String name;

    private int age;

    public static void main(String[] args) {
        HelloWorld helloWorld = new HelloWorld();
        helloWorld.printInfo();
    }

    public void printInfo() {
        System.out.println("Hello:" + name + " age:" + age);
    }
}

编译后利用classpy打开Class文件
claapy
到这里我们对Class文件有了个感性的认识,接下来再来详细学习一下Class文件的结构

Class文件结构

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                  interface_count;    //接口计数器
    u2                  interfaces[interface_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文件是一组以8位字节为单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有分隔符,没有字符对齐。JVM规范使用Big-Endian来做顺序储存数据(参考)。上面采用类似C语结构的伪结构来存储数据,这种数据有两种类型:无符号数

无符号数属于基本类型,以u1、u2、u4、u8分别代表1字节、2字节、4字节和8字节。无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码组成的字符串

是有多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性地以info结尾。Class文件本质上就是一张表

魔数(Magic)

用来确定这个文件是否虚拟机能够接受的Class文件,固定值为0xCAFEBABE
magic

版本号

副版本号

副版本号

主版本号

主版本号

常量池

常量池记录了Class文件使用到的资源,例如引用到的字符串,类和接口的名字和其他的信息

常量池计数器

常量池计数器
记录了常量池有多少项,常量池第一项的索引是从1开始的,索引0空出来是为了满足某些指向常量池的索引值得数据在特定情况下需要表达“不引用任何一个常量池项目”

常量池

常量池

访问标志

标志类或者接口的访问信息。例如这个类是否为public、是否为abstract
访问标志

标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类可以设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这个一个注解
ACC_ENUM0x4000标识这是一个枚举

类索引

类索引(thisClass)用来确定这个类的全限定名
thisClass给的是常量表里的索引值,可以顺藤摸瓜去获取类的信息
thisClass

父类索引

父类索引(superClass)用来确定这个类的父类全限定名,类似累索引

接口计数器&接口表

接口计数器

标识这个类的实现的接口个数
interface-count

接口表

标识这个类实现的具体接口,由于例子没有实现任何接口,因此这里的接口表并不占用空间(不存在)
interfaces

字段计数器与字段表

字段计数器

标识类级变量以及实例级变量的数量
field-count

字段表

描述字段信息
fields
accessFlags 字段访问标志

标志名称标志值含义
ACC_PUBLIC0x0001字段是否public
ACC_PRIVATE0x0002字段是否private
ACC_PROTECTED0x0004字段是否protected
ACC_STATIC0x0008字段是否为static
ACC_FINAL0x0010字段是否为final
ACC_VOLATILE0x0040字段是否为volatile
ACC_TRANSIENT0x0080字段是否为transient
ACC_SYNTHETIC0x1000字段是否由编译器自动产生
ACC_ENUM0x4000字段是否为enum

nameIndex 字段的简单名字

descriptorIndex 字段或方法描述符

下面解析一下“简单名称”、“描述符”以及“全限定名”含义
全限定名是指形如”com/lzw/jvm/TestClass”,仅仅把类全面的”.”替换为”/”并以”;”结束
简单名称是指没有参数类型和参数修饰的方法或者字段名称,例如main()方法的简称是main,name字段的简称是name
字段描述符作用是用来描述字段的数据类型、方法的参数列表和返回值,基本类型都用一个大写字母来表示,而对象类型则用字符L加对象的全限定名来表示

标识字符含义
B基本类型byte
C基本类型char
D基本类型double
F基本类型float
I基本类型int
J基本类型long
S基本类型short
Z基本类型boolean
V特殊类型void
L对象类型,入Ljava/lang/Object

数组使用”[“来描述,例如String数组”java.lang.String[][]”会被表示为”[[Ljava/lang/String;”,”int[]”会被表示为”[I”
用来描述方法是,按章先参数列表,后返回值的顺序描述,例如void inc()会表示为”()V”,方法java.lang.String toString()会表示为”()Ljava/lang/String”,方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[] target, int targetOffset,int targetCount,int fromIndex)表示为”([CII[CIII)I”
字段表集合中不会列出超类或者父接口中集成而来的字段,担忧可能列出原来Java代码中不存在的字段

attributesCount与attributes
如果字段有额外信息,会在attributes里面出现

方法计数器&方法表

方法计数器

标识方法数量

方法表

方法表
上面例子可以看到类里面有是三个方法,<init>mainprintInfo<init>是编译器加上去的方法,每个实例初始化的时候会调用<init>这个方法。
下面看看方法表的结构
accessFlags
这个与字段表的解析差不多,只是去取值上有不同

标志名称标志值含义
ACC_PUBLIC0x0001方法是否public
ACC_PRIVATE0x0002方法是否private
ACC_PROTECTED0x0004方法是否protected
ACC_STATIC0x0008方法是否为static
ACC_FINAL0x0010方法是否为final
ACC_SYNCHRONIZED0x0020方法是否为synchronized
ACC_BRIDGE0x0040方法是否为编译器产生的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100方法是否为native
ACC_ABSTRACT0x0400方法是否为abstract
ACC_STRICTFP0x0800方法是否为strictfp
ACC_SYNTHETIC0x1000方法是否由编译器自动产生

nameIndex
与字段表解析一样

descriptorIndex
与字段表解析一样

attributesCount与attributes
方法的代码就是放在attributes的Code属性里面

属性表

属性表在之前已经出现过几次,在Class文件、字段表、方法表都可以携带自己的属性表集合。
JVM预定义了一部分属性,是必须要实现的,至于其他属性可以按需添加,java虚拟机运行的时候会忽略它
下面摘抄了一部分属性

属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表final关键字定义的常量值
Deprecated类、方法表、字段表被声明为deprecated的方法和字段
Exception方法表方法抛出的异常
EnclosingMethod0x0010方法是否为final
InnerClasses类文件内部类列表
LineNumberTableCode属性Java源码的行数与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
SourceFile类文件记录源文件名称

更多的属性,会在以后遇到的时候学习

总结

这篇大概学习了ClassFile的基本结构,各个项的基本含义,接下来接下来将会通过编写代码来体会一下

参考资料

自己动手写Java虚拟机
深入理解Java虚拟机(第二版)

欢迎关注个人公众号
Ray乐园

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值