类的生命周期
加载、验证、准备、解析、初始化、使用、卸载。
java支持动态语言绑定,解析可能在初始化之后。其他的顺序是固定的。
类加载的过程
加载
- 根据类的全限定类名获取定义类的二进制字节流
- 将二进制字节流代表的类静态存储结构转化为方法区的数据结构
- 在堆中生成一个java.lang.Class对象封装方法区的数据结构,作为方法区的数据结构的入口
验证
文件格式验证
:验证字节流是否符合class文件规范。是否以魔数0XCAFEBABY开头,主次版本号是否在jvm的处理范围,常量是否有不存在的类型等。元数据验证
:对字节流描述的信息进行分析,验证是否符合java语言语法规范。是否有父类,继承链是否正确,与父类的字段是否有冲突,抽象父类和接口的方法是否都实现等。字节流验证
:分析数据流和控制,分析语义是否合法、符合逻辑。特别是对方法体进行验证,例如验证跳转指令是否跳转到方法体之上。符号引用验证
:确保之后的解析阶段可以将符号引用转换为直接引用。主要是验证是否有无法访问到类、接口、字段、方法等。
准备
给类静态变量分配内存,并赋予默认零值。被final修饰的静态变量在准备阶段初始化。
解析
将常量池中的符号引用转换为直接引用。主要是类、接口、方法、字段等。符号引用就是字面常量符号。直接引用,能直接指向目标,偏移量或句柄。
初始化
执行clinit()
,为静态变量赋予正确的初始值。若类无静态变量声明和静态代码块,该类可无clinit()
。若接口无静态变量声明,则该接口可无clinit()
。
clinit()
是按顺序收集静态变量声明和静态代码块得到的。jvm会保证clinit方法线程安全,相当于被synchronized修饰的静态方法。
类初始化时机
- 访问静态变量、赋值给静态变量、调用静态方法。注意不包括访问静态常量。
- new类的实例、Class对象的newInstance
- 反射Class.forName("")
- 初始化子类前会先初始化父类。
- 启动类会首先被jvm加载
- MethodHandle和VarHandle可以看作是轻量级的反射机制,调用这两个指令必须先对要调用的类初始化
- 使用了default关键字的接口在实现类初始化时,该接口也要初始化
xxclassLoader的loadClass不会导致类初始化。类.class获取Class对象也只是不会导致类初始化。
类的唯一性
jvm中类由全限定名和类加载器唯一确定。
类加载器
启动类加载器(BootstrapClassLoader)
c++实现,为jvm的一部分。加载jre/lib下的几个核心jar包,例如rt.jar等。尝试获取启动类加载器的话会返回null。
扩展类加载器(ExtClassLoader)
继承自java.lang.ClassLoader,加载jre/lib/ext和java.ext.dirs系统属性指定的路径下的类。
应用类加载器(系统类加载器 AppClassLoader)
继承自java.lang.ClassLoader,加载java命令指定的-classpath、系统属性java.class.path、环境变量CLASSPATH下的类。ClassLoader.getSystemClassLoader()
返回应用类加载器。应用类加载器是默认加载器。parent为扩展类加载器。
自定义类加载器
- 继承java.lang.ClassLoader,默认parent为应用类加载器。
- 实现findClass方法
双亲委派模型
类加载请求到达类加载器时,先委托给父类加载器加载,若父类加载器还有父类加载器则递归向上委托,最终委托给启动类加载器。当父类加载器在其搜索范围找不到类时才由子类加载器自己加载。
ClassLoader的loadClass方法定义了双亲委派模型的逻辑。
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 (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派模型好处
- 使基础类得到统一,防止基础类被篡改。
- 确保只有一份Class对象。
类加载机制
- 全盘负责。某个类加载器负责加载一个类时,该类加载器也负责加载类中引用的类。
- 双亲委派。
- 缓存机制。被加载过的类的Class对象会被缓存起来。获取Class对象时,先在缓存中查找是否有,若无才去加载。这就是修改class后,需要重启jvm才能生效的原因。
线程上下文加载器
线程上下文加载器用于加载线程中运行代码中引用的类。默认的线程上下文加载器为应用类加载器。默认子线程会继承父线程的线程上下文加载器。通过Thread对象的setContextClassLoader
方法可以设置线程上下文加载器。
- JNDI服务通过线程上下文加载器加载SPI(Service Provider Interface)的代码,使得父类加载器的类加载请求委托给了子类加载器。也就是逆向打通了双亲委派模型,这实际上违背了双亲委派模型。
- 代码热替换(HotSwap)、模块热部署(Hot Deployment)等,OSGi实现模块热部署的关键是自定义的类加载器机制的实现。每个程序模块(bundle)都有一个类加载器,更换bundle时,将bundle和类加载器一同替换掉以实现热部署。