我们知道在java程序中,需要将我们写的java文件编译为class文件才能被使用,一个java程序也是由许许多多的class文件组成。在程序运行的时候是需要将这些class文件加载到内存中才能被我们使用的,而且这些class文件也不是一次性的被加载进内存的,它是什么时候需要就什么时候加载,加载这些class文件到内存中就需要使用ClassLoader类加载器来完成。一般情况下我们不需要关系ClassLoader,但是在一些特殊的情况下我们不得不去了解它们,比如如果我们需要从网络下加载一个class文件,或者从一些特殊的路径下加载class文件,那么就需要我们对ClassLoader有一个清晰的认识才能知道该如何下手。
一、默认类加载器
JDK提供了3个默认的类加载器:Bootstrap ClassLoader、Ext ClassLoader和App ClassLoader。
- BootstrapClassLoader:也叫引导类加载器,是用C++写的,不是继承java中的ClassLoader,在jvm启动的时候就会初始化它,它主要加载
%JAVA_HOME%\jre\lib
和%JAVA_HOME%\jre\lib\classes
以及-Xbootclasspath
参数路径下的jar或者class。比如我们的String类等一些JDK提供的默认类都是由它来加载的。
我们可以使用如下代码来查看Boostrap加载器的加载路径:System.getProperty("sun.boot.class.path");
D:\DevTools\Java\jdk1.8.0_65\jre\lib\resources.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\rt.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\sunrsasign.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\jsse.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\jce.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\charsets.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\jfr.jar
D:\DevTools\Java\jdk1.8.0_65\jre\classes
ExtClassLoader:扩展类加器,它是用java代码实现的,主要加载%JAVA_HOME%\jre\lib\ext下的jar或者class。可以使用
System.getProperty("java.ext.dirs")
来查看它的加载路径D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext C:\Windows\Sun\Java\lib\ext
AppClassLoader:系统类加载器,主要加载应用程序中的类,可以使用
System.getPropertyntege("java.class.path")
来查看它的加载路径,使用不同的IDE,在不同的环境下该方法获取的路径可能会各不相同,比如我自己使用IntelliJ IDEA,执行结果如下:D:\DevTools\Java\jdk1.8.0_65\jre\lib\charsets.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\deploy.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\access-bridge-64.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\cldrdata.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\dnsns.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\jaccess.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\jfxrt.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\localedata.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\nashorn.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\sunec.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\sunjce_provider.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\sunmscapi.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\sunpkcs11.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\zipfs.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\javaws.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\jce.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\jfr.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\jfxswt.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\jsse.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\management-agent.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\plugin.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\resources.jar D:\DevTools\Java\jdk1.8.0_65\jre\lib\rt.jar D:\workspace\Test\out\production\Test D:\workspace\Test\test\libs\fastjson-1.2.37.jar D:\DevTools\JetBrains\IntelliJ IDEA 2017.2.2\lib\idea_rt.jar
这3个类加载器在jvm启动的时候会被初始化,其中Bootstrap ClassLoader会先初始化来加载核心类库。我们可以看看sun.misc.Launcher这个类的构造方法:
//sun.misc.Launcher
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//初始化ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//初始化AppClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//设置线程上下文classLoader为AppClassLoader
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
...
}
}
在Launcher对象初始化的时候就会初始化ExtClassLoader和AppClassLoader,这两个类都是Launcher的内部类。
static class ExtClassLoader extends URLClassLoader {
static class AppClassLoader extends URLClassLoader {
我们看看这几个默认类加载器的继承关系:
可以看到除了BootstrapClassLoader在java中看不到,其他的类加载器都是直接或者间接继承ClassLoader的。其实它们的继承关系不是我们要重点关注的,我们关注的它们之间的一个父类加载器的一个关系。我们看看ClassLoader的构造方法。
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
...
}
在构造ClassLoader对象的时候需要传递一个额外的类加载器来作为父类加载器,为什么要使用父类加载器与类的加载机制有关,我们后面再详说。
那么各个类加载器之间的关系是什么呢?
一般情况下我们自己定义的类加载器的父类加载器就是AppClassLoader,而AppClassLoader的父类加载器是ExtClassLoader,ExtClassLoader的父类加载器是BootstrapClassLoader。
再回到Launcher的构造方法中来:
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//设置线程上下文累加器为AppClassLoader
Thread.currentThread().setContextClassLoader(this.loader);
发现在初始化AppClassLoader的时候将ExtClassLoader对象作为参数传给了AppClassLoader,从这里也就看到了为什么说AppClassLoader的父类加载器是ExtClassLoader。
那为什么说一般我们自己定义的ClassLoader的父类加载器的是AppClassLoader呢,还是回到ClassLoader的构造方法中来:
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
...
return scl;
}
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;
scl = l.getClassLoader();
...
}
sclSet = true;
}
}
如果我们在构造ClassLodaer的时候不传递parent classloader的时候,会默认调用initSystemClassLoader
来初始化一个父类加载器,而这个默认的父类加载器就是Launcher.getLauncher().getClassLoader
,看看这里面是什么
public Launcher{
...
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
...
}
public ClassLoader getClassLoader() {
return this.loader;
}
发现这个默认的classloader就是AppClassLoader。
我们用代码来验证一下:
ClassLoader classLoader = Test.class.getClassLoader();
System.out.println(classLoader);
while (classLoader != null) {
classLoader = classLoader.getParent();
System.out.println(classLoader);
}
打印如下:
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@74a14482
null
返回null是因为BootstrapClassLoader是C++写的,java无法直接拿到。
二、双亲委托机制
如果需要加载一个class文件,我们需要调用ClassLoader.loadClass()方法。我们来看看这个方法里面具体做了什么
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// 1.判断要加载的类是否已经被加载了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//2-1.如果没有加载,并且父类加载器不为null,委托父类加载器去加载该类
c = parent.loadClass(name, false);
} else {
//2-2.如果父类加载器为null,让Bootstrap加载器去加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//3.如果父类加载器没有找到该类,那么就自己来加载
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的过程很简单,具体看注释就明白了。
双亲委托加载机制:当使用类加载器加载一个类的时候,该类加载器会先判断该类是否被加载了,如果已经加载了就直接返回;如果没有加载那么将在加载请求委托给父类加载器,同样父类加载器也会先判断该类是否被加载了,如果已经加载了就返回,如果没有那么父类加载器继续委托它的父类加载器去加载,这样最终该加载请求到达了BootstrapClassLoader,如果BootstrapClassLoader成功加载,那么就返回加载的class对象,如果它也没有找到,那么就按照刚刚委托向上的这个路线,向下返回给下一个类加载器去加载,依次最后返回到发送委托请求的类加载器那里,如果还是没有加载成功,那么就由它自己来加载,如果它也没有找到,就抛出ClassNotFoundException异常。
为什么ClassLoader要使用这种委托机制来实现类的加载呢?
判断一个类是不是相同除了判断它们是不是同一个class文件外,还需要判断这个class文件是不是被同一个ClassLoader加载。例如:我们自己的一个Test类,正常来说被AppClassLoader加载,如果我们使用自定义的类加载器来加载这个Test类,加载的class对象会是一样的吗?答案肯定是不一样的
public class Test {
public static void main(String[] args) {
Class<Launcher> launcherClass = Launcher.class;
try {
//获取Launcher中的loader对象,它是一个AppClassLoader
Field loaderField = launcherClass.getDeclaredField("loader");
Field launcherField = launcherClass.getDeclaredField("launcher");
launcherField.setAccessible(true);
//先获取到一个launcher对象,在Launcher类中是一个静态成员
Object launcher = launcherField.get(null);
loaderField.setAccessible(true);
//launcher中的appClassLoader对象
Object appClassLoader = loaderField.get(launcher);
//获取AppClassLoader.getAppClassLoader静态方法重新创建一个新的appClassLoader对象
Class<?> loaderClass = appClassLoader.getClass();
Method getAppClassLoaderMethod = loaderClass.getMethod("getAppClassLoader", ClassLoader.class);
getAppClassLoaderMethod.setAccessible(true);
//需要获取原来默认的appclassloader的parent,在构造新的appclassloader的时候需要一个父类加载器
Method getParentMethod = loaderClass.getMethod("getParent");
Object parentClassLoader = getParentMethod.invoke(appClassLoader);
//创建了一个新的appclassloader
Object newAppClassLoader = getAppClassLoaderMethod.invoke(null,parentClassLoader);
//获取loadClass方法
Method loadClassMethod = loaderClass.getMethod("loadClass", String.class, boolean.class);
loadClassMethod.setAccessible(true);
//使用新创建的AppClassLoader来加载当前的Test类,当前的Test没有写包名,所以直接写Test就可以了
Object testClass = loadClassMethod.invoke(newAppClassLoader, "Test", false);
System.out.println(testClass);
System.out.println(Test.class);
//判断两个Test.class是否相等
System.out.println(Test.class.equals(testClass));
//验证新加载的Test.class创建的对象能否强转为当前默认的Test.class
Test t = (Test) ((Class)testClass).newInstance();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
我们的验证思路如下:一般来说我们自己写的java类,都是由AppClassLoader来加载的,因此我再重新创建一个新的AppClassLoader对象来和默认的AppClassLoader来加载同一个类,比如这里我们都来加载同一个Test.class,因为AppClassLoader我们无法直接访问到,所以通过反射我们在Launcher中找到AppClassLoader,然后创建新的AppClassLoader对象,接着使用这个新的AppClassLoader对象来加载Test,然后和当前已经加载的Test.class进行比较。我们看打印结果:
class Test
class Test
false
Exception in thread "main" java.lang.ClassCastException: Test cannot be cast to Test
at Test.main(Test.java:52)
从结果可以看到使用两个类加载器加载进来的Test是不一样的,通过它们创建的对象也不能进行强转,否则抛出ClassCastException异常。
为了保证同一个java类在jvm中只存在一个Class,所以需要这种委托机制,是谁加载的某个类,不管使用哪个classloader,以后都让最开始的classloader来加载,如果同一个类被多个classloader加载,那不就出乱子了吗。正如上面的例子所以,同一个类被两个加载器加载进来,在使用的时候就报ClassCastException异常,这明显是不行的,所以在不强行使用恶意手段的时候,委托机制能够保证我们使用的都是同一个Class。
三、自定义ClassLoader
使用自定义的ClassLoader,我们可以加载文件系统中其他位置的class,甚至是网络上的class,为了保证class文件的安全性,我们还可以通过将class文件加密,在加载的过程的解密class文件来加载类,想怎么加载我们都是可以定制的。
一般来说,自定义ClassLoader的步骤很简单
- 继承ClassLoader类
- 覆写findClass方法
- 调用defineClass方法将字节数据转换为Class对象。
其中我们主要的工作就是在findClass中完成。
在ClassLoader中与加载类的几个核心方法有这么几个:loadClass()、findLoadedClass()、findClass()、defineClass(),其中loadClass()是我们加载类的入口,里面已经做了委托机制的处理,所以不建议去覆写loadClass()方法,findLoadedClass()是用来检查Class是否已经被加载了,所以是不需要我们重写的,findClass()就是ClassLoader真正去加载类的核心实现,我们需要来覆写该方法,defineClass()是用来将字节数组转为Class对象的方法,我们也不需要覆写此方法,我们在加载Class文件后需要调用此方法来转为Class对象。
现在我们来自定义个从D:\test
目录下加载class的自定义加载器
public class CustomClassLoader extends ClassLoader {
private String rootPath;
public CustomClassLoader(String rootPath) {
this.rootPath = rootPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = loadData(name);
if (data != null) {
return defineClass(name,data,0,data.length);
}
return null;
}
private byte[] loadData(String name) {
String classPath = generateClassName(name);
try {
InputStream in = new FileInputStream(new File(rootPath,classPath));
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf,0,len);
}
byte[] bytes = out.toByteArray();
in.close();
out.close();
return bytes;
} catch (IOException e) {
e.printStackTrace();
} catch (NoClassDefFoundError e) {
e.printStackTrace();
}
return null;
}
private String generateClassName(String name) {
return name.replaceAll("\\.","/")+".class";
}
}
测试类:
public class Demo {
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader("D:\\test");
try {
Class<?> loadTest = customClassLoader.loadClass("Test");
Object test = loadTest.newInstance();
Method print = loadTest.getMethod("print");
print.invoke(test);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
我们将Test.class放在我们自定义的目录下,该类中有一个print()
方法:
public void print(){
System.out.println("test class loader");
}
我们看打印结果:
通过自定义的ClassLoader,我们成功的加载了给定目录下的class。