类加载机制
2019-10-26 本文已影响0人
后来丶_a24d
目录
- 概念
- 加载过程
- 初始化时机
- 类初始化顺序注意点
- 双亲委派模型
- 自定义类加载器
类加载
概念
- Java虚拟机把描述类的数据从Class(经过编译器将.java文件生成的)文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
加载过程
- 加载、验证、准备、解析、初始化
- 加载: 类的装载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。该阶段既可以使用系统提供的类加载器完成,也可以由用户自定义的类加载器来完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。
- 验证的目的是为了确保 Class 文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
- 准备 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。只是默认初始化, int a=3 在准备阶段是 a=0;
- 初始化 真正赋值,调用构造函数。虚拟机会保证一个类的类构造器方法在多线程环境中被正确地加锁和同步。如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的类构造器方法,其它线程都需要阻塞等待,直到活动线程执行类构造器方法完毕。如果在一个类的类构造器方法中有耗时很长的操作,那么就可能造成多个进程阻塞。
- 解析动作主要针对类或接口、字段、类方法、接口方法,解析时机 在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),一个符号引用将要被使用前才去解析它(初始化之后)。对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。
- 如果编写一个使用接口的应用程序,可以等到运行时再指定其实际的实现(多态),解析过程有时候还可以在初始化之后执行。
类初始化时机
- 主动引用
- 创建类的实例
- 访问类的静态变量(除常量被final修辞的静态变量外,这种是直接替换)
- 访问类的静态方法
- 反射如(Class.forName("xxx"))
- 当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
- 虚拟机启动时,定义了main()方法的那个类先初始化
- 被动引用, 代码可以证明这三点
- 子类调用父类的静态变量,子类不会被初始化
- 通过数组定义来引用类,不会触发类的初始化
- 访问类的常量,不会初始化类
public class Test {
public static void main(String[] args) {
System.out.println(SubClass.value);// 被动应用1
SubClass[] sca = new SubClass[10];// 被动引用2
System.out.println(ConstClass.HELLOWORLD);// 被动引用3
}
private static class SuperClass {
static {
System.out.println("superclass init");
}
public static int value = 123;
}
private static class SubClass extends SuperClass {
static {
System.out.println("subclass init");
}
}
private static class ConstClass {
static {
System.out.println("ConstClass init");
}
public static final String HELLOWORLD = "hello world";
}
}
输出:
superclass init
123
hello world
public class Test {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1 = " + singleTon.count1);
System.out.println("count2 = " + singleTon.count2);
}
private static class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0;
private SingleTon() {
count1++;
count2++;
}
public static SingleTon getInstance() {
return singleTon;
}
}
}
输出: 准备阶段将几个变量做初始赋值。初始化阶段先执行new SingleTon()调用构造方法将count1,count2都变成1
后来调用count2 = 0进行赋值,将count2赋值成0了。
count1 = 1
count2 = 0
类初始化顺序注意点
- 先执行父类的静态代码块和静态变量初始化,并且静态代码块和静态变量的执行顺序只跟代码中出现的顺序有关。
- 执行子类的静态代码块和静态变量初始化。
- 执行父类的实例变量初始化
- 执行父类的构造函数
- 执行子类的实例变量初始化
- 执行子类的构造函数
- 如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的
- 接口初始化不会导致父接口的初始化
双亲委派模型:
- 目的是保证安全性,我们不会重写String之类的
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,
只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
- 启动类加载器:Bootstrap ClassLoader,跟上面相同。主要加载JVM自身工作需要的类:将%JAVA_HOME%\lib路径下或-Xbootclasspath参数指定路径下的、能被虚拟机识别的类库(仅按照文件名识别,如:rt.jar,名字不符合的类库不会被加载)加载至虚拟机内存中。启动类加载器是无法被 Java 程序直接引用的。
- 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器:Application ClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
自定义类加载器
- 因为系统的ClassLoader只会加载指定目录下的class文件,如果你想加载自己的class文件,那么就可以自定义一个ClassLoader.
- 而且我们可以根据自己的需求,对class文件进行加密和解密。
- Java 能否自定义一个类叫 java.lang.System:根据双亲委派模型即使使用了自定义类加载器去加载自定的类,还是会先委派给最上层的类加载器,所以最后还是去加载了系统的java.lang的类
类加载相关问题排查
- 循环引用同步锁问题:Java类加载同步锁故障排查与修复
- 引用不能初始化响应类问题: 从排查一个登录问题看jvm类加载机制