类加载器负责在运行时动态地加载类。类也不是一次全部加载到内存的,而是需要时才加载到内存中。系统提供了下面3中类加载器,同时也可以自定义类加载器。
一、启动类加载器(Bootstrap Class Loader)
负责加载<JAVA_HOME>/jre/lib中的类,也可以通过-Xbootclasspath参数所指定的路径加载类,也就是加载java api。该加载器在java中没有对应的类,是jvm实现的一部分,对于HotSpot来说,启动类加载器由c++语言编写。因此即使Class.getClassLoader方法返回最高层的类,只会得到null。启动类加载器是所有其他ClassLoader实例的祖先加载器。
二、扩展类加载器(Extension ClassLoader)
该加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>/lib/ext中路下的类,或者通过java.ext.dirs系统变量指定目录。为启动类加载器的子加载器,负责加载java核心类的扩展。
三、系统类加载器(System Class Loader)
叫系统或应用类加载器,负责加载用户类(ClassPath)路径上的类库。可以通过classpath环境变量、-classpath或-cp命令行选线指定。可以通过ClassLoader.getSystemClassLoader方法获得,一般情况下为程序中的默认类加载器。
四、类加载器工作过程
当jvm需要一个类时,类加载器会通过全限定类名尝试定位类然后加载类定义到方法区并生成相应的Class对象。
jvm通过ClassLoader.loaderClass方法加载类的,传入一个全限定类名。下面看看他的源码:
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 {
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;
}
}
从代码中可以看出,loadClass实现了双亲委派模型,不过现在先不讨论这个。loadClass先通过findLoadedClass方法查看内存中是否已经存在过已被加载了的类。如果没有则调用父加载器的loadClass方法加载类,也就是委派给了父加载器。如果父类加载器是启动类加载器,由于没有对应的类,因此使用findBootstrapClassOrNull方法委派给启动类加载器类加载类。如果父加载器失败了则该加载器自己尝试加载类,通过findClass方法。而resolveClass方法的作用是让类被解析。最后如果该加载类不成功,则抛出ClassNotFoundException异常。
五、双亲委派模型(Parents Delegation Model)
如果一个类加载器收到一个类加载请求后,会将请求委派给父加载器,父加载器也是如此,知道传到顶层的启动类加载器中。如果父加载器无法完成,则子加载器会尝试加载。
六、唯一性
一个类加载器确定一个类名称空间,不同类加载器加载的同一个Class文件所产生的类不属于同一个类,只有类加载器和类完整名都一致才属于同一类对象。
七、自定义类加载器
类加载器会调用loadClass方法加载类,因此可以直接覆盖该方法实现自己的类加载器。但是不建议这么做,从loadClass源码中可以看出,在父加载器加载失败时会尝试自己加载,通过findClass方法实现,因此我们可以实现该方法,这样就不会打破双亲委派模型。
八、线程上下文类加载器(Thread Context ClassLoader)
通过线程上下文加载器可以打破双亲委派模型,比如JNDI服务,它是java标准服务,由启动类加载器加载,但是启动类加载器却不能加载在classpath下的实现了类。由于双亲委派模型的存在,启动类加载器不能向下委派,于是出现了线程上下文加载器。可以通过getContextClassLoader获得它,然后委派该加载器加载类实现。
可以在线程创建时通过setContextClassLoader设置上下文类加载器,如果没有设置,那么会从父线程中继承它的线程上下文加载器。如果整个程序中都没做任何事,他会使用系统类加载器作为他的线程上下文加载器。
其实也可以委派系统类加载器来加载实现类,通过getSystemClassLoader来获得。这种做法很不推荐,因此在一些web环境下部署应用会出问题,因为web应用不会使用系统类加载器加载类,而是使用自己的定义的类加载器。
九、可以直接使用的类加载器
当动态加载资源时,可以我们至少可以选择三种类加载器:系统类加载器、当前类加载器(current classloader)、当前线程上下文类加载器。
通常我们不会选择系统类加载器,因为他会从classpath路径下加载类,在web应用中会出问题。一些ClassLoader.getSystemXXX方法会默认通过该加载器加载路由。
一般会选择使用后两个加载器。当前类加载器就是加载了当前方法所属的类的加载器。这种类加载器在类被动态解析时会被隐式使用,也会在使用Class.forName、Class.getResource和一些类似方法时隐式使用当前类加载器。当使用类字面常量时也是通过当前类加载器加载类的。
当前上下文类加载器可以通过getContextClassLoader获得。
参考:
https://blog.csdn.net/javazejian/article/details/73413292#t9
https://www.baeldung.com/java-classloaders
https://www.javaworld.com/article/2077344/core-java/find-a-way-out-of-the-classloader-maze.html
《深入理解java虚拟机》周志明