类加载机制
- 类的生命周期
一个Java类,从.java文件到可以使用到最后使用结束,经历的过程包含:
step | 作用 | 备注 |
---|---|---|
编译 | 从.java到.class文件,使得可以被虚拟机识别和使用 | - |
加载 | (被虚拟机读入内存) | - |
验证 | 验证Class字节流的数据是否遵守JVM的规定 | - |
准备 | 正式为类变量(静态变量)分配内存并设置初始值,并非代码中设置的值 | - |
解析 | 将常量池中的符号引用解析为直接引用 | 验证、准备和解析属于连接阶段 |
初始化 | 真正执行类中定义的Java代码 | - |
使用 | - | - |
卸载 | - | - |
加载、验证、准备、初始化和卸载的执行顺序是确定的,解析阶段可能会在初始化之后,这也是Java动态特性的支撑。
- 加载
读取class文件的二进制字节流(不问来源,通过类的全限定类名);将二进制流代表的静态存储结构转化为方法区的运行时数据结构;在堆中生成对应的Class对象,作为方法区数据结构的入口。
什么时候加载一个class文件,JVM规范没有硬性规定,可以添加JVM参数+XX:+TraceClassLoading来查看类的加载 - 解析
JVM规范没有规定解析的执行时机,只要求在执行anewarray
,checkcast
,getfield
,getstatic
,instanceof
,invokeinterface
,invokespecial
,invokestatic
,invokevirtual
,multianewarray
,new
,putfield
,putstatic
这13个字节码指令之前,对他们所使用的符号引用要解析完成。 - 初始化
初始化时类加载的最后阶段,初始化阶段是执行类构造器<clinit>()
方法的过程。
初始化的执行时机,有四种情况会触发初始化:
- 遇到new、getstatic、putstatic或invokestatic指令时,如果相关的类没有初始化,会触发初始化。场景有new创建对象,读写类的静态变量或调用类的静态方法。注意如果是编译时期加入常量池的静态变量(final static 常量),那么这个静态变量与定义它的类已经剥离了关系,这种调用不会触发该类的初始化。
例如:
public class InitOrder {
static {
System.out.println("initOrder init!");
}
public static void main(String[] args) {
System.out.println("before get final static var");
// Demo.A在编译时加入了常量池,是共享的数据,访问不会触发Demo类的初始化
int a = Demo.A;
System.out.println("get final static var end");
System.out.println("before get static var");
// 非常量的访问 会触发初始化
int b = Demo.a;
System.out.println("get static var end");
}
}
class Demo {
static int a = 100;
final static int A = 1000;
static {
System.out.println("Demo init");
}
}
// 结果
// initOrder init!
// before get final static var
// get final static var end
// before get static var
// Demo init
// get static var end
- 使用Java反射机制的时候,如果类没有初始化,会触发初始化。
- 初始化一个类时,会先初始化其父类(如果父类没有初始化的话)
- JVM启动时,程序的入口类会先初始化
- 使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且这个句柄对应的类没有进行过初始化,则需要先进行初始化。
有且仅有以上5种场景会触发初始化。
<clinit>() <init>()
<clinit>()
方法是由编译器自动收集类中静态变量的赋值语句以及静态代码块中定义的语句合并产生的,且内部的语句的顺序是由定义的顺序决定的,后面的语句可以访问前面定义的变量,反过来是不可以的。即静态代码块只能访问定义在此之前的静态变量。<clinit>()
和<init>()
是不同的,前者对应的是类,后者对应的是实例,即前一个是Class的构造器(是编译器生成的),后者是实例对象的构造器(也就是我们定义或继承的构造函数)。且虚拟机会保证子类的<clinit>()
执行之前,其父类的<clinit>()
一定执行完成,无需显式指定。所以第一个执行<clinit>()
的类是java.lang.Object
;- 因为父类比子类先执行
<clinit>()
,所以父类的静态变量和静态代码块是先于子类执行的。- 如果一个类或者接口中没有静态变量或静态代码块,编译器可以不生成
<clinit>()
- 接口中没有静态代码块,但是可以有静态变量。所以可以有
<clinit>()
的初始化动作,但是接口和类不同之处在于接口不需要先执行父接口的<clinit>()
方法。- 虚拟机会保证一个类的
<clinit>()
方法在多线程环境中能被正确的同步,利用这一点可以实现线程安全的单例模式。
初始化结束后,一个类就可以被正常的使用了。
loadClass
loadClass是抽象类ClassLoader中实现的方法,先不看classLoader的加载机制,看一下loadClass的实现:
源码:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
// 省略加载机制实现代码
if (resolve) {
resolveClass(c);
}
return c;
}
}
从源码来看,当调用ClassLoader.loadClass()方法时,调用的是loadClass(name,false)
,注意到第二个参数false
,看一下源码就能理解是设置是否resolve,所以loadClass
只是加载,不会解析更不会初始化。
Class.forName()
Class.forName()也可以用来加载一个指定类,那它和上面的loadClass
有什么不同呢?看一下源码:
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
return forName0(className, true,
ClassLoader.getClassLoader(Reflection.getCallerClass()));
}
/** Called after security checks have been made. */
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException;
很清楚可以看出来,第二个参数的true
对应的是initialize,就是这个方法加载完类会初始化。
有一点注意一下,这个方法加载类使用的类加载器是调用这个方法的类所使用的类加载器,ClassLoader.getClassLoader(Reflection.getCallerClass())
。
参考资料:
《深入理解Java虚拟机:JVM高级特性与最佳实践》 周志明 著