类加载机制
Java程序运行,首先会将编写的Java代码转换成字节码,然后加载到内存中,最后被JVM解释执行。本文讲介绍类的加载过程
虚拟机加载对应的class文件,会经过5个流程
一、加载
就是将class文件加载到虚拟机的内存中,在这个阶段,需要完成三件事情:
1.通过类的全限定名来获取这个类的二进制字节流——class文件
2.将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
3.在内存中生成这个类对应的class对象,作为各种数据的访问入口
有8种情况会要求立即进行对应类的初始化,而加载在初始化之前,
其实也可以理解会加载类的时机。
1.使用关键字new对象的时候
2.使用或者设置类的静态变量的时候,需要注意的是,使用被final修饰的静态变量,不会导致类加载,因为其已经在编译期写入常量池中。
3.使用类的静态方法的时候
4.初始化一个类的时候,发现其父类还没有加载,则需要先加载父类
5.虚拟机启动的时候,首先会对主方法所在类进行加载
6.对类进行反射调用的时候
7.使用JDK动态代理的时候
8.被default关键字修饰的接口方法,若这个接口的实现类加载,那么这个接口需要提前加载。
以上的情况都是基于对应类没有加载时,会被要求立即进行加载,同时以上的情况称为对类的主动引用,其他情况称为被动引用,被动引用不会触发对应类的加载,下面举几种被动引用的例子
1.通过子类引用父类的静态变量,不会导致父类初始化
public class Super {
static {
System.out.println("Super init");
}
public static String name = "Super";
}
public class Sub extends Super{
static {
System.out.println("Sub init");
}
}
public class test {
public static void main(String[] args) {
System.out.println(Sub.name);
}
}
********执行结果******
Super init
Super
只有定义静态变量的类去使用和设置这个静态变量,才会导致对应类的初始化
2.类当作数组的类型时,不会导致对应类的初始化
public class Super {
static {
System.out.println("Super init");
}
public static String name = "Super";
}
public class test {
public static void main(String[] args) {
Super[] array = new Super[5];
}
}
*******
无结果输出
3.使用类的常量,不会导致常量所有类加载
public class Super {
static {
System.out.println("Super init");
}
public static final String NAME = "Super";
}
public class test {
public static void main(String[] args) {
System.out.println(Super.NAME);
}
}
*****执行结果****
Super
二、验证
其实也就是各种各样的检验,产生class文件的并不只有Java源代码的编译,所以需要对文件格式、元数据、字节码、符号引用四个方面进行验证,以确保程序产生的行为不会对虚拟机产生危害
1.文件格式验证
检验class文件是否符合规范并且能被当前的虚拟机处理,也就是当前运行环境不低于class文件中的版本号,保证输入的字节流能正确
2.元数据验证——对字节码描述的信息进行语义分析,一般是:这个类是否有父类,这个类的父类是否继承了不允许被继承的类等
3.字节码验证——通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。主要是对类的方法进行检验分析,例如:保证任何的跳转指令不会跳转到方法体之外,保证方法题中的类型转换时有效的。在这里需要注意的是,一个方法体的字节码检验没有通过,那么它是有问题的,如果一个方法题通过了字节码检验,并不能说明它是没问题的。
4.符号引用验证——这个检验发生在虚拟机将符号引用转换成直接引用的时候,对类自身以外的信息进行校验,例如引用类的访问权限等,这个在解析阶段会展开讲述
三、准备
正式为类中定义的静态变量在方法区分配内存并设置初始值,这里的初始值是虚拟机默认的初始值,例如:int类型的为0,boolean为false等,并不是给变量定义的值,真正的赋值需要等到初始阶段。但是,被final修饰的静态变量,在这个阶段就会被设置成指定的值。
四、解析
将符号引用转换为直接引用的过程。
符号引用:通过使用一组符号来描述所引用的目标,可以是任意形式的字面量,只要无歧义即可
直接引用:可以直接指向目标的指针
主要是对类或接口、字段、类方法、接口方法四种符号引用进行转换
1.类或接口
在类A中,要对一个没有解析过的符号引用N解析或一个类或者接口B的直接引用,需要完成3个步骤
1)如果B不是数组类型,B的全限定名会由虚拟机传递给A的类加载器去加载类B。这个过程还会触发其他类的加载,有任何异常,解析则失败
2)如果B是数组类型,并且B中的元素为对象,会按照1的规则加载数组元素类型
3)以上没有异常,则B就已经被加载到虚拟机中了,或者可以说是有效的类、接口了。还需要进行符号引用权限的验证,就是说,确认类A是否有对B的访问权,如果不具备权限,则会抛出异常
2.字段
对于一个没有被解析过的字段符号引用,首先会解析这个字段所属的类或接口的符号引用,这一步的任何异常都会导致字段解析失败。假设字段所属的类或接口为A,已解析成功,那下面对字段进行解析
1)如果A中就有简单名称和字段描述符都与目标字段匹配的,则返回这个字段的直接引用,结束
2)根据在A中的继承关系,从下往上,按照1的情况进行搜索
若成功返回直接引用,则对这个字段进行权限验证
3.类方法
对于一个没有被解析过的方法符号引用,首先会解析这个方法所属的类或接口的符号引用,这一步的任何异常都会导致方法解析失败。假设方法所属的类或接口为A,已解析成功,那下面对方法进行解析
1)对于类的方法,如果发现A是接口,则抛出异常
2)在类A中找是否有简单名称和描述符都和目标方法匹配的,有则直接返回,结束
3)根据类A的继承关系,从下往上,按照2的规则进行查找
4)找不到,抛出异常;找到,验证方法权限
4.接口方法——和类方法大致相同,只是步骤1相反,会判断A是否为类,是的话则抛出异常结束
五、初始化
虚拟机真正开始执行类中编写的Java代码
在准备阶段的时候,变量已经被赋值过一次虚拟机默认的值,实际赋值将在这个阶段进行。在初始化阶段,会执行类构造器<clinit> ()方法,这个不是编写的构造器,而是Javac编译器自动生成的。这个方法由编译器自动收集类中的所有变量的赋值动作和静态语句中的语句合并产生的。我的理解就是这个方法会自动给那些变量按照编写代码的主观意愿进行赋值。当然<clinit> 方法并不是必须的,对于一个没有静态代码块和对变量赋值的类,编译器可以不为这个类生成方法
|