5.类加载时机和类加载过程

一. 类的加载时机

类从加载到卸载一共经历7个步骤:

加载--------验证---------准备----------解析---------初始化----------使用-------------卸载

其中验证、准备、解析又叫做连接的过程

加载、验证、准备、初始化、卸载这五个步骤顺序是固定的,而解析阶段不一定,解析可以发生在初始化之后,为了支持java语言的运行时绑定。

那么什么时候会触发JVM是开始加载一个类的过程呢?JVM规范中并没有强制约束,但是规范严格规定了有且只有5种情况发生时,必须对类进行初始化,那么在这之之前,加载,验证,准备的过程肯定也要完成

  • 遇到new、getStatic、putStatic或invokestatic这4条指令字节码时,如果类还没有初始化,则要对类进行初始化操作。生成这4条字节码指令操作:new关键字实例化对象的时候,访问或者设置类的静态变量(被final修饰、已在编译期把结果放入到常量池的的静态字段除外),以及调用一个类的静态方法的时候
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,先对类进行初始化
  • 当初始化一个类时,如果他的父类没有被初始化,则首先初始化父类
  • 当虚拟机启动时,先会初始化包含main方法的那个类
  • 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析的结果是REF_getStatic、REF_putStatic、REF_invokestatic的方法的句柄时,先初始化句柄对应的类

这五种行为叫做对类的主动引用,除此之外,所有引用类的方法都不会触发初始化,称为被动引用。

被动引用一

package xidian.lili.classloading;
 
public class Demo01 {
 
	public static class SupClass{
		public  static int a=7;
		static{
			System.out.println("Supclass init");
		}
	}
	public static class SubClass extends SupClass{
		public  static int b=7;
		static{
			System.out.println("Subclass init");
		}
	}
	public static void main(String [] args){
		//System.out.println(SubClass.b);
		System.out.println(SubClass.a);
	}
}

通过子类调用父类的静态变量,只会初始化父类。也就是操作哪个类的静态变量就会初始化那个类

被动引用二

package xidian.lili.classloading;
 
public class Demo2 {
 
	public static class SupClass{
		public  static final int a=7;
		static{
			System.out.println("Supclass init");
		}
	}
	public static class SubClass extends SupClass{
		public  static int b=7;
		static{
			System.out.println("Subclass init");
		}
	}
	public static void main(String [] args){
		//System.out.println(SubClass.b);
		System.out.println(SubClass.a);
	}
}

继续上一个示例,如果变量a是final修饰的静态变量,那么a在编译期间就被加载到方法区的常量池,就相当于跟类是没有关系的,再调用就是类本身对自身常量池的引用,而不需要通过类的class文件中的符号引用来调用,所以不需要初始化类

被动引用三

package xidian.lili.classloading;
 
public class Demo2 {
 
	public static class SupClass{
		public  static final int a=7;
		static{
			System.out.println("Supclass init");
		}
	}
	public static class SubClass extends SupClass{
		public  static int b=7;
		static{
			System.out.println("Subclass init");
		}
	}
	public static void main(String [] args){
		//System.out.println(SubClass.b);
		//System.out.println(SubClass.a);
		SubClass [] subs=new SubClass[10];
	}
}

创建一个类的对象数组,不会引发类的初始化创建动作由newarray触发,它触发的是"[Lorg.xidian.lili.classloading.SubClass"类的初始化,是虚拟机自动生成的,直接继承于java.lang.Object的子类。

对于接口的加载

对于接口的加载和类有一些区别,接口也需要初始化,但是接口中不会存在static{}这样的语句块,但编译器还是为接口生成()类构造器,用于初始化接口中定义的成员变量。

上述类的5中触发类初始化接口与类有区别的在第三条,就是一个接口在初始化的时候并不要求父接口全部初始化,只有在用到父接口的时候才会初始化。

在初始化一个类时,并不会先初始化它所实现的接口。

在初始化一个接口时,并不会先初始化它的父接口

二. 类加载过程

2.1 加载

加载主要是将.class文件(并不一定是.class。可以是ZIP包,网络中获取)中的二进制字节流读入到JVM中。
在加载阶段,JVM需要完成3件事:

1)通过类的全限定名获取该类的二进制字节流;

如果是数组类,前面我们说过数组类,它触发的是"[Lorg.xidian.lili.classloading.SubClass"类的初始化,是虚拟机自动生成的,直接继承于java.lang.Object的子类,所以如果数组类的创建就要遵循以下:

如果数组类的元素是引用类型,那就递归采用正常的加载过程去加载元素,数组类将在元素类型的类加载器的类名称空间上被标识

如果数组的元素是基本数据类型,比如 int[] a=new int [10],JVM会把数组类与引导类加载器关联

