标签:
欢迎访问我的个人博客 http://rayleung.xyz/
搞IT有几年了,也学了不少框架中间件之类的东西。东西越学越多,越感觉迷茫,后来突然觉醒,发现弄懂技术的基础原理和算法这些基本的东西,才是自己继续往下走的根本。
偶然发现有一本书教人写JVM,叫自己动手写Java虚拟机,于是萌生把学习Jvm的过程记录下来,目标是能自己写一个最简单的JVM,能运行Java程序。
要说JVM,首先先认识一下Class文件这东西。
什么是Class文件这个问题听上去很SB,搞Java的人有谁不知道Class文件
不过对于初学者来说,搞清楚这个问题却很有意义
我们写的程序会通过Java编译器编译成字节码(ByteCode),因为字节码使得应用与平台无关,能一次编译到处运行。
这些字节码一般保存在Class文件里面(Java虚拟机规范并没有强制字节码一定要存放在文件里面),因此虚拟机是与Class文件打交道的,与你用什么编程语言来编写程序没什么关系。一般来说我们默认是用Java语言来写Java程序,不过其实只要特定语言的编译器能够生成符合JVM规范的字节码,都是能在JVM上运行的,例如Scala。
就像我还没了解Class文件结构之前一样,看到Class文件会有一股陌生感。
于是乎如果有一个直观的工具能观察到Class文件内部结构,那是有多好。
嗯,的确有这样的工具 —– classpy(点击传送到GitHub)
用法很简单,双击jar文件,然后选择编译好的class文件打开,就能看到效果,这里就不演示怎样打开了。
另外我们也可以用javap这个工具来查看输出的常量表。
为了研究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文件
到这里我们对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文件本质上就是一张表。
用来确定这个文件是否虚拟机能够接受的Class文件,固定值为0xCAFEBABE
常量池记录了Class文件使用到的资源,例如引用到的字符串,类和接口的名字和其他的信息
记录了常量池有多少项,常量池第一项的索引是从1开始的,索引0空出来是为了满足某些指向常量池的索引值得数据在特定情况下需要表达“不引用任何一个常量池项目”
标志类或者接口的访问信息。例如这个类是否为public、是否为abstract
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这个一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
类索引(thisClass)用来确定这个类的全限定名
thisClass给的是常量表里的索引值,可以顺藤摸瓜去获取类的信息
父类索引(superClass)用来确定这个类的父类全限定名,类似累索引
标识这个类的实现的接口个数
标识这个类实现的具体接口,由于例子没有实现任何接口,因此这里的接口表并不占用空间(不存在)
标识类级变量以及实例级变量的数量
描述字段信息
accessFlags 字段访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否public |
ACC_PRIVATE | 0x0002 | 字段是否private |
ACC_PROTECTED | 0x0004 | 字段是否protected |
ACC_STATIC | 0x0008 | 字段是否为static |
ACC_FINAL | 0x0010 | 字段是否为final |
ACC_VOLATILE | 0x0040 | 字段是否为volatile |
ACC_TRANSIENT | 0x0080 | 字段是否为transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否为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>
、main
和printInfo
。<init>
是编译器加上去的方法,每个实例初始化的时候会调用<init>
这个方法。
下面看看方法表的结构
accessFlags
这个与字段表的解析差不多,只是去取值上有不同
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否public |
ACC_PRIVATE | 0x0002 | 方法是否private |
ACC_PROTECTED | 0x0004 | 方法是否protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_FINAL | 0x0010 | 方法是否为final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否为synchronized |
ACC_BRIDGE | 0x0040 | 方法是否为编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICTFP | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生 |
nameIndex
与字段表解析一样
descriptorIndex
与字段表解析一样
attributesCount与attributes
方法的代码就是放在attributes的Code属性里面
属性表在之前已经出现过几次,在Class文件、字段表、方法表都可以携带自己的属性表集合。
JVM预定义了一部分属性,是必须要实现的,至于其他属性可以按需添加,java虚拟机运行的时候会忽略它
下面摘抄了一部分属性
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exception | 方法表 | 方法抛出的异常 |
EnclosingMethod | 0x0010 | 方法是否为final |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行数与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 记录源文件名称 |
更多的属性,会在以后遇到的时候学习
这篇大概学习了ClassFile的基本结构,各个项的基本含义,接下来接下来将会通过编写代码来体会一下
欢迎关注个人公众号
标签:
原文地址:http://blog.csdn.net/lzw_2006/article/details/51942726