码迷,mamicode.com
首页 > 其他好文 > 详细

我的JVM之旅-ClassFile

时间:2016-07-19 10:32:31      阅读:142      评论:0      收藏:0      [点我收藏+]

标签:

欢迎访问我的个人博客 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文件
技术分享
到这里我们对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
技术分享

版本号

副版本号

技术分享

主版本号

技术分享

常量池

常量池记录了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>mainprintInfo<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的基本结构,各个项的基本含义,接下来接下来将会通过编写代码来体会一下

参考资料

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

欢迎关注个人公众号
技术分享

我的JVM之旅-ClassFile

标签:

原文地址:http://blog.csdn.net/lzw_2006/article/details/51942726

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!