2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构;

3)在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

2.2 验证

确保class文件的字节流符合当前虚拟机的要求

1)文件格式验证

验证class文件的魔数,主次版本号,常量池是否有不被支持的常量类型等等。只有经过这步验证,字节流才会进入到方法区存储,后面的验证不在接触二进制流,而是基于它在方法区的存储结构进行的

2)元数据验证

对字节码信息进行语义分析

比如这个类是否有父类,是否继承了不被允许继承的类,final修饰,是否实现了继承的抽象类中的所有方法。保证元数据信息都符合java语言规范

3)字节码验证

元数据验证对数据类型做完校验,这个阶段是对方法体进行校验分析

4)符号引用验证

这个阶段发生在解析阶段,确保解析阶段可以正常执行,如果这个阶段验证失败,抛出

java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchMethodError等异常。可以验证;

是否可以通过类的全限定名找到类

在指定类中是否存在符合方法描述的字段信息以及简单名称描述的方法和字段

符号引用的类,字段,方法的访问权限是否可以被当前类访问

如果运行的代码都已经被反复用过了,就可以在实施阶段使用参数-Xverify:none来关闭参数验证

是为类变量分配内存并设置初始零值的,类变量将在方法区分配,设置初零值根据类型的不同设置不同的零值(boolean类型的默认值是false)。但是如果类变量是被final修饰,那么准备阶段就会直接根据预设的值赋值给变量

2.3 准备阶段

2.4 解析

解析就是把符号引用替换为直接引用的过程。

符号引用:是一组符号来描述所引用的对象,可以是任意形式的自面量,与虚拟机实现的内存布局无关

直接引用:可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用与虚拟机的内内存布局相关,有了符号引用那么目标必定已经在内存中存在了

前面说过解析不一定会发生在什么时候,虚拟机根据需要来判断到底是在类被加载的时候就对常量池中的符号进行解析还是等到一个符号引用要被使用前去解析它。

2.5 初始化

初始化阶段是类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值。前面讲到了类的主动引用 和 被动引用就是在初始化的时候发生

完成初始化在JVM中就完成了类加载的过程

3. 基于无符号数和表结构的类文件结构

Java的跨平台性

就是因为java编译器(javac)生成了.class文件,可以在任何只要安装了虚拟机就可以执行的字节码文件,java虚拟机没有和任何编程语言绑定,包括java语言,他只和.class二进制文件有关

字节码文件组成(.class)

是一种基本数据类型,u1,u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节长度的无符号数

是有多个无符号数和其他表组合构成的复合型数据结构,所有表都已_info结尾。所以说整个class文件就是一张表

当需要描述同一类型但数量不定的多个数据时,会使用一个前置的容量计数器加若干个连续的数据项形式来表示,称为某一类型的集合,如后面会提到的字段表,方法表等都是这样的格式。

3.1 字节码文件具体内容

1. 魔数

个字节码文件都是以魔数开头的,魔数是很多文件的存储标准用来区别文件类型的,比如class文件的前四个字节用来存储魔数,魔数是0xCAFEBABY,不因后缀名或者叫拓展名来区别文件类型,是因为拓展名都是可以更改的。

2. 版本号

接着魔数的就是class文件的版本号,5,6两个字节是次版本号,7,8两个字节是主版本号

3. 常量池(constant_pool_count)

常量表紧挨着版本号,可以说是class文件的资源仓库,后面的方法表,字段表都要有指向常量表中常量的引用。在常量池里会把代码中所有的常量(一般包括字面量,和符号引用,不是平时说的被final修饰的“常量”)都存储起来,并设置索引。常量池是class文件中第一个表结构。

  • 字面量:比如字符串常量"hello word",以及final修饰的,比如final int MAX=123;123就是字面量
  • 符号引用:类和接口的全限定名;字段的名称和描述符,比如public int a=0;这里a是字段名称,int是描述符;还有方法的名称以及描述符,比如public int add(int a,int b){},那么add是名称,int是描述符。前面我们说过在符号引用阶段,不涉及jvm的内存布局信息,只有在解析阶段以后,符号引用转化为直接引用,才会把引用翻译到具体的内存地址上。

常量表的大小是不固定的,取决于你程序中有多少个常量,所以在常量表的开始,放置一个u2类型的数据,就是前面说的容器计数器,来表示常量表中有多少个常量,比如一个有20个常量,那么他们的索引取值范围是1-20,后面用到某一个常量时,就可以用"#索引值"来访问常量。至于索引取值为0,表示数据没有引用任何常量(特定情况下使用)。

在常量池中存储的每一个常量都是以表结构存储在常量池中

有14种表结构,每一个常量对应这14种表结构中的一种

image

