JVM的类加载分为三个部分:加载、连接(验证、准备、解析)、初始化。类的加载是线程安全的!
加载:
加载分为三个步骤:
1、根据类的全限定类名将该类以二进制流的方式读入。可以重写ClassLoader的loadClass()方法来改写获取二进制流的方式。
2、在方法区中生成该类的结构。
3、生成Class对象作为访问方法区中类结构的入口。注意在hotspot中Class对象存放在方法区中。
连接:
1)验证
(1)文件格式验证
(2)类元数据验证:
1、方法有无父类。
2、方法是否继承了不该被继承的类(final类)。
3、方法若不是抽象类,继承的接口/抽象类的抽象方法是否全部实现。
(3)字节码验证:
1、保证字节跳转不会跳转到方法体以外的方法上。
2、确保类型转换可以正常进行。
3、保证操作数栈的数据类型与指令序列可以配合工作。
(4)符号引用验证:
通过字符串描述的全限定类名是够能找到响应的类。
在指定类中是否存在符合方法的字段描述的方法以及简单名称所描述的字段。
符号引用中的类、字段、方法的修饰符(private、public、protected、default)是否可被当前类访问。
2)准备
准备的目的是将类变量(static修饰的变量)进行初始化为0。注意不是真正的赋值。真正的赋值需要等到初始化时进行。但是如果是常量(public static final)的话会直接赋予程序员定义的值,不会先进行初始化。
3)解析
解析的目的是将符号引用转化为直接引用,即将名字字符串转化为运行时的地址。
(1)类/接口解析
1、设当前类为A,若想在A中加载类/接口B,则JVM会将B的全限定类名传入A的类加载其中进行加载。
如果B 触发了其他的类加载动作但这个过程中出现了失败,则解析结束。
2、若B是一个对象数组类型,则按照一对元素类型进行加载,再接着由JVM生成数组对象。
3、若以上的步骤没有错误,则虚拟机中已经有了一个有效的B类/接口。最后进行符号引用验证,确保A可
以访问B,否则抛出IllegalAccessError异常。
(2)字段解析
1、JVM首先会解析该字段所属的类/接口(通过常量池),若类/接口解析失败则此次字段失败。
若成功,设声明该字段的类为A。
2、如果A中有与该字段的简单名称和字段描述符匹配的字段,则返回这个字段的直接引用。
3、否则去A的父接口递归的逐层寻找与该字段的简单名称和字段描述符匹配的字段,若有则
返回这个字段的直接引用。
4、否则去A的父类递归的逐层寻找与该字段的简单名称和字段描述符匹配的字段,若有则
返回这个字段的直接引用。
5、若没有该字段抛出NoSuchFieldError异常。
6、对成功返回的引用进行权限验证,如果不能对该字段进行访问抛出IllegalAccessError异常。
(3)类方法解析
1、如果在类方法表中发现class_index索引的类A是个接口,直接抛出java.lang.IncompatibleClassChangeError异常。
2、若步骤1成功,在类A中查找是否有简单名称和描述符都与目标匹配的方法,有则返回该方法的直接引用。
3、否则按步骤2的方法递归的查找父类。
4、否则按步骤2的方法递归的查找C实现的接口列表及其父接口。若找到说明C为抽象类,抛出AbstractMethod异常。
5、查找成功之后会进行权限验证,如果不具备对此方法的访问权限,抛出IllegalAccessError异常。
(4)接口方法解析
1、如果在接口方法表中发现class_index索引的接口A是个类,直接抛出IncompatibleClassChangeError异常。
2、若步骤1成功,在接口A中查找是否有简单名称和描述符都与目标匹配的方法,有则返回该方法的直接引用。
3、否则按步骤2的方法递归的查找父接口,直到Object(包括Object)。
4、否则宣告查找失败,抛出NoSuchMethodError异常。
初始化:
初始化阶段是类加载过程的最后一步,主要是<clinit>()方法执行。<clinit>()方法负责把类变量的赋值动作
和静态代码块中的语句收集在一起(由编译器自动收集),收集顺序按照源文件中的出现顺序所决定。所以静态代码块只可以访问
在其之前的变量,可以赋值但是不能访问在其之后的变量。
public class Test {
static {
i = 2; //成功
System.out.println(i); //失败
}
static int i = 1;
}
<clinit>()方法有以下特点:
1、<clinit>()方法保证在其执行之前该类父类中的<clinit>()方法已经执行完毕
2、由于父类<clinit>()方法先执行,所以父类静态代码块比子类先执行
3、对于接口来说不需要先执行父接口的<clinit>()方法,接口的实现类也不必先执行父接口的<clinit>()方法。
只有当父接口定义的类变量使用时才会执行<clinit>()方法。
4、<clinit>()方法不是必须的,若没有类变量或者静态代码块的话可以不生成。
5、若有多个线程初始化一个类,JVM保证只用其中一个线程去执行<clinit>()方法,其他线程阻塞。所以 <clinit>()方法是线程安全的。但是如果 <clinit>()方法中有很耗时的做法的话会造成线程等待:
public class Test {
static {
while(true) {}
}
}
类加载器:
三种加载器:
JVM中只存在两种不同 的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++实现 ,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java实现,独立于虚拟机外部且全都继承自抽象类 java.lang.ClassLoader。
启动类加载器(Bootstrap ClassLoader):将存放在<JAVA_HOME>\lib目录中的,或者被Xbootclasspath参数所指定的路径中的,并且是虚 拟机识别的(仅按照文件名识别,如rt.jar,名字 不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。该加载器无法被 Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器, 那直接使用null代替即可。
扩展类加载器(Extension ClassLoader):它负责加载<JAVA_HOME>\lib\ext目录中 的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,可以直接使用或者扩展该加载器。
应用程序类加载器(Application ClassLoader):一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
唯一性:
在JVM中,一个类和他所在的加载器在JVM中确定唯一性。也就是说,要想比较两个类是否相等,这两个类必须处于同一类加载器命名空间中:
类加载器A 类加载器B
———————— ————————
| 类名称空间1 | | 类名称空间1 |
| | | |
| Class C | | Class C |
| | | |
———————— ————————
比如这两个Class都叫做C,但是由于分别处在不同的类加载器命名空间中,所以这两个C不相等。
双亲委派机制:
由于Java中可以实现自己的类加载器,那么根据唯一性原理的话,用多个类加载器去加载类C那么会导致JVM中有多个不通的
类C。那么如果要创建类C的实例,那么使用哪一个类C呢?这就会导致类加载的不确定性。为了解决这个问题,JVM中引入了
双亲委派机制:如果要加载一个类,那么从子类开始不断地递归的往父类加载器传递让父类完成加载。因此所有的类加载请求
最终都应该传到顶层的bootstrap classloader中进行加载。注意类加载器之间的关系不是使用继承来实现,而是使用组合来实现。
双亲委派机制的优点:
1、沙箱安全机制:自己写的String类并不会加载,bootstrap loader只会加载lib目录下JRE中的String类。
2、避免类的重复加载。
全盘委托机制:
当一个ClassLoader加载一个类时,除非指定使用另一个ClassLoader,否则该类使用和依赖的类也由这个ClassLoader加载。