一、Class类文件结构
Class类文件严格按照顺序紧凑的排列,由无符号数和表构成,表是由多个无符号数或其他数据项构成的符合数据结构。
Class类文件格式按如下顺序排列:
| 类型 | 名称 | 数量 |
| u4 | magic(魔术) | 1 |
| u2 | minor_version(次版本号) | 1 |
| u2 | major_version(主版本号) | 1 |
| u2 | constant_pool_count(常量个数) | 1 |
| cp_info | constant_pool(常量池表) | constant_pool_count-1 |
| u2 | access_flags(类的访问控制权限) | 1 |
| u2 | this_class(类名) | 1 |
| u2 | super_class(父类名) | 1 |
| u2 | interfaces_count(接口个数) | 1 |
| u2 | interfaces(接口名) | interfaces_count |
| u2 | fields_count(域个数) | 1 |
| field_info | fields(域的表) | fields_count |
| u2 | methods_count(方法的个数) | 1 |
| method_info | methods(方法表) | methods_count |
| u2 | attributes_count(附加属性的个数) | 1 |
| attribute_info | attributes(附加属性的表) | attributes_count |
魔术用来判断该文件是否是Class类文件。
常量池的个数从1开始计数,所以常量池的个数为nstant_pool_count-1。常量池主要存放两大类常量,字面量以及符号引用。符号引用包括:类和接口的权限定名,字段名称和描述符,方法的名称和描述符。常量池的每一项表都是一个表,中共有11中表,具体可以看《深入理解java虚拟机》Page146,上面很详细的介绍而这11中常量,字面量的结构都是一个u1长度的tag,表示这个常量的类型,一个u2长度的length,表示这个常量的长度,以及length个u1长度的bytes(u1,u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节)。
常量池后面紧接着是类的访问权限控制符,类以及父类的全限定名,以及接口的个数,之后是接口的全限定名,全限定名都是指向常量池的符号引用。
再下面就是字段的个数,以及相应个数的表示字段的表,字段表的结构为:
| 类型 | 名称 | 数量 |
| u2 | access_flag(字段修饰符) | 1 |
| u2 | name_index(字段的简单名称) | 1 |
| u2 | descriptor_index(字段的描述符) | 1 |
| u2 | attributes_count (字段的额外属性的个数) | 1 |
| attribute_info | attributes(字段的额外属性) | attributes_count |
全限定名:com/froest/TestClass;把comm.froest.TestClass中的"."换成"/",并且在最后加上";"就成为了全限定名,简单名称就是域的名称或者方法的名称;比如有方法 int getList(int a,char b,long c),那么该方法的描述符为:(ICJ)I;I为int类型的描述符,C为char类型的描述符,J为long类型的描述符,参数列表用"()",最后加上返回值的,描述符。
方法表的结构和字段表一样
在Class文件、字段、方法表中都可以携带自己的属性表结合,用于描述某些场景专有的信息。虚拟机预定义的属性如下表所示:
| 属性名称 | 使用位置 | 含义 |
| Code | 方法表 | java代码编译成的字节码指令 |
| ConstantValue | 字段表 | final关键字定义的常量值 |
| Deprecated | 类、方法表、字段表 | 被声明为Deprecated的方法和字段 |
| Exceptions | 方法表 | 方法抛出的异常 |
| InnerClasses | 类文件 | 内部类列表 |
| LineNumberTable | Code属性 | java源码的行号和字节码指令的对应关系 |
| LocalVariableTable | Code属性 | 方法的局部变量描述 |
| SourceFile | 类文件 | 源文件名称 |
| Synthetic | 类、方法表、字段表 | 表示方法或字段为编译器自动生成 |
下面具体讲下Code属性,其他属性可以在《深入理解java虚拟机》中找到。Code属性的表结构如下:
| 类型 | 名称 | 数量 |
| u2 | attribute_name_index(指向常量池中的”Code“常量,表示这个是"Code"属性) | 1 |
| u4 | attribute_length("Code"属性的长度) | 1 |
| u2 | max_stack(操作数栈的最大深度) | 1 |
| u2 | max_locals(局部变量表的最大空间,以slot为一个基本单位) | 1 |
| u4 | code_length(方法的字节码指令的长度) | 1 |
| u1 | code(方法的字节码指令) | code_length |
| u2 | excepion_table_length(方法体重用try-catch捕获的异常类型的个数) | 1 |
| exception_info | exception_table(方法体重用try-catch捕获的异常类型) | excepion_table_length |
| u2 | attributes_count(方法表的属性的个数) | 1 |
| attribute_info | attributes(方法表的属性) | attributes_count |
其中max_locals不一定是所有的局部变量的总和,因为有些局部变量是有作用域的,离开了作用域,这个局部变量就失去了作用,他所占用的slot也就可以被重用,所以max_locals可以小于等于方法中的所有的局部变量的总和。字节码指令只占用一个字节,用u1表示。局部变量的顺序,按照this,参数,局部变量。也就是第一个slot用来存放this(指向常量池中该类的符号引用,是一个地址),参数在局部变量中从第2个slot开始存放。
二、类加载机制
类加载按加载,连接,初始化这个顺序进行的,其中连接又可以细分为验证,准备,解析三个阶段,部分解析可以在初始化开始之后再开始,这样可以支持java的运行时绑定。虽然部分解析可以在初始化阶段开始以后再开始,但是这部分的初始化还是需要当前的部分解析以后才可以初始化。java虚拟机规范中严格规定了有且之友中情况必须立即对类进行初始化:
1)遇到new创建实例,getstatic获取类的静态字段,putstatic设置静态字段,invokestatic调用类的静态方法
2)用java.lang.reflect包方法对类进行反射调用的时候,如果这个类没有初始化过,那么先触发其初始化
3)初始化一个类的时候,如果父类没有进行初始化,那么必须先触发其父类的初始化
4)当虚拟机启动的时候,需要指定一个执行的主类,虚拟机会先初始化这个主类
用new关键字创建数组不会触发相应的类初始化。调用一个类的静态常量也不会触发该类的初始化,因为调用类在编译阶段就已经把常量转化为对自己的常量池的引用,例:
1 class ConstClass { 2 static { 3 System.out.println("ConstClass init"); 4 } 5 public final static String HELLODWORLD = "hello world"; 6 } 7 8 public class NotInitialization { 9 public static void main(String[] args) { 10 System.out.println(ConstClass.HELLODWORLD); 11 } 12 }