这里主要说明一下CONSTANT_Utf8_info,以及CONSTANT_Class_info这两种类型的表结构,我们知道常量池中第一个常量肯定是类或接口的符号引用,然后就会是他们的全限定名常量,比如我的public class Test,全限定名是xidian/lili/mytest/Test.

那么在常量池中第一个常量类型就是CONSTANT_Class_info,它的表结构有两个参数,第一个是u1类型的tag,表示当前常量的类型,那么我们这里第一个常量的tag=0x07(图中圈出来);第二个参数是u2类型的name_index,指向一个类型为CONSTANT_Utf8_info的常量。我们知道类或接口的符号引用一定会有描述信息(字段和方法也一样),这里类的描述信息就是字符串"xidian/lili/mytest/Test",这个字符串是常量池的第二个常量,类型是CONSTANT_Utf8_info。所以我们第一个常量的name_index=#2(他的描述在第二常量)。

那么如上所说第二个常量是CONSTANT_Utf8_info,在class文件中,类,接口,字段,方法的描述信息都是CONSTANT_Utf8_info类型的常量来描述的。它有三个参数,第一个是u1类型的tag,表示当前常量的类型,那么这里tag=0x01;第二个参数是u2类型的length,表示描述信息的长度,比如length=0x001D,说明描述信息长度是29,第三个参数是u1类型的byte,它的数量等于length,那么我们就往后数29个字节,都表示描述信息,每一个字节是一个byte,可以根据ASCII换算回我们的字符串。

4. 访问标志

常量池之后就是访问标志(access_flags)信息了。是一个u2类型的数据,用来描述类的访问权限,比如0x0001(ACC_PUBLIC)代表public等等,一共有16种标志

5. 类索引、父类索引、接口索引

这三个按照顺序排列在访问标志之后,前面我们说了常量池,那么这些索引(包括这三个索引以及后面要说的字段表和方法表中的索引)就是执行常量池,然后在运行期间,根据这些索引找到常量池存的内容进行操作。

那么这三个索引就确定了类的继承关系。

类索引和父类索引都是u2类型的数据,而接口索引是u2类型数据的结合(一个类可以实现多个接口)。所以接口索引集合入口第一项是一个u2类型的接口计数器。

对于前面的Test例子,我们没有继承的类和实现的接口,但是父类有java.lang.Object.所以这三个u2数据类型会依次是:

0x0001、0x0003、0x0000

0001表示我们的类索引是常量池中的第一个变量,如上面所说

0003表示我们类的父类Object是常量池中的第三个索引,如上面所说第二个常量是类的描述信息,所以父类会是第三个常量

0000表示这个类没有实现接口,如果这个数字不为0,那么后面继续加接口对应的常量池的索引,这个数字是多少,就会加多少个接口索引。

6. 字段表集合

这里说的字段包括java程序中的类属性和实例属性,但是不包括方法中的局部变量

一般一个类中是有多个属性的,所以字段表是一个多个字段信息的集合,而每一个字段由是一个表结构,同理,字段表集合入口处会有字段计数器。对于一个字段来说,包含的信息有很多,作用域,是否是静态变量,可变性(final),并发可见性(volatile),字段数据类型,字段名称,可否被序列化等都需要借助常量池中的索引来描述。

对于一个字段表结构,它有5个参数。

  1. u2类型的access_flags:与类的access_flags相似,比如access_flags=0x0002,表示这个字段是private修饰的

  2. u2类型的name_index:对常量池中字段的名称的索引

  3. u2类型的descriptor_index:对常量池中字段描述符常量的索引,对于字段和方法的描述符合与上面说的类的描述信息不一样(类的全限定名)

比如java程序中有:private int m;

那么access_flags=0x0002,name_index=0x0005,descriptor_index=0x0006

0005表示这个字段的名称是常量池中的第五个常量,类型肯定是CONSTANT_Utf8_info,内容(byte)是m

0006表示这个字段的描述符是常量池中的第六个常量,类型肯定是CONSTANT_Utf8_info,内容(byte)是I,字段描述符用一个大写字母来表示,I代表int,V代表void,D代表double,L代表对象类型等等,这些字段描述符也是方法的描述符,用描述符描述方法时,按照先参数后返回值来描述。参数放在()类,比如void com(){}方法,描述符就是"()V",无参数无返回值。

  1. u2类型的attributes_counts:属性表计数器

  2. attribute_info类型的attributes:属性表,这两参数用来存放字段的额外信息,那比如private int m,属性表计数器就是0,没有属性表这个参数,比如final static int m=20;就会有一个ConstantValue属性。具体的后面谈到属性表再讲,因为方法中也有属性表。

