37. 请你详细说说类加载流程,类加载机制及自定义类加载器 上
一、引言
当程序使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、链接、初始化三个步骤对该类进行类加载。
二、类的加载、链接、初始化
1、加载
类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象。类的加载过程是由类加载器来完成,类加载器由JVM提供。我们开发人员也可以通过继承ClassLoader来实现自己的类加载器。
1.1、加载的class来源
从本地文件系统内加载class文件
从JAR包加载class文件
通过网络加载class文件
把一个java源文件动态编译,并执行加载。
2、类的链接
通过类的加载,内存中已经创建了一个Class对象。链接负责将二进制数据合并到 JRE中。链接需要通过验证、准备、解析三个阶段。
2.1、验证
验证阶段用于检查被加载的类是否有正确的内部结构,并和其他类协调一致。即是否满足java虚拟机的约束。
2.2、准备
类准备阶段负责为类的类变量分配内存,并设置默认初始值。
2.3、解析
我们知道,引用其实对应于内存地址。思考这样一个问题,在编写代码时,使用引用,方法时,类知道这些引用方法的内存地址吗?显然是不知道的,因为类还未被加载到虚拟机中,你无法获得这些地址。
举例来说,对于一个方法的调用,编译器会生成一个包含目标方法所在的类、目标方法名、接收参数类型以及返回值类型的符号引用,来指代要调用的方法。
解析阶段的目的,就是将这些符号引用解析为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必会触发解析与初始化)。
3、类的初始化
类的初始化阶段,虚拟机主要对类变量进行初始化。虚拟机调用< clinit>方法,进行类变量的初始化。
java类中对类变量进行初始化的两种方式:
在定义时初始化
在静态初始化块内初始化
3.1、< clinit>方法相关
虚拟机会收集类及父类中的类变量及类方法组合为< clinit>方法,根据定义的顺序进行初始化。虚拟机会保证子类的< clinit>执行之前,父类的< clinit>方法先执行完毕。
因此,虚拟机中第一个被执行完毕的< clinit>方法肯定是java.lang.Object方法。
public class Test { static int A = 10; static { A = 20; } } class Test1 extends Test { private static int B = A; public static void main(String[] args) { System.out.println(Test1.B); } } //输出结果 //20
从输出中看出,父类的静态初始化块在子类静态变量初始化之前初始化完毕,所以输出结果是20,不是10。
如果类或者父类中都没有静态变量及方法,虚拟机不会为其生成< clinit>方法。
接口与类不同的是,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>方法。
public interface InterfaceInitTest { long A = CurrentTime.getTime(); } interface InterfaceInitTest1 extends InterfaceInitTest { int B = 100; } class InterfaceInitTestImpl implements InterfaceInitTest1 { public static void main(String[] args) { System.out.println(InterfaceInitTestImpl.B); System.out.println("---------------------------"); System.out.println("当前时间:"+InterfaceInitTestImpl.A); } } class CurrentTime { static long getTime() { System.out.println("加载了InterfaceInitTest接口"); return System.currentTimeMillis(); } } //输出结果 //100 //--------------------------- //加载了InterfaceInitTest接口 //当前时间:1560158880660
从输出验证了:对于接口,只有真正使用父接口的类变量才会真正的加载父接口。这跟普通类加载不一样。
虚拟机会保证一个类的< clinit>方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的< clinit>方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>方法完毕。
public class MultiThreadInitTest { static int A = 10; static { System.out.println(Thread.currentThread()+"init MultiThreadInitTest"); try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { Runnable runnable = () -> { System.out.println(Thread.currentThread() + "start"); System.out.println(MultiThreadInitTest.A); System.out.println(Thread.currentThread() + "run over"); }; Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); } } //输出结果 //Thread[main,5,main]init MultiThreadInitTest //Thread[Thread-0,5,main]start //10 //Thread[Thread-0,5,main]run over //Thread[Thread-1,5,main]start //10 //Thread[Thread-1,5,main]run over
从输出中看出验证了:只有第一个线程对MultiThreadInitTest进行了一次初始化,第二个线程一直阻塞等待等第一个线程初始化完毕。
3.2、类初始化时机
当虚拟机启动时,初始化用户指定的主类;
当遇到用以新建目标类实例的new指令时,初始化new指令的目标类;
当遇到调用静态方法或者使用静态变量,初始化静态变量或方法所在的类;
子类初始化过程会触发父类初始化;
如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口初始化;
使用反射API对某个类进行反射调用时,初始化这个类;
Class.forName()会触发类的初始化
3.3、final定义的初始化
注意:对于一个使用final定义的常量,如果在编译时就已经确定了值,在引用时不会触发初始化,因为在编译的时候就已经确定下来,就是“宏变量”。如果在编译时无法确定,在初次使用才会导致初始化。
public class StaticInnerSingleton { /** * 使用静态内部类实现单例: * 1:线程安全 * 2:懒加载 * 3:非反序列化安全,即反序列化得到的对象与序列化时的单例对象不是同一个,违反单例原则 */ private static class LazyHolder { private static final StaticInnerSingleton INNER_SINGLETON = new StaticInnerSingleton(); } private StaticInnerSingleton() { } public static StaticInnerSingleton getInstance() { return LazyHolder.INNER_SINGLETON; } }
看这个例子,单例模式静态内部类实现方式。我们可以看到单例实例使用final定义,但在编译时无法确定下来,所以在第一次使用StaticInnerSingleton.getInstance()方法时,才会触发静态内部类的加载,也就是延迟加载。
这里想指出,如果final定义的变量在编译时无法确定,则在使用时还是会进行类的初始化。
3.4、ClassLoader只会对类进行加载,不会进行初始化
public class Tester { static { System.out.println("Tester类的静态初始化块"); } } class ClassLoaderTest { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); //下面语句仅仅是加载Tester类 classLoader.loadClass("loader.Tester"); System.out.println("系统加载Tester类"); //下面语句才会初始化Tester类 Class.forName("loader.Tester"); } } //输出结果 //系统加载Tester类 //Tester类的静态初始化块
从输出证明:ClassLoader只会对类进行加载,不会进行初始化;使用Class.forName()会强制导致类的初始化。