与C/C++那些需要在编译器期进行连接工作的语言不同,Java类的加载、连接和初始化都是在程序运行时完成的,只有在类被需要的时候才进行动态加载。
1)JVM何时加载类?
有且只有以下5种情况:
- 创建新对象(new)、设置/读取static字段(putstatic/getstatic)或调用静态方法(invokestatic)这四条指令时,如果该类没有初始化,则初始化。
- 使用java.lang.reflect包得方法进行反射调用的时候,如果该类没有初始化,则初始化。
- 当初始化一个类时,父类没有初始化,则先初始化父类。
- 当虚拟机启动,需要执行main()的主类,JVM首先初始化该类。
- JDK 1.7的动态语言支持时,如果java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则初始化。
2)如何加载类?
类加载过程包括加载(Loading)、链接(Linking)和初始化(Initialization)三个过程。
加载(Loading)
- 其中“根据全限定名获取字节流”的过程,可以有系统提供的类加载器完成,也可以由用户自定义类加载器。
- 对于HotSpot,Class对象虽然是对象,但仍然存放在方法区中。
链接-准备
- 该阶段值正式在方法区为类变量分配内存初始化类变量,
public static int value = 123
在准备阶段初始化零值,而非123。初始化123的过程在初始化阶段完成。
链接-解析
- 将常量池中的符号引用替换成直接引用。其中对于非虚方法,在类加载阶段就可以确定调用的版本,因此可以在此阶段直接解析为直接引用;为对于虚方法(即支持多态),无法在此阶段确定调用版本,虚方法的符号引用需等到程序执行到该符号引用的字节码时才能解析为直接引用。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法类型等符号引用。
- 执行读写字段(getstatic、putstatic、getfield、putfield)、instanceof、方法调用(iinvokestatic、invokespecial、invokevirtual、invokedynamic)、new等操作符号引用的字节码之前,需要先进行符号引用解析。
初始化(Initialization)
- 执行类构造器
<clinit>
:自动收集static变量和static{}块,按原文件出席西安的顺序执行初始化。 -
<clinit>
由编译器自动生成,如果没有static变量和static{}块,就不会生成。
“根据全限定名获取字节流”的过程,即类加载器的工作。JVM通过ClassLoader和类本身共同判断两个Class是否相同。即使两个类属于同一个Class文件,如果加载他们的ClassLoader不同,那么他们将被视为不同的类。
对于核心库类如java.lang.Object,如果用户程序自己编写了一个名为java.lang.Object的类,并使用自定义类加载器加载,那么将导致程序中存在多个java.lang.Object类,程序会十分混乱,为了保证程序加载的都是核心库类的java.lang.Object,引入了双亲委托模型。
双亲委托模型
双亲委托模式的思路是:当一个类加载器收到了类加载请求,它会首先将该请求委托给父类加载器去完成,并且每一个层次类加载器都是如此,只有父类加载器无法完成加载请求时,子类加载器才尝试自己去加载。
启动类加载器(Bootstrap Class Loader) 基本由本地代码实现,用于加载Java最基本API如rt.jar
。只用于加载<JAVA_HOME>/lib
目录下具有高可靠性的类。classLoader=null
即表示启动类加载器。
扩展类加载器(Extension Class Loader) 加载Java extension APIs,如security extension功能类。加载位于<JAVA_HOME>/lib/ext
路径的类。
系统类加载器(System Class Loader) 程序默认类加载器,加载用户类路径classpath的类。
用户自定义加载器(User Defined Class Loaders) 自定义类加载器,如Tomecat对每一个Web应用程序对应一个WebApp类加载器,每个JSP文件对应一个Jsp类加载器。
图片出处:http://blog.jamesdbloom.com/JVMInternals.html#constant_pool
扩展阅读:深入探讨 Java 类加载器