字段表中不会表示从父类继承来的属性,在内部类中会自动添加指向外部类实例的字段,在字节码文件中,两个字段的描述符不同就不是同一个字段,但是在java中,修饰符,数据类型不管是不是相同,都必须使用不同的属性名称

7. 方法表集合

了解了属性表结合,方法表集合类似。结合入口是方法表计数器。每一个方法是一个表结构,类似字段表结构。

特别的方法表的访问标志没有acc_volatile,acc_transient,但是增加了acc_synchroniaed,acc_abstract,acc_native,acc_strictfp等只能修饰方法的修饰符。还有就是在java代码中,实现方法体的代码,都放在方法表的code属性中,是属性表的一种类型,下面会介绍属性表。

我们知道java方法重载中,除了方法名必须一样,还必须有一个与原方法不同的特征签名,特征签名就是方法中各个参数在常量池的字段符号引用的集合,也就是因为java的特征签名不包括返回值,所以无法仅仅依靠返回值的不同来进行重载。

(java代码层面的特征签名包括方法名称,参数顺序,参数类型。而字节码的特征签名还包括返回值以及受查异常表)


以上就是一个字节码文件包括的几部分内容,下面将介绍在在字段表和方法表中都出现的属性表,当然在class文件中也可以有自己的属性表


3.2 属性表集合

预定义的属性已经有21种,对于每个属性,他有三个参数。

u2类型的attribute_name_index,是从常量池中引用一个CONSTANT_Utf8_info类型的常亮来表示

u4类型的attribute_length u1类型的info

code属性

就是java程序代码经过javac编译之后,变成字节码指令存储在code属性集合中,code属性出现在方法表后面的属性集合中。

max_stack代表了操作数栈深度的最大值,方法运行期间操作数栈都不会超过这个深度,虚拟机就是根据这个值来分配方法区栈帧的操作栈深度。

max_locals代表了存储了局部变量表需要的内存,max_locals的单位是变量槽slot,slot是虚拟机为局部变量分配内存使用的最小单位。对于不足32位的数据类型,占用一个slot。64位的long和double需要占用两个slot。不是每个变量所占的slot之和就是max_locals的和,因为局部变量表中的slot可以重用。当一个变量过了自己的作用域,虚拟机会把它的slot分配给其他变量使用

code_length和code用来存储代码翻译过来的字节码指令,code中存储的就是字节码指令,根据一个字节一个字节对照字节码指令表,翻译出字节码指令。

可以在code属性表的args_size看出任何方法都有一个this对象的属性,比如构造方法和无参数的方法,args_size=1.可见这个实现是编译器把this对象当做参数传入,局部变量表也会把第一个slot空出来存放这个实例对象的引用。

在字节码指令之后,存放的是这个方法的显示异常处理表,这个表不是必须的

异常表有四个参数

u2类型的start_pc(from),end_pc(to),handler_pc(target),catch_type(type).代表了在运行start_pc到end_pc之间的代码,出现了类型为catch_type或者其子类的异常,则转到handler_pc行继续执行

try-catch-finaly语句执行语句

(1)try语句中出现属于catch的异常或者其子类,转到catch块中处理

(2)try语句中出现出现不属于catch捕获的异常,转到finally语句块处理

(3)catch语句块捕获到异常,则转到finally语句执行

出现异常或者正常执行都会执行finally语句块

code属性是class文件最重要的一个属性,java执行字节码是基于栈的体系结构

Exception属性

区别与上面的异常表

Exception属性的作用是列举出方法中可能抛出的受查异常,也就是方法描述时throws关键字后面列举的异常

LineNumberTable属性

描述Java程序代码行号与字节码指令行号之间的对应关系,默认会出现在class文件,可以用-g:none或者-g:var来选择打开或者关闭

ConstantValue属性

作用是通知虚拟机自动为静态变量赋值。只有被static修饰的变量(或者类变量)才能拥有这个属性

int a=10;普通变量没有constantvalue属性,对于这种变量的赋值是在实例构造器方法中进行的。对于类变量

static int a=10;可以有两种方式进行赋值

可以在方法中或者使用constantvalue属性赋值。习惯上在sun javac的编译器,如果变量是static final修饰的基本数据类型常量或者字符串常量(常说的常量)就用字段表后面的constantvalue属性赋值。如果这个变量没有被final修饰或者不是基本数据类型或者string类型就用来赋值

InnerClasses属性

记录内部类和宿主类之间的关系。如果是匿名内部类,参数inner_name_index是0

Signature属性

专门用来记录泛型签名信息。专门设置这样一个属性,是因为java中采用的泛型擦除,在字节码中,泛型信息(类型变量,参数化类型)都别擦除。所以反射调用时无法获得泛型信息。signature属性就是为了弥补这个缺陷,现在的java的反射API获取的泛型类型数据都是来源与这个属性

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值