一、类加载机制
类加载器将类的.class文件中的二进制数据读入到内存中,将其放在方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
二、类的生命周期
类的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载7个阶段。其中加载、验证、准备、初始化、卸载5个阶段是按照这种顺序按部就班的开始,而解析阶段则不一定:某些情况下,可以在初始化之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
注意:这里写的是按部就班的开始,而不是按部就班地进行或完成,因为这些阶段通常都是互相交叉混合式进行的,通常会在一个阶段执行过程中调用、激活另外一个阶段。
1、加载
加载阶段会做3件事情:
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
此处第一点并没指明要从哪里获取、怎样获取,因此这里给开发人员预留了创造力空间。许多Java技术就建立在此基础上,例如:
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
2、验证
确保被加载的类的正确性,分为4个验证阶段:
验证阶段非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间
3、准备
4、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
5、初始化
为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。初始化阶段是执行类构造器<clinit>()方法的过程。
public class TestClassLoader {
public static int A = 1;
static {
A = 2;
// System.out.println(A);
}
static class Sub extends TestClassLoader {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
<clinit>()方法和<linit>()方法区别
执行时机不同:init方法是对象构造器方法,在new一个对象并调用该对象的constructor方法时才会执行。clinit方法是类构造器方法,是在JVM加载期间的初始化阶段才会调用。
执行目的不同:init是对非静态变量解析初始化,而clinit是对静态变量,静态代码块进行初始化。
三、类加载器
虚拟机把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自助机决定如何去获取所需要的类。这个模块称为类加载器。
类加载器最初是为了满足Java Applet需求开发出来的,但却在类层次划分、OSGI、热部署、代码加密等领域大放异彩。
类加载器的层次关系图(这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的):
类加载器大致可分为三类:
应用程序是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件。
1、双亲委派模型
工作流程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
意义:系统类防止内存中出现多份同样的字节码。