一、Class类文件结构

  Class类文件严格按照顺序紧凑的排列,由无符号数和表构成,表是由多个无符号数或其他数据项构成的符合数据结构。

  Class类文件格式按如下顺序排列:

 
类型 名称 数量
u4 magic(魔术) 1
u2 minor_version(次版本号) 1
u2 major_version(主版本号) 1
u2 constant_pool_count(常量个数)
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 }
调用静态常量不会触发其初始化案例

相关文章: