0 类加载器介绍
Java类加载器是Java运行时环境(Java Runtime Environment)的一部分,它负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。
Java中的类加载器是java.lang.ClassLoader
,它是一个抽象类。给定一个类名,ClassLoader就负责把这个类从特定的文件系统中加载到虚拟机中。
Class类有一个方法getClassLoader()
,每一个类的Class对象都可以调用这个方法来获取把这个类加载到虚拟机中的ClassLoader。
对于数组来说,它们不是由ClassLoader来创建,而是由Java运行时创建,数组的ClassLoader就是加载该数组元素类的ClassLoader。如果元素类型是基本类型,那么数组就没有ClassLoader。
ClassLoader采用的是代理模式来加载类,每一个ClassLoader实例都有一个父ClassLoader(并不是继承关系),当一个类加载器需要加载一个类的时候,它会首先传递这个类的信息到parent 类加载器,请求parent来加载,然后依次传递,直到该类被成功加载或者失败。如果失败了,那么就由最开始的那个类加载器来进行加载。在Java虚拟机中有一个内置的类加载器是bootstrap class loader
,它是没有parent的,但是可以作为所有ClassLoader实例的parent。这种加载方式也叫作双亲委派机制或者父委托机制。
通常来讲,类加载器都是加载本地的Class文件,但是它也可以加载其它来源的文件,比如从网络下载下来的。可以通过继承java.lang.ClassLoader
类的方式实现自己的类加载器,以满足一些特殊的需求而不需要完全了解Java虚拟机的类加载的细节。ClassLoader的一个方法defineClass
可以把一个字节数组转为Class实例。然后可以根据Class.newInstance()
方法来创建一个对象。被ClassLoader创建的类的方法或者构造方法可能还会引用其它的类,为了确定引用的类,虚拟机会调用最开始加载引用类的ClassLoader的loadClass
方法。
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
例如,想要自定义一个NetworkClassLoader,来加载从网络传来的Class类:
ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();
. . .
NetworkClassLoader必须重写findClass
方法,,然后定义一个方法来返回Class类的字节数组。当下载完毕,需要调用defineClass方法,示例如下:
class NetworkClassLoader extends ClassLoader {
String host;
int port;
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the connection
. . .
}
}
1 JVM中的ClassLoader
JVM中有3个默认的类加载器:
- 引导(Bootstrap)类加载器。用C/C++写的,在Java代码中无法获取到。主要是加载存储在
<JAVA_HOME>/jre/lib
目录下的核心Java库,对应的加载路径是sun.boot.class.path
。 - 扩展(Extensions)类加载器.用来加载
<JAVA_HOME>/jre/lib/e。t
目录下或者对应的加载路径java.ext.dirs
中指明的Java扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。该类由sun.misc.Launcher$ExtClassLoader
实现。 - Apps类加载器(也称系统类加载器)。根据 Java应用程序的类路径(java.class.path或CLASSPATH环境变量)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过
ClassLoader.getSystemClassLoader()
来获取它。该类由sun.misc.Launcher$AppClassLoader
实现,它的parent类加载器是ExtClassLoader。
下面通过一个示例来看一下:
package classLoader;
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoaderTest clt=new ClassLoaderTest();
ClassLoader cl=clt.getClass().getClassLoader();
System.out.println(cl);
System.out.println(cl.getParent());
System.out.println(cl.getParent().getParent());
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));
}
}
打印结果:
sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$ExtClassLoader@7852e922
null
sun.misc.Launcher$AppClassLoader@2a139a55
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/resources.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/rt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/sunrsasign.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/jsse.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/jce.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/charsets.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/jfr.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/classes
/Users/jason/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/ext:
/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
/works/EclipseWorkSpace/classLoader/bin
java.lang.ClassLoader
继承关系如下:
其中AppClassLoader和ExtClassLoader都是在Laucher中的内部类。而这个Laucher是JVM的入口。
注意: 并不是说子加载器继承自父加载器
2 ClassLoader源码分析
前面已经讲了,类的加载使用的是双亲委派机制。那我们启动一个Java应用程序,它的类加载顺序是从AppClassLoader委托ExtClassLoader,如果ExtClassLoader也找不到就会去委托Bootstrap类加载器加载。如果父加载器没有找到的话,再从子加载器中加载,加载到的类会被缓存起来,如果最终都没有找到这个类,就会报一个异常ClassNotFoundException
。
我们先看一下ClassLoader的构造方法,它有3个构造方法,但是其中有一个私有的:
//最终调用的还是这个私有的方法
private ClassLoader(Void unused, ClassLoader parent) {}
//有参构造,传递parent类加载器
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
// 无参构造,默认采用getSystemClassLoader()方法获取的ClassLoader作为parent类加载器
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
下面我们来看getSystemClassLoader()这个方法:
public static ClassLoader getSystemClassLoader() {
// 初始化系统类加载器
initSystemClassLoader();
if (scl == null) {
return null;
}
// 做一些安全方面的校验
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
initSystemClassLoader()方法如下:
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
//获取Launcher对象
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
//调用Launcher对象的getClassLoader()方法,这个获取的就是AppClassLoader,详细内容可以看下面对Launcher的分析
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;
}
}
下面我们就ClassLoader加载一个类的过程来进行一下分析。ClassLoader加载一个类,调用的方法是loadClass()方法
:
//通常外界是调用ClassLoader的这个loadClass方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//ClassLoader的默认加载方式,如果需要自定义ClassLoader最好不要重写这个方法。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 同步代码块
synchronized (getClassLoadingLock(name)) {
// findLoadedClass是一个native方法,如果已经加载过的类是会被缓存起来的,直接从缓存获取即可
Class<?> c = findLoadedClass(name);
if (c == null) {
// c为null,说明没有缓存,就需要初次加载
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果parent不为null就委托parent去加载
c = parent.loadClass(name, false);
} else {
// 如果parent为null就委托bootstrap class loader去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果非空的父加载器找不到类会抛出异常,在这里try-catch住了
}
if (c == null) {
// 如果父加载器和bootstrap加载器都没有找到,就会调用ClassLoader实例自身的findClass()方法。
// 其方法体是抛出一个ClassNotFoundException异常,
// 所以继承ClassLoader的子类加载器需要重写这个findClass()方法
long t1 = System.nanoTime();
c = findClass(name);
...
}
}
if (resolve) {
// 使Classloader链接指定的类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。
// 否则,这个类将被按照 Java™规范中的Execution描述进行链接
resolveClass(c);
}
return c;
}
}
需要注意的是第一个参数,name表示的是二进制名称(Binary name),例如:
"java.lang.String"
"javax.swing.JSpinner$DefaultEditor"
"java.security.KeyStore$Builder$FileBuilder$1"
"java.net.URLClassLoader$3$1"
需要指出:类加载过程是同步的
简单总结一下类加载器的工作过程:
- 如果当前加载的类已经加载过,直接从缓存获取。
- 之前没有加载过,如果该ClassLoader对象的parent不为null就委托父加载器加载,父加载器会重新开始走第1步。如果parent为null,那么就采用根加载器bootstrap class loader进行加载。
- 如果之前还是没有成功加载类,那么就会调用当前ClassLoader的findClass()方法去加载。
类加载器采用双亲委派机制的好处:
- 加载的类会被缓存起来,下次加载就快了。
- 安全,比如我们自定义一个与系统String包名类型一致的类,然后想要把这个String类加载进来干点坏事的话实际上是做不到的,由于父委托机制,真正的String类会被bootstrap class loader 加载(String类是存放在bootstrap class loader 负责加载的区域),就不会再调用我们这个假的String类。实际上,如果你自定义了一个类加载器并且重写了loadClass的逻辑,最终还是不能加载假的String类,因为ClassLoader有一个preDefineClass方法,该方法会检测类的包名,如果是'java'开头就会抛出一个SecurityException异常。
那么AppClassLoader和ExtClassLoader是什么时候初始化的呢?下面我们再去看一下Launcher的部分源码:
# sun.misc.Launcher
//构造方法
public Launcher() {
ClassLoader extcl;
try {
// 创建ExtClassLoader对象
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
//创建AppClassLoader对象loader,这个loader就是上面讲到的,Launcher的getClassLoader()方法返回的对象。
//AppClassLoader.getAppClassLoader()方法的参数为extcl,实际上就是把ExtClassLoader对象当作其父加载器
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
// 设置上下文的类加载器,也就是AppClassLoader对象。
Thread.currentThread().setContextClassLoader(loader);
// Finally, install a security manager if requested
String s = System.getProperty("java.security.manager");
if (s != null) {
SecurityManager sm = null;
if ("".equals(s) || "default".equals(s)) {
sm = new java.lang.SecurityManager();
} else {
try {
sm = (SecurityManager)loader.loadClass(s).newInstance();
} catch (IllegalAccessException e) {
} catch (InstantiationException e) {
} catch (ClassNotFoundException e) {
} catch (ClassCastException e) {
}
}
if (sm != null) {
System.setSecurityManager(sm);
} else {
throw new InternalError(
"Could not create SecurityManager: " + s);
}
}
}
关于ExtClassLoader和AppClassLoader的源码我们这里就不做多余的介绍了,感兴趣的可以去看一下Launcher这个类,ExtClassLoader和AppClassLoader都是其静态内部类。
3 自定义类加载器
我们完全可以通过自定义类加载器来加载我们想要加载的类,这个类可能来源于网络,也可能来源于文件系统。
从前面的分析我们知道,加载一个类的过程调用的是ClassLoader的loadClass()方法。自定义类加载器通常不要重写loadClass()方法的逻辑。在这个方法内部,如果所有的父加载器都没有成功加载,就会调用ClassLoader对象自身的findClass()方法,自定义类加载器可以实现这个findClass()方法即可。
还有一个关键的方法就是调用ClassLoader对象的defineClass()方法,这样就可以创建一个Class对象了。
public class CustomClassLoader extends ClassLoader {
private String dirPath;
public CustomClassLoader(String dirPath) {
this.dirPath = dirPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
//注意这个name一定要是二进制名称,如'java.lang.String'
//根据类的二进制名称,获得该class文件的字节码数组
byte[] classData = getClassDataBytes(name);
if (classData == null) {
throw new ClassNotFoundException();
}
//调用define()方法将class的字节码数组转换成Class类的实例
clazz = defineClass(name, classData, 0, classData.length);
return clazz;
}
private byte[] getClassDataBytes(String name) {
FileInputStream is = null;
try {
String path = classNameToPath(name);
is = new FileInputStream(path);
byte[] buff = new byte[1024];
int len;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = is.read(buff)) != -1) {
baos.write(buff, 0, len);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
private String classNameToPath(String name) {
return dirPath + "/" + name.replace(".", "/") + ".class";
}
}
自定义的类加载器已经写好了,下面我们来演示一下如何加载一个类,首先我们编写一个java类:
package com.sososeen09;
class Test {
public Test() {
}
public void print() {
System.out.println("this is Test Class");
}
}
根据javac
命令把java文件编译为对应的class文件。这个Test.class文件的二进制名称就是com.sososeen09.Test
。
public class ClassLoaderTest {
public static void main(String[] args) {
String srcPath = "/test/bin";
CustomClassLoader customClassLoader = new CustomClassLoader(srcPath);
String classname = "com.sososeen09.Test";
try {
Class clazz = customClassLoader.loadClass(classname);
System.out.println("loaded class: " + clazz);
System.out.println("class loader: " + clazz.getClassLoader());
System.out.println("class loader parent: " + clazz.getClassLoader().getParent());
Constructor constructor = clazz.getConstructor();
constructor.setAccessible(true);
Object o = constructor.newInstance();
Method print = clazz.getDeclaredMethod("print");
print.setAccessible(true);
print.invoke(o);
} catch (Exception e) {
e.printStackTrace();
}
运行一下可以查看打印结果:
loaded class: class com.sososeen09.Test
class loader: com.sososeen09.javamodule.classloaders.CustomClassLoader@14ae5a5
class loader parent: sun.misc.Launcher$AppClassLoader@18b4aac2
this is Test Class
可以看到我们自定义的类加载器已经成功的把一个文件系统中的class加载了。
需要注意:我们是把二进制文件前面的包名转为路径了,所以我们传递的srcPath是"/test/bin"
,那么实际上class文件存放路径应该是"/test/bin/com/sososeen09/"
。
4 扩展知识点
在Java中,类的加载时按需加载,也就是需要的时候才会把class文件加载到内存中。可以分为隐式加载和显示加载。
- 隐式加载:由当new一个Java对象,或者调用类的静态方法或者使用静态成员变量的时候,会加载当前的Class。
- 显示加载,显示的调用Class.forName()方法,或者调用ClassLoader的loadClass()方法。
参考文章