博客地址:liuzhengyang
Java中的ClassLoader
先抛出几个问题
- 类初始化 static{} 块会在什么时候执行
- Class在方法区内的结构
- 不同类加载器加载一个类,类初始化块会被执行几次,不同ClassLoader加载的类存放在哪里,是否指向同一个Class实例
- 类A触发了类B的加载,那么类B的加载器是什么
- 如何实现热部署,即在运行时改变类的行为,或类替换
- Class.forName(name) 做了什么
回答这些问题还是要参考JDK的代码实现,另外还要依靠Java语言规范和Java 虚拟机规范
本篇文章用类来泛指类和接口
背景介绍
类加载器是用于加载Class的机制,Class可以以文件的形式或者是二进制流的形式存在,一般 按照最常见的Class文件来称呼。
<!-- -->
ClassLoader负责加载类,java.lang.ClassLoader
类是一个抽象类, 可以通过一个类的二进制名称,来定位类在哪里,并生成class
的数据定义。 最常见的策略是将类的名称转换成文件名并从文件系统中读取Class文件
.
每个Class
对象都会有指向定义它的ClassLoader
的引用,通过Class#getClassLoader()
可以获得。
类的二进制名(binary name)是Java语言规范规定的,常见的有:
java.lang.String
test.loader.Test$Test2 // 静态内部类
Class文件可以存在jar包中,可以以目录形式存放。
加载、链接、初始化过程
- 如果要加载一个类的时候发现类没有被加载过(就是JVM中没有这个类的二进制表示)就会使用类加载器尝试加载这个二进制表示。
- 链接阶段设计验证、准备和解析。
- 验证用于验证字节码格式是否正确。
- 准备阶段会进行static字段的创建,并将这些字段初始化成默认值。这个阶段不会执行任何代码。static 字段的初始化会在初始化阶段执行。
- 一些JVM实现会在准备阶段预先计算一些附加数据结构来使之后的类操作更加有效。一个特别的结构就是方法表或其他的数据结构,可以在让任意的调用在实例上的方法无需搜索父类。类的二进制表示通过符号引用引用其他类、字段、方法构造器等。对于字段和方法,符号引用包括字段、方法所在类的名字以及字段和方法自己的名称。在符号引用使用前必须要进行解析,解析阶段会对符号引用进行检查,并通常会替换成直接引用。如果解析失败,会抛出场景的NoSuchMethodError等错误。
Java中常见的ClassLoader
Java的类加载结构有bootstrap class loader用来加载$JAVA_HOME/jre/lib下载的 rt.jar中的文件,其中是Java的核心类.Bootstrap classloader是JVM中的实现, 如果要在ClassLoader中表示其为父类,用null表示。 另外有ExtClassLoader加载lib/ext文件夹下的jar包 AppClassLoader是用来加载ClassPath目录下的jar包和Class文件。 常说这三者是父类关系,并不是Java中的集成关系,而是ClassLoader中定义的 parent.
委托机制
ClassLoader文件在第一次加载类的时候会先委托其父加载器加载,如果加载失败再自己加载。 这样,一些关键的类,如String等就不会遭到篡改。但是在J2EE中,一个Web容器下 可能有多个应用,每个应用加载时有子类优先的需求,这时就需要覆盖默认的逻辑。 代码逻辑在ClassLoader类的loadClass(String name, boolean resolve)
方法中.ClassLoader#loadClass
调用的是ClassLoader#loadClass(name, false)
resolve表示的是是否进行链接步骤。 去掉一些不相关代码,loadClass方法的逻辑如下
类加载锁
synchronized (getClassLoadingLock(name)) {
// 查看是否已经被加载过,会调用一个native方法判断
Class<?> c = findLoadedClass(name);
if (c == null) {
// 如果没加载过,则会进入加载过程
try {
// 如果parent不是null,则说明是Java中的类加载器
if (parent != null) {
// 调用parent的loadClass方法递归向上加载
c = parent.loadClass(name, false);
} else {
// 如果是null,说明是Bootstrap class loader,
// 则使用Bootstrap加载器加载
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.
// 调用findClass,会找到class的字节流然后使用
// defineClass()来定义类
c = findClass(name);
}
}
if (resolve) {
// 如果要链接,则进行链接,其中有验证、准备和符号引用解析过程
resolveClass(c);
}
return c;
}
另外,Class的静态方法Class.forName(className)
也能够完成类的加载工作, 返回一个Class对象。会对类进行初始化,使用调用Class.forName的方法所在的类的类加载器 来加载。 Class.forName(className, initialize, loader)
方法会通过参数控制由是否进行初始化和由哪个类加载器加载。
ClassLoader中的loadResource方法
我们在写程序时,经常会在classpath下放置一些配置文件,在运行时读取配置文件的内容 可以通过Class.getResource(), 例如
Test.class.getResourceAsStream("/config.properties")
这个方法会委托Test类的加载器来进行加载资源
类加载器与类
当我们判断两个Class对象是否是相等时,或判断是否是集成关系时,需要看它们的类加载器是否是同一个。 类加载器和类,组成了Class对象的标识。 测试代码
自定义的类加载器,修改默认的委托机制。
private static class MyClassLoader extends URLClassLoader {
public MyClassLoader(URL[] urls) {
super(urls);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return defineClass(name);
}
}
public static class Class1 {
static {
System.out.println("Initialize class 1");
Class2.doSay();
Class<Class2> class2Class = Class2.class;
System.out.println("Class2 Loader is " + class2Class.getClassLoader());
}
public static void say() {
}
}
public static class Class2 {
static {
System.out.println("Initialize Class2");
}
public static void doSay() {
System.out.println("Say");
}
}
public static void main(String[] args) throws Exception {
URL path = ClassLoaderTest.class.getResource("/");
URL rtPath = Object.class.getResource("/");
MyClassLoader myClassLoader = new MyClassLoader(new URL[]{path, rtPath});
Class<?> class1 = myClassLoader.loadClass("classloader.ClassLoaderTest$Class1");
Class<?> aClass = Class.forName("classloader.ClassLoaderTest$Class1", true, myClassLoader);
Class<?> aClass2 = Class.forName("classloader.ClassLoaderTest$Class1");
System.out.println(class1);
System.out.println("Class 1 classLoader is " + class1.getClassLoader());
System.out.println(aClass);
}
能够看出,类A触发类B的初始化时,会用类A的加载器去加载。
什么时候回触发类的初始化
- 创建类的实例,通过new 或者Class.newInstance
- 类的初始化方法被调用 invokestatic
- 类的static字段被绑定 putstatic
- 类的static字段被使用,并且不是常量
- 一个类被初始化的时候,它的父类会被先初始化 我们看到,用两个加载器加载一个类,初始化块被执行了两次。