ClassLoader
参考 blog:
Java 类加载机制是技术体系中比较重要的部分,对其背后机理有一定理解有助于排查 ClassNotFound
问题,以及自定义 ClassLoader、动态加载 jar 包等高端用法,同时对理解 java 虚拟机的连接模型以及 java 语言的动态性都有一定帮助。
0. Java 类加载
在学习类加载器之前,首先应该了解类加载的相关知识。
- 什么是类加载?
- 类加载的过程是什么?
- 类加载为什么要这样设计?
0.1 什么是类加载
类加载就是将类的字节码信息加载到内存中,具体放到 jvm 运行时数据区的方法区内,并在在堆中创建这个类的 java.lang.Class 对象。这个 Class 对象封装了类在方法区内的数据结构,并且向程序员提供了访问方法区的数据结构的接口,例如反射、clazz.newInstance()。
我们聊的类加载实际上是类的生命周期的第一步,只包含了三个步骤:
- 通过类的全限定名获得定义此类的二进制字节流,也就是 .class 文件
- 将字节流所带表的静态存储结构转化为方法区的运行时数据结构
- 在内存生成一个代表这个类的 java.lang.Class 对象,作为这个类在发发去各种数据的访问入口(Class 对象是存在堆中的),注意这时候并没有做连接和初始化,也就是说这个 Class 对象没有生成完全?
一般加载类有几种方式:
- 从本地系统直接加载 .class 文件
- 通过网络加载 .class 文件
- 从 zip、jar 等归档文件中加载 .class 文件
- 从专有数据库中提取 .class 文件
- 将 Java 源文件动态编译为 .class 文件,再加载
newInstance
这里补充一个知识点,创建一个对象有两种方式,一种是 clazz.newInstance(),使用类加载机制创建的对象的;另一种是常用的 new 关键字,
除了 Class 的 newInstance,还有 Constructor 的 newInstance,通过 clazz.getDeclaredConstructor 可以拿到类的构造器,然后用构造器 newInstance,就可以弥补 clazz.newInstance() 只能使用默认无参构造函数的缺陷了。
- newInstance() 必须保证这个类被加载,且被连接,可以拿到类对象 clazz
- newInstance() 可以实现解耦
- new 关键字,如果类没有被加载,那么就会先加载
- new 可以调用任何构造方法,newInstance() 只能调用默认的无参构造方法
- new 出来的对象是强类型的,效率高;newInstance() 是弱类型的,效率相对较低
前文提到的都是方法区。不同的虚拟机实现只是遵循了虚拟机的概念模型(逻辑),但是实现可能不同(物理)。Hotspot 在 JDK 1.7 之前,permgen 承担了方法区的任务和其他任务,permgen 物理上来说是在堆上,但是在逻辑上是独立的。而在 1.8 后,permgen 被移除,取而代之的是 metaspace,而 metaspace 又不是借助堆实现的。
0.2 类加载的过程
找到 .class 二进制码,加载到内存(方法),对数据进行校验,转换解析和初始化,然后生成 Class 对象,生成可以被虚拟机执行局使用的 Java 类型的过程就是虚拟机的 类加载机制
。
这里要提到类的生命周期:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
其中 2,3,4 又被称作连接过程
加载阶段在前面已经提到过,实际上加载和连接的部分内容(例如验证动作)是交叉进行的,加载尚未完成时,连接可能已经开始。
验证主要为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求。
准备阶段是为类变量的 static 成员变量分配内存并设置类变量初始值的阶段。也就是说对于 static int value = 123; 这种,在这个阶段会给个 0 值。而不会给 123。赋值 123 是在初始化阶段才会执行。 如果是 static final int value = 123; 那么就会在准备阶段初始化为 123。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。在这个阶段执行
public class Test{
static{
i=0;
System.out.println(i);//Error:Cannot reference a field before it is defined(非法向前应用)
}
static int i=1;
}
虚拟机会保证一个类的类构造器 <clinit> () 在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行 <clinit>() 方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行 <clinit>() 方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行 <clinit>() 方法,因为 在同一个类加载器下,一个类型只会被初始化一次。此外,类构造器 <clinit>() 与实例构造器 <init>() 不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器 <clinit>() 执行之前,父类的类构造 <clinit>() 执行完毕。由于父类的构造器 <clinit>() 先执行,也就意味着父类中定义的静态语句块/静态变量的初始化要优先于子类的静态语句块/静态变量的初始化执行。特别地,类构造器
0.3 什么时候类加载
Java 与其他编译时需要进行连接工作的语言不同,它是在运行期间完成类型的加载和连接,这样虽然在类加载时会增加些性能开销,但是却提高了高度的灵活性。多态就是依赖运行期动态加载和连接这个特点实现的,在运行时再指定其实际实现。
虚拟机规范中并未规定何时开始加载一个类,这点交给虚拟机的自由实现来把握。但是类初始化的时机是有严格规定的:
- new 的时候
- putstatic,getstatic,invokestatic 的时候,也就是读取或者设置一个类的静态字段(被 final 修饰,已在编译期把结果上襦常量池的静态字段除外)的时候
- 调用一个类的静态方法的时候
- 反射调用的时候
- 初始化一个类,父类还没有被初始化的时候,则需要先触发其初始化
- 启动时的主类
1. Java 虚拟机类加载器结构
JVM 有三种预定义的类加载器,Bootstrap ClassLoader、Extension ClassLoader、System ClassLoader。除了 Bootstrap ClassLoader,其余两个在 Launcher
类中可以找到它们的定义。
这里有个疑问?Launcher 在 jvm 中是单例存在吗?ExtClassLoader 和 AppClassLoader 是单例吗?
1.1 类加载器介绍
1.1.1 Bootstrap ClassLoader
启动类加载器,又叫引导类加载器,native 实现的类加载器,负责将 Java_Runtime_Home/lib 下面的核心库 rt.jar
,或者 -Xbootclasspath 选项指定的 jar 包加载到内存。因为引导类加载器涉及到了虚拟机本地实现细节,是用 C++ 写的,不是 ClassLoader 的子类,所以开发者无法获取到引导类加载器的引用,不允许通过引用直接操作。
1.1.2 Extension ClassLoader
扩展类加载器,由 sun.misc.Launcher$ExtClassLoader 实现。负责将 Java_Runtime_Home/lib/ext 或者 -Djava.ext.dir 指定位置中的类库加载到内存中。
1.1.3 System ClassLoader
系统类加载器,由 sun.misc.Launcher$AppClassLoader 实现,负责将 系统类路径 java -classpath 或 -Djava.class.path 变量指定的目录下的类库加载到内存中。
1.2 双亲委派机制
对于双亲委派机制可能比较熟悉了,JVM 在加载类时默认采用的是双亲委派机制。通俗的说,就是某个特定的类加载器在接到加载类请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成,则返回,否则自己尝试加载。
双亲委派机制是在 ClassLoader 的 loadClass
方法中实现的,标准扩展类加载器和系统类加载器都遵循了双亲委派机制,因为他们都继承了 ClassLoader,而且没有重写 loadClass 方法。在 loadClass 的逻辑中,1.8 是这样实现的:
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) {
long t0 = System.nanoTime();
try {
if (parent != null) { // 委派给父类加载器加载
c = parent.loadClass(name, false);
} else { // 可以看到如果没有 parent 就会默认委派给 BootstrapClassLoader 加载
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);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
一般介绍双亲模型时,会把 BootstrapClassLoader 作为 ExtensionClassLoader 的父类,但是实际上并不是如此。从上述代码可以看到,实际上,BootstrapClassLoader 是在找不到 parent 的时候才会被委派,而 AppClassLoader 的 parent 会被指定为 ExtensionClassLoader,而 ExtensionClassLoder 的 parent 则为 null。这点可以从构造函数中获取到答案。
ClassLoader 中的构造函数中,parent 默认是通过 getSystemClassLoader() 方法获取到的系统构造类加载器
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
AppClassLoader 的构造函数中传入了 parent ClassLoader,这个构造函数是 protect 的,在 AppClassLoader 的 getAppClassLoader 静态方法里被调用,而这个静态方法需要一个 ClassLoader 作为 AppClassLoader 的 parent。再看 Launcher 的构造方法,这个参数是 Launcher.ExtClassLoader.getExtClassLoader() 生成的,也就是说,在构造 AppClassLoader 时传入的是 ExtClassLoader。
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null?new File[0]:Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null?new URL[0]:Launcher.pathToURLs(var2);
// 传入的是 var0
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
// 这里要传 parent
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 扩展类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
...
try {
// 把扩展类加载器塞给了系统类加载器,完美
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
ExtClassLoader 的构造函数很容易看,parent 直接传入的是 null。
public ExtClassLoader(File[] var1) throws IOException {
// 第二个参数 parent 传入了 null ClassLoader,所以 ExtClassLoader 没有父类加载器
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
值得一提的是,如果把自己的 jar 放到 ext 加载的目录下,是可以被扩展类加载器正常加载的。而如果放到 lib 下,是不会被启动类加载器加载的,这是虚拟机处于安全考虑,做了过滤。
1.3 Class.forName
在了解了虚拟机的双亲委派模型后,先了解一下 Class.forName 这个方法
/**
* Returns the {@code Class} object associated with the class or
* interface with the given string name, using the given class loader.
* Given the fully qualified name for a class or interface (in the same
* format returned by {@code getName}) this method attempts to
* locate, load, and link the class or interface. The specified class
* loader is used to load the class or interface. If the parameter
* {@code loader} is null, the class is loaded through the bootstrap
* class loader. The class is initialized only if the
* {@code initialize} parameter is {@code true} and if it has
* not been initialized earlier.
*
* <p> If {@code name} denotes a primitive type or void, an attempt
* will be made to locate a user-defined class in the unnamed package whose
* name is {@code name}. Therefore, this method cannot be used to
* obtain any of the {@code Class} objects representing primitive
* types or void.
*
* <p> If {@code name} denotes an array class, the component type of
* the array class is loaded but not initialized.
*
* <p> For example, in an instance method the expression:
*
* <blockquote>
* {@code Class.forName("Foo")}
* </blockquote>
*
* is equivalent to:
*
* <blockquote>
* {@code Class.forName("Foo", true, this.getClass().getClassLoader())}
* </blockquote>
*
* Note that this method throws errors related to loading, linking or
* initializing as specified in Sections 12.2, 12.3 and 12.4 of <em>The
* Java Language Specification</em>.
* Note that this method does not check whether the requested class
* is accessible to its caller.
*
* <p> If the {@code loader} is {@code null}, and a security
* manager is present, and the caller's class loader is not null, then this
* method calls the security manager's {@code checkPermission} method
* with a {@code RuntimePermission("getClassLoader")} permission to
* ensure it's ok to access the bootstrap class loader.
*
* @param name fully qualified name of the desired class
* @param initialize if {@code true} the class will be initialized.
* See Section 12.4 of <em>The Java Language Specification</em>.
* @param loader class loader from which the class must be loaded
* @return class object representing the desired class
*
* @exception LinkageError if the linkage fails
* @exception ExceptionInInitializerError if the initialization provoked
* by this method fails
* @exception ClassNotFoundException if the class cannot be located by
* the specified class loader
*
* @see java.lang.Class#forName(String)
* @see java.lang.ClassLoader
* @since 1.2
*/
@CallerSensitive
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}
这个方法用于获取 Class 对象,其实也是完成了加载类的部分,initialize 是控制是否在类加载的同时初始化的,有些时候会需要强制初始化,而这里面会传入一个 loader。那么在调用 forName 方法时,到底触发哪个类加载器进行加载呢。如果没有指定的情况下,会使用调用类的类加载器进行加载,可以看到它拿到了调用者,之后,拿到了调用者的 classLoader。
值得提的是,不同的 classLoader 加载同一个类,包路径相同,但是 Class 对象是不同的,因为 ClassLoader 不同。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer 引用了类 com.example.Inner,则由类 com.example.Outer 的定义加载器负责启动类 com.example.Inner 的加载过程。方法 loadClass() 抛出的是 java.lang.ClassNotFoundException 异常;方法 defineClass() 抛出的是 java.lang.NoClassDefFoundError 异常。
2. 用户自定义类加载器
从双亲委派模型和 ClassLoader 的默认构造函数的逻辑可以看出,用户自定义类加载器,即便没有指定 parent,同样可以加载下面三个地方的类:
- Java_Runtime_Home/lib下的类;
- Java_Runtime_Home/lib/ext下或者由系统变量 java.ext.dir 指定位置中的类;
- 当前工程类路径下或者由系统变量 java.class.path 指定位置中的类。
因为 ClassLoader 构造函数默认把 系统类加载器 作为父类加载器。
如果把父类加载器强制设为 null 呢,在没有重写 loadClass 的情况下,/lib 下的类还可以加载,但是 ext 下的就不可以了。
在自定义类加载器时,有些需要注意的点:
- 尽量不要重写 loadClass 的委派逻辑,避免无法加载那些 lib 或者 ext 下的类
- 正确设置父加载器类
- findClass 逻辑正确,能够获取到对应的字节码
3. 线程上下文类加载器
线程上下文类加载器,可通过方法 getContextClassLoader()
和 setContextClassLoader()
获取和设置。在没有指定线程上下文加载器的情况下,线程将继承父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是应用类加载器,所以在不指定的情况下就默认是 应用类加载器。
为什么要有线程类加载器这个概念呢?
假设当前只有双亲委派模型,那么并不能解决 Java 应用开发中遇到的全部的类加载问题。Java 提供了很多服务提供接口 SPI (Service Provider Interface),允许第三方为这些接口提供实现。比如说 JDBC,JCE,JAXP,JNDI 等等。这些 SPI 接口由 Java 核心库提供,而这些 SPI 接口的实现代码很可能是由作为 java 应用所依赖的 jar 包,可通过类路径 classpath 找到。问题在于,SPI 的接口是 Java 核心库的一部分,是由启动类加载器实现的,而 SPI 的实现类一般是由系统类加载器加载的。启动类加载器无法加载 SPI 的实现,也没法交给它的子类系统类加载器来加载实现类。
这里有点不理解了,就是如果在加载的时候,都用系统类加载器来加载,如果想要加载核心库的部分,就向上交给启动类加载器不就行了?实际上,在加载接口的时候,用的是启动类加载器,而在加载实现类的时候,用的又是系统类加载器。这里可能涉及到一个问题,就是类加载器是怎么被调用的。一种是显式的调用,一种是隐式的调用。我觉得这里是选择的关键,能够解释为什么 SPI 不能够使用系统类加载器,自底向上的去找到启动类加载器。我们知道,Java 核心代码的类加载器是启动类加载器,而核心代码中要用到 SPI 实例类,就需要加载这个类,而由于是在当前这个类里,会用到调用方的类加载器,这点之前也说过,这时候就不能用双亲委派模型了。这是一个反向的过程!所以线程上下文类加载器提供了一套新的体系或者是环境,用于打破这个规则。当前类加载器是一个体系,而线程上下文类加载又是另一个体系,它是一个后门!使用当前类加载器已成为缺省规则,它们广泛应用在类声明、Class.forName等情景中。而 ContextClassLoader 是一个后门,不能隐式的替代当前类加载器
使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载模式,而是用线程上下文类加载器来加载需要的类,这样就可以显示的指定类加载器。大部分的 java application 如 jboss,tomcat 都是采用 contextClassLoader 来处理 web 服务。还有一些采用 hot swap 的框架,也是采用线程上下文类加载器。
使用线程上下文类加载器也有自己的弊端。前面讲过如何判断两个类是同一个类,前提就是使用了同一个类加载器。所以如果当多个线程间需要通信的时候,而通信的对象的类正好是线程上下文类加载器加载的类,那么就要求用的是同一个类加载器,防止类型转换异常。
线程上下文类加载器不是一种类型的类加载器,类加载器分为 启动类加载器、扩展类加载器、应用类加载器、自定义类加载器,而类加载器会存放或者说出现在三个地方供获取,系统类加载器、当前类加载器(所在类使用的加载器,记得调用方的类加载器吗)、线程上下文类加载器。
- 系统类加载器
- ClassLoader.getSystemClassLoader()
- 系统类加载器是最后创建的类加载器
- 当前类加载器
- Class.forName
- Class.getResource
- 线程上下文类加载
一般来说,上下文类加载器要比当前类加载器更适合于框架编程,而当前类加载器则更适合于业务逻辑编程。
类加载器与 web 容器
对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
(1)每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
(2)多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
(3)当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。
总结
首先,类加载器的任务其实就是把字节码加载到内存,配合链接初始化生成一个可用的类对象放在堆里。在 JVM 启动时,首先交由 启动类加载器(C++)实现,来做这些工作。而这些类都有了自己的当前类加载器。接下来,Bootstrap ClassLoader 会加载 Launcher 这个类,这个类里有 ExtClassLoader 和 AppClassLoader,扩展类加载器就负责加载 /lib/ext 下的类,而 AppClassLoader 会在后续作为系统类加载器使用。同时 Launcher 指定了 ExtClassLoader 和 AppClassLoader 的 parent,构建了类加载的双亲委托模型。AppClassLoader 负责加载 main 的类,作为当前类加载器,main 下的方法都会用到调用类的类加载器。当构建新的线程时,这时候就由线程上下文类加载器体系来解决,子线程会继承父线程的线程类加载器。这时候新线程的当前类加载器以及线程上下文的类加载器是系统类加载器。而由于线程上下文类加载器的引入,保证了类加载更加灵活,可以通过替换线程上下文的类加载器来打破双亲模型。
留下的两个疑问
- 扩展类加载器的作用,为啥要分启动类加载器和扩展类加载器?
启动类加载器加载那些不希望被重写的类,这些类是 jdk 的核心,不希望被覆盖掉,所以用启动类加载器来加载。而 ext 没有那么重要,用户也可以把 jar 包放到扩展这里,所以这部分就搞了 ext 类加载器。
- 一个新线程被创建后,当前类加载器和线程上下文类加载器是什么?在线程中创建的话,究竟是谁来做类加载?线程上下文类加载器和当前类加载的关系是什么?
第二个问题,我感觉是由 Thread 的类加载器作为线程中 run 方法中新类的类加载。实际上当前类加载器和线程上下文类加载器没什么关系。