我们知道,JAVA虚拟机(后文均称之为JVM)在加载类的过程中,需要经过加载,验证,准备,解析,初始化等操作,而最初的加载阶段,便是通过类加载器ClassLoader将class文件加载到内存中的。
JVM中大致有两种类加载器,启动类加载器以及其他加载器。
启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在
其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
扩展类加载器(Extension ClassLoader):负责加载
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
说起ClassLoader,绕不过去的一个知识点就是ClassLoader的双亲委派模型,简述起来很简单: 如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。大致模型如下图:
双亲委派的最重要的一个作用是保证非核心类不会入侵到核心加载器中,防止非可信类扮演可信类,也就是为了安全。
但是就我个人来看,这种机制抹平了JAVA带来的多态优势,而且,在Tomcat和jdbc的加载过程中,常常需要破坏双亲委派模型,甚至于官方也是默认了这种做法(下文的官方JDK代码中有体现),实现确实不是特别优雅。
我们可以直接通过官网的在线网站直接访问源码,当然,也可以直接down到本地工程中打开,不过本文章只是分析很小的一部分,就直接在线看了,我们就看一下最常用的JDK8的源码,链接如下:
jdk8源码
我们首先找到src/share/classes/sun/misc/Launcher.java,该文件就是JVM加载main方法的主要类,这个文件代码比较长,我把其中主要的拷贝出来:
/** * This class is used by the system to launch the main application. */
public class Launcher {
//很明显Launcher设计上是一个单例模式
private static Launcher launcher = new Launcher();
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader(); } catch (IOException e) {
throw new InternalError("Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl); } catch (IOException e) {
throw new InternalError("Could not create application class loader", e);
}
// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
}
}
从上面代码里面可以看出,Laucher是一个单例,ClassLoader以成员变量的形式存在于Launcher中,而ClassLoader的加载,是在Launcher初始化的时候进行的,首先Launcher先加载ExtClassLoader,再将ExtClassLoader作为参数,传递进getAppClassLoader中去获取应用类加载器
loader = AppClassLoader.getAppClassLoader(extcl);
再来看看getAppClassLoader做了什么:
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
// Note: on bugid 4256530
// Prior implementations of this doPrivileged() block supplied
// a rather restrictive ACC via a call to the private method
// AppClassLoader.getContext(). This proved overly restrictive
// when loading classes. Specifically it prevent
// accessClassInPackage.sun.* grants from being honored.
//
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
/*
* Creates a new AppClassLoader
*/
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
ucp.initLookupCache(this);
}
}
我们可以看到AppClassLoader的构造方法是使用了父类的构造方法,我们再往上朔源,可以看到,ExtClassLoader和AppClassLoader是Launcher的内部类,它们继承自于URLClassLoader,继续往上层分析以后可以发现,这两个都继承于抽象类ClassLoader.java(路径在src/share/classes/java/lang/ClassLoader.java),ClassLoader存在一个ClassLoader类型的成员变量parent。也就是说,类加载器确实是以层级的形式存在的,AppClassLoader的parent是ExtClassLoader。
从上面的代码可以看出,AppClassLoader的父加载器是ExtClassLoader,但是为何ExtClassLoader.getExtClassLoader不需要BootstrapClassLoader作为参数,同时也并没找到ExtClassLoader的parent的设置代码。让我们来看看类加载时是怎么做的,通过跟踪ExtClassLoader.loadClass方法可以发现,他们都是调用了父类ClassLoader.java的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) {
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;
}
}
我们可以看到,关键代码就是
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
}
当类加载时,ClassLoader先会尝试用父加载器parent去加载,如果parent为空的话,则证明是当前的加载器是ExtClassLoader,所以调用findBootstrapClassOrNull去调用BootstrapClassLoader加载类,那为什么不将BootstrapClassLoader设置为ExtClassLoader的parent成员呢,其实通过ClassLoader.loadClass()的注释就可以大致猜想出来:
/** Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded.
* * Invoke the {@link #loadClass(String) loadClass} method * on the parent class loader. If the parent is null the class
* loader built-in to the virtual machine is used, instead.
* * Invoke the {@link #findClass(String)} method to find the
* class.
* * **/
如果parent为空,则会调用一个JAVA虚拟机内置(built-in)的ClassLoader,其实就是BootstrapLoader,那么很明显BootstrapLoader就是用非JAVA语言编写的,以native的形式存在于虚拟机中。通过findBootstrapClassOrNull的源码可以证明:
/**
* Returns a class loaded by the bootstrap class loader; * or return null if not found.
*/
private Class<?> findBootstrapClassOrNull(String name) {
if (!checkName(name))
return null;
return findBootstrapClass(name);
}
// return null if not found
private native Class<?> findBootstrapClass(String name);
双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢。典型的例子就是JNDI服务,它的代码由启动类去加载,但是JNDI的目的本来就是对资源的集中管理和查找,因此需要加载厂商部署在应用程序ClassPath下的JNDI接口提供者的代码。
上面说过,双亲委派模型其实是以破坏JAVA的多态性为代价的,因此,官方在此之上做了一个妥协,可以看到,Launcher的代码中有一句
// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
这句代码将AppClassLoader设定到了ClassLoader的上下文环境中,借此打通了双亲委派模型的从父到子的通道,当需要调用到应用的资源时候,可以调用上下文的ClassLoader来完成操作。
另外,从上面观察到,ClassLoader.loadClass代码中,在父启动器和BootstrapClassLoader都加载不到类之后,还有一段:
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();
}
再来看看ClassLoader.findClass,是一个protected的方法,只抛出一个ClassNotFoundException:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
这个findClass方法为我们自定义ClassLoader提供了一个打通上下层的机会,我们只要重写findClass方法,那么在就可以在上层ClassLoader都找不到类的情况下,按照我们自定义的逻辑进行类的加载。
更进一步,我们可以发现,ClassLoader.loadClass方法也是一个protected方法,所以,我们也可以自定义loadClass的逻辑,让双亲委派模型整个失效,不过后面这种方案可能会有比较多的坑需要踩。