类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。
类加载器(class loader)
用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
类加载器双亲委派模型.png双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)
的关系来实现,而是都使用组合(Composition)
关系来复用父加载器的代码。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,如代码清单7-10所示,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 先检查类是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果父类加载器抛出ClassNotFoundException
//说明父类加载器无法完成加载请求
}
if (c == null) {
//在父类加载器无法加载的时候
//再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
return c;
}
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
启动类加载器(Bootstrap ClassLoader)
这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
String类就是rt.jar里面提供的,这个类我们经常用,下面我们看下String类的类加载器是什么。
public static void main(String[] args) {
ClassLoader cl = String.class.getClassLoader();
System.out.println(cl);
}
执行结果如下。
null
可知由于BootstrapClassloader对Java不可见,所以返回了null,我们也可以通过看某一个类的加载器是否为null来作为判断该类是不是使用BootstrapClassloader进行加载的依据。另外ExtClassLoader的父加载器返回的null,那是否说明ExtClassLoader的父加载器是BootstrapClassloader呢?
扩展类加载器(Extension ClassLoader)
这个加载器由sun.misc.Launcher $ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader)
这个类加载器由sun.misc.Launcher $App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
调用ClassLoader.getSystemClassLoader()
可以获取该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器,这点通过java.lang.ClassLoader的无参构造函数可以证明,代码如下
public abstract class ClassLoader {
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private static Void checkCreateClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
return null;
}
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
//从launcher中获取classloader
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
下面看一下Launcher中的实现:
public class Launcher {
private ClassLoader loader;
public Launcher() {
...
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
...
}
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
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);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
}
}
从AppClassLoader的构造函数中可以看到,通过获得classpath加载路径来创建应用程序加载器(看到这是不是终于知道环境变量里的java.class.path干嘛用的了)
另外我们写的含有main函数的类的加载就是使用该类加载器进行加载的,证明如下:
public static void main(String[] args) {
ClassLoader cl = App2.class.getClassLoader();
System.out.println(cl);
System.out.println(cl.getParent());
}
运行结果如下。
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
并且可以看出AppClassLoader的父加载器是ExtClassLoader
除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。
自定义类加载器
其中有如下三个比较重要的方法
方法 | 说明 |
---|---|
defineClass(String name, byte[] b, int off, int len) | 把字节数组 b中的内容转换成 Java 类,该字节数组可以看成是二进制流字节组成的文件,返回的结果是java.lang.Class 类的实例。这个方法被声明为 final的。 |
loadClass(String name) | 上文中已贴出源码,实现了双亲委派模型,调用findClass() 执行类加载动作,返回的是java.lang.Class 类的实例。 |
findClass(String name) | 通过传入的类全限定名name来获取对应的类,返回的是java.lang.Class 类的实例,该类没有提供具体的实现,开发者在自定义类加载器时需重用此方法,在实现此方法时需调用defineClass(String name, byte[] b, int off, int len) 方法。 |
我们可以容易地意识到自定义类加载器有以下两种方式:
-
采用双亲委派模型:继承ClassLoader类,只需重写其的
findClass(String name)
方法,而不需重写loadClass(String name)
方法。 -
破坏双亲委派模型:继承ClassLoader类,需要整个重写实现了双亲委派模型逻辑的
loadClass(String name)
方法。
下面我们来实现一个自定义类加载器,用来加载存储在文件系统上的 Java 字节代码。
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
类 FileSystemClassLoader的 findClass()
方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class
类的实例。
网友评论