Java 类加载器的功能
java.lang.ClassLoader
类的基本职责就是根据一个指定的类的名称(全限定名),找到或者生成其对应的字节码,然后从这些字节码中定义出一个Java 类对象,即java.lang.Class
类的一个实例。
JDK中提供的ClassLoader
1.Bootstrap ClassLoader
Bootstrp加载器是用C++语言(针对HotSpot JVM)写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib
路径下的jar包,-Xbootclasspath
参数指定的jar包以及%JAVA_HOME%/jre/classes
路径下的类。
-Xbootclasspath:bootclasspath
让jvm从指定的路径中加载bootclass,用来替换jdk的rt.jar。一般不会用到。-Xbootclasspath/a:path
被指定的文件追加到默认的bootstrap路径中。-Xbootclasspath/p:path
让jvm优先于默认的bootstrap去加载path中指定的class
而我们把jar包放在%JAVA_HOME%/jre/lib
JVM是不会被加载的。而只能使用-Xbootclasspath
参数指定jar包或者将编译生成的class文件放在classes目录下。
2.ExtClassLoader
Bootstrp ClassLoader 加载 Ext ClassLoader,并且将 Ext ClassLoader 的父加载器设置为Bootstrp loader。Ext ClassLoader 是用 Java 写的,具体来说就是sun.misc.Launcher$ExtClassLoader
,ExtClassLoader 主要加载%JAVA_HOME%/jre/lib/ext
路径下的jar
包以及所有classes子目录内的class文件以及java.ext.dirs
系统变量指定的路径中类库(java -Djava.ext.dirs=path)。而不能通过System.setProperty
指定。毕竟加载(针对ExtClassLoader以及AppClassLoader)是独立于JVM的一项操作。
如果您的程序没有指定该系统属性(-Djava.ext.dirs=path
)那么该加载器默认加载%JAVA_HOME%/jre/lib/ext
目录下的所有jar文件。但如果你手动指定系统属性且忘了把%JAVA_HOME%/jre/lib/ext
路径给加上,那么Ext ClassLoader不会去加载%JAVA_HOME%/jre/lib/ext
下面的jar文件,这意味着你将失去一些功能,例如java自带的加解密算法实现。
3.AppClassLoader
Bootstrp loader加载完Ext ClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为Ext ClassLoader。AppClassLoader也是用Java写成的,它的实现类是sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar包,它也是Java程序默认的类加载器。
双亲委托模型
Java中ClassLoader的加载采用了双亲委托机制,采用双亲委托机制加载类的时候采用如下的几个步骤:
- 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
- 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader。
- 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。
为什么要采用双亲委托模型?
理解这个问题,我们引入另外一个关于Classloader的概念“命名空间”,它是指要确定某一个类,需要类的全限定名以及加载此类的ClassLoader来共同确定。也就是说即使两个类的全限定名是相同的,但是因为不同的ClassLoader加载了此类,那么在JVM中它是不同的类。明白了命名空间以后,我们再来看看委托模型。采用了委托模型以后加大了不同的ClassLoader的交互能力,比如上面说的,我们JDK原生提供的类库,比如hashmap,linkedlist等等,这些类由bootstrp类加载器加载了以后,无论你程序中有多少个类加载器,那么这些类其实都是可以共享的,这样就避免了不同的类加载器加载了同样名字的不同类以后造成混乱。
下面通过jdk里的ClassLoader源码来验证一下查找过程:
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 {
//没有父加载器的话使用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.
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;
}
}
如何自定义ClassLoader
先来介绍几个核心的方法:
1.loadClass 方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
可以看到该公有方法loadClass(String name)
调用了 protected 方法loadClass(String name, boolean resolve)
,第二个参数指示是否进行链接操作。
我们可以覆盖该公有方法来实现自己的加载逻辑,以打破上面提到的双亲委托机制。
2.findClass 方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可以看到该方法的默认实现直接抛出异常。
一般的,为了遵循原有的双亲委托模型我们不去重写 loadClass 方法,而是重写 findClass 方法来实现自己的查找逻辑。你可以从磁盘读取,也可以从网络上获取 class 文件所对应的字节流。
说白了,findClass 方法就是类加载机制留给你的一个回调函数。
3.defineClass 方法
作用:把字节数组转换为 java.lang.Class 类对象
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}
从上面的代码我们看出此方法被定义为了final,这也就意味着此方法不能被Override,其实这也是jvm留给我们的唯一的入口,通过这个唯一的入口,jvm保证了类文件必须符合Java虚拟机规范规定的类的定义。此方法最后会调用native的方法来实现真正的类的加载工作。
基于文件系统的 ClassLoader
package cn.bjut.study;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* 自定义 ClassLoader
*/
public class MyClassLoader extends ClassLoader {
private String directory;
public MyClassLoader(String directory) {
this.directory = directory;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classLocation = getClassLocation(name);
// System.out.println(classLocation);
byte[] buf = getClass(classLocation);
if (buf == null) {
throw new ClassNotFoundException();
}
return defineClass(name, buf, 0, buf.length);
}
/**
* 获得类的绝对路径
*
* @param name 类的全限定名
* @return
*/
private String getClassLocation(String name) {
return directory + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
}
/**
* 获得类对应的字节流
*
* @param name
* @return
*/
private byte[] getClass(String name) {
byte[] buf = null;
File file = new File(name);
if (file.exists() && file.isFile()) {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
buf = new byte[fileInputStream.available()];
fileInputStream.read(buf);
} catch (IOException e) {
e.printStackTrace();
}
}
return buf;
}
}
基于网络的ClassLoader
package cn.bjut.study;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
/**
* 加载网络 class 的 ClassLoader
*/
public class NetworkClassLoader extends ClassLoader {
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] getClassData(String name) {
URL url = null;
try {
url = new URL(classNameToPath(name));
} catch (MalformedURLException e) {
e.printStackTrace();
}
if (url == null) {
return null;
}
try (InputStream is = url.openStream()) {
byte[] buff = new byte[is.available()];
is.read(buff);
return buff;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String name) {
return rootUrl + "/" + name.replace(".", "/") + ".class";
}
}
不遵循“双亲委托机制”的场景
上面说了双亲委托机制主要是为了实现不同的ClassLoader之间加载的类的交互问题,被大家公用的类就交由父加载器去加载,但是Java中确实也存在父类加载器加载的类需要用到子加载器加载的类的情况。下面我们就来说说这种情况的发生。
Java中有一个SPI(Service Provider Interface)标准,使用了SPI的库,比如JDBC,JNDI等,我们都知道JDBC需要第三方提供的驱动才可以,而驱动的jar包是放在我们应用程序本身的classpath的,而jdbc 本身的api是jdk提供的一部分,它已经被bootstrp加载了,那第三方厂商提供的实现类怎么加载呢?这里面JAVA引入了线程上下文类加载的概念,线程类加载器默认会从父线程继承,如果没有指定的话,默认就是系统类加载器(AppClassLoader),这样的话当加载第三方驱动的时候,就可以通过线程的上下文类加载器来加载。
另外为了实现更灵活的类加载器OSGI以及一些Java app server也打破了双亲委托机制。
此处的loader即为 App ClassLoader
通过上面的描述,我们来思考下面一个问题:
假如我们自己写了一个java.lang.String
的类,我们是否可以替换掉JDK本身的类?
答案是否定的。我们不能实现。为什么呢?
即使我们打破双亲委托机制,自己自定义一个 ClassLoader 来加载自己写的java.lang.String
类,但是你会发现也不会加载成功,因为java.*开头的类,jvm的实现中已经保证了必须由bootstrp Classloader来加载。
wiki:
所谓全盘负责,即当一个ClassLoader加载一个Class的时候,这个Class所依赖和引用的所有Class也由这个ClassLoader负责载入,除非显示的使用另外一个ClassLoader载入。例如,由于java.lang.String是由Bootstrap ClassLoader载入的,那么String中引用的类如CharSequence等默认都是使用Bootstrap ClassLoader载入。
由不同的类装载器装载的类将被放在虚拟机内部的不同命名空间。命名空间由一系列唯一的名称组成,每一个被装载的类有一个名字。JAVA虚拟机为每一个类装载器维护一个名字空间。例如,一旦JAVA虚拟机将一个名为Volcano的类装入一个特定的命名空间,它就不能再装载名为Valcano的其他类到相同的命名空间了。可以把多个Valcano类装入一个JAVA虚拟机中,因为可以通过创建多个类装载器从而在一个JAVA应用程序中创建多个命名空间。