类加载器
所谓类加载器, 就是用于加载java的class文件到java虚拟机中的组件, 它负责读取java字节码, 并转换成java.lang.Class类的一个实例, 使字节码.class文件得以运行. 一般类加载器负责根据一个指定的类找到对应的字节码, 然后根据这些字节码定义一个java类. 另外, 它还可以加载资源, 包括图像文件和配置文件
类加载器在实际使用中给我们带来的好处是: 它可以使java类动态地加载到jvm中并运行, 即可在程序运行时再加载类, 提供了很灵活的动态加载方式
在java中有如下已经定义好的类加载器:
- 启动类加载器(BootStrap ClassLoader): 加载对象是java核心库, 把一些核心的java类加载到jvm中, 这个类加载器使用原生C++代码实现, 并不是继承java.lang.ClassLoader, 它是所有其它类加载器的最终父类加载器, 负责加载%JAVA_HOME%/jre/lib目录下jvm指定的类库, 其实它是属于jvm整体的一部分, jvm一启动就将这些指定的类加载到内存中, 避免以后过多的I/O操作, 提高系统的运行效率, 该类加载器无法被java程序直接使用
- 扩展类加载器(Extension ClassLoader): 加载的对象为java的扩展库, 即加载%JAVA_HOME%/jre/lib/ext里面的类, 这个类由启动类加载器加载, 但因为启动类加载器并非java代码实现, 已经脱离了java体系, 所以如果尝试调用扩展类加载器的getParent()方法获取父类加载器会得到null的返回, 然而它的父类加载器确实是BootStrap ClassLoader
- 应用程序类加载器(Application ClassLoader): 也叫作系统类加载器(System ClassLoader), 它负责加载用户类路径(classpath)指定的类库, 如果程序没有自定义类加载器, 就默认使用应用程序类加载器, 它也由启动类加载器加载, 但是它的父类加载器是Extension ClassLoader, 如果要使用这个类加载器, 可以通过ClassLoader中提供的静态方法获取: ClassLoader.getSystemClassLoader()
public static void main(String[] args) {
// java程序启动的时候, 可以通过-D参数配置当前的这个java进程的系统类加载器. 但是一般不会去配置该参数, 默认的就是AppClassLoader
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
// sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(systemClassLoader);
// sun.misc.Launcher$ExtClassLoader@1b6d3586
System.out.println(systemClassLoader.getParent());
// null: 因为启动类加载器并非java代码实现, 已经脱离了java体系, 所以如果尝试调用扩展类加载器的getParent()方法获取父类加载器会得到null的返回
System.out.println(systemClassLoader.getParent().getParent());
/**
* idea中运行main方法的时候, 后面就会带上classpath, 可以看到有/jre/lib下的, 由BootStrap类加载器加载
* 有/jre/lib/ext下的, 由ExtClassLoader加载
* 有自己项目/targer/classes下的, 由AppClassLoader加载
* -classpath
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\charsets.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\deploy.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\access-bridge-64.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\cldrdata.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\dnsns.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\jaccess.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\jfxrt.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\localedata.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\nashorn.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\sunec.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\sunjce_provider.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\sunmscapi.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\sunpkcs11.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\zipfs.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\javaws.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\jce.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\jfr.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\jfxswt.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\jsse.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\management-agent.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\plugin.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\resources.jar;
* F:\java\jdk1.8\jdk1.8.0_291\jre\lib\rt.jar;
* F:\code\tomcattest\target\classes com.darkness.comcattest.classloader.ParentaDelegationTest
*/
}
如果使用ExtClassLoader加载classpath下的类会怎样?
public static void main(String[] args) throws Exception {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
// 使用ExtClassLoader加载不了classpath下的类
Class<?> aClass = systemClassLoader.getParent().loadClass("com.darkness.comcattest.classloader.Test");
System.out.println(aClass.getClassLoader());
}
报错如下
Exception in thread "main" java.lang.ClassNotFoundException: com.darkness.comcattest.classloader.Test
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at com.darkness.comcattest.classloader.TestClassLoader.main(TestClassLoader.java:12)
classpath下的类必须使用AppClassLoader加载
public static void main(String[] args) throws Exception {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Class<?> aClass = systemClassLoader.loadClass("com.darkness.comcattest.classloader.Test");
System.out.println(aClass.getClassLoader());
}
输出如下
sun.misc.Launcher$AppClassLoader@18b4aac2
为什么第一种会报错?就需要看看java中类加载器是如何执行loadClass方法的
双亲委派机制
java中类加载是使用双亲委派机制, 说简单点, 就是父类加载器能加载的, 就不会给子类加载器加载.
loadClass方法执行逻辑
点开loadClass方法, 发现跳出了3个选择
所以只能先看看AppClassLoader中的loadClass(String str), 发现没有, 看看AppClassLoader的类继承关系
可以看出AppClassLoader是继承自如上3个ClassLoader的, 所以唯一可能就是调用了ClassLoader的loadClass方法. 代码如下
类名: java.lang.ClassLoader
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
发现调用的其实是loadClass(String name, Boolean resolve), 但是这个方法会走到哪里呢? 实际上还是走到了子类AppClassLoader中的方法中去. 这是一个模板方法设计模式, 我们看看AppClassLoader的loadClass方法是什么样吧
类名: sun.misc.Launcher.AppClassLoader
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
// 46是., var1就是传过来的类名的全限定名, 如此截取可以得到SimpleClassName
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
// 如果传过来的类全限定名没问题, 就会看看是不是开启了安全管理器
// 这个不管, 一般是null, 不会进入下面这个if
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
// ucp是UrlClassPath的实例, 里面有类的加载路径, 但是这个方法一般是会返回false的, 直接进入下面的else分支
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
// 最终还是调用的父类的loadClass方法
return super.loadClass(var1, var2);
}
}
下面看看父类的loadClass方法是怎么实现的
类名: java.lang.ClassLoader
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 这个方法里面首先check一下name, 校验name通过后, 就会调用一个native方法, 看看在jvm中是否已经加载过这个类
// 这个方法中check的name仅仅只是校验格式正不正确(为什么莫名其妙在这里提这一嘴, 校验名字难道除了校验名字的格式之外, 还会校验其它的东西? 后面会讲到)
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 拿到parent, 父类加载器, 使用父类加载器去递归调用父类加载器的loadClass方法
if (parent != null) {
// parent不为空, 就是ExtClassLoader, ExtClassLoader的loadClass方法和AppClassLoader基本一样, 最终也还是会调到父类ClassLoader中的这个方法中来
c = parent.loadClass(name, false);
} else {
// parent为空, 就只能是BootStrap类加载器了, 调用顶级类加载器去加载类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果父类加载器加载的类为null, 也就是没加载到
if (c == null) {
long t1 = System.nanoTime();
// 调用自己的findClass方法去找类, 如果没找到, 由于这是一个递归的过程, 没找到之后又会归到子类加载器的代码中去findClass, 这就是双亲委派的原理
// 这里会真正的去加载磁盘中的class文件到内存中
c = findClass(name);
// 加载到Class对象之后, 做一些记录操作
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
// 链接指定的类
// 如果类 c 已经被链接,那么这个方法简单地返回。否则,将按照Java语言规范的“执行”一章中的描述链接该类
resolveClass(c);
}
// 最终返回加载到的c: Class<?>类型
return c;
}
}
可以看到里面最终真正加载磁盘.class文件的其实是findClass方法, 下面就看看findClass是怎么做的
类加载器加载磁盘中的类: findClass方法
我们通过看类的继承关系, 知道了AppClassLoader的父类有哪些: URLClassLoader, SecureClassLoader, ClassLoader, 发现在URLClassLoader中重写了findClass(String name)
类名: java.net.URLClassLoader
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
// 调用AccessController的一个静态本地方法doPrivileged
// 传入了一个PrivilegedExceptionAction接口类型的匿名内部类, 实现的run方法return了一个defineClass方法
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
别的我们没必要看, 我们看到传入的匿名内部类return了一个defineClass, 而这个返回正好又是Class<?>类型, 说明很可能读取磁盘.class文件的就是这个defineClass方法, 通过查找可以发现, 在ClassLoader中, 有这么一个defineClass方法.
类名: java.lang.ClassLoader
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError {
return defineClass(name, b, off, len, null);
}
说明defineClass并不是读取.class文件到磁盘的方法, 而是把输入流转成的字节数组格式进而构造成一个Class对象的方法. 真正从磁盘加载.class文件的代码肯定在调用defineClass之前. 所以我们可以尝试着自己继承一下ClassLoader类, 去读取一个磁盘中随便一个位置的Test.class, 实现一个自己的类加载器, 看看能不能获取成功一个Test类的Class对象. 由于要加载磁盘随便位置的类, 所以必须要打破双亲委派机制.
打破双亲委派
public class MyClassLoader extends ClassLoader {
private String name;
public MyClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
FileInputStream in = new FileInputStream("F:\myclassloader\Test.class");
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] readBytes = new byte[1024];
int len;
while ((len = in.read(readBytes)) != -1) {
out.write(readBytes, 0, len);
}
byte[] bytes = out.toByteArray();
return this.defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return findClass(name);
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader(MyClassLoader.class.getClassLoader(), "myClassLoader");
Class<?> aClass = myClassLoader.loadClass("com.darkness.comcattest.classloader.Test");
System.out.println(aClass.getClassLoader());
}
}
运行报错:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655)
at java.lang.ClassLoader.defineClass(ClassLoader.java:754)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at com.darkness.comcattest.classloader.MyClassLoader.findClass(MyClassLoader.java:32)
at com.darkness.comcattest.classloader.MyClassLoader.loadClass(MyClassLoader.java:42)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at com.darkness.comcattest.classloader.MyClassLoader.findClass(MyClassLoader.java:32)
at com.darkness.comcattest.classloader.MyClassLoader.loadClass(MyClassLoader.java:42)
at com.darkness.comcattest.classloader.MyClassLoader.main(MyClassLoader.java:47)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at com.darkness.comcattest.classloader.MyClassLoader.findClass(MyClassLoader.java:32)
at com.darkness.comcattest.classloader.MyClassLoader.loadClass(MyClassLoader.java:42)
at com.darkness.comcattest.classloader.MyClassLoader.main(MyClassLoader.java:47)
上面的代码为什么会报错? 通过异常堆栈可以看到, 一共调用了2次MyClassLoader.loadClass(MyClassLoader.java:42)和2次MyClassLoader.findClass(MyClassLoader.java:32) 通过debug调试可以看到, 之所以调用2次, 是因为第一次进去name确实是com.darkness.comcattest.classloader.Test, 但是加载的时候发现, 它还有一个父类Object, 所以转头就去加载name=java.lang.Object, 我们可以看到最终报错的地方是: at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655), 我们可以看看这个方法
类名: java.lang.ClassLoader
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
// 校验名字, 不能以java.开头
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
这正是前面提到的校验name, name是有要求的, 自定义的类加载器, 是不允许加载java.开头的类的(需要提一嘴, AppClassLoader和ExtClassLoader是可以加载java.开头的类的, 它们都是Lanucher类的受保护的静态内部类, 外部是无法访问的, 它们不属于自定义类加载器), 所以我们自定义的类加载器加载java.lang.Object的时候, 就会报错了, 抛一个安全异常SecurityException, 因为这样是不安全的 所以我们只需要改动loadClass方法, 让父类加载器去加载Object类
类名: com.darkness.comcattest.classloader.MyClassLoader
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass(name);
if (aClass != null)
return aClass;
return findClass(name);
}
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
报错是不报了, 结果为什么我加载的Test类的类加载器是AppClassLoader呢? 那我自定义的类加载器不是白干了吗? 实际上是因为双亲委派的原因, 虽然我class类路径是F:\myclassloader\Test.class, 但是我在项目中有Test类啊, 我开头就用AppClassLoader去加载类, 加载不到我才会用自定义的findClass去读取, 但是AppClassLoader是能加载到classpath下的Test类的, 所以这样打印就会是AppClassLoader, 但是如何解决呢? 很简单, 我让ExtClassLoader去加载Object类就行了, 代码如下
类名: com.darkness.comcattest.classloader.MyClassLoader
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> aClass = null;
try {
// 这里一定要try catch住, 因为使用ExtClassLoader是一定不可能加载到Test的, F:\myclassloader\Test.class它是肯定加载不到的, classpath下的那个Test它一样是加载不到, 这样就可以让我自定义的loadClass方法走到我自己的findClass方法去读取我F盘的Test类
aClass = ClassLoader.getSystemClassLoader().getParent().loadClass(name);
} catch (ClassNotFoundException e) {
}
if (aClass != null)
return aClass;
return findClass(name);
}
运行结果:
com.darkness.comcattest.classloader.MyClassLoader@1b6d3586
所谓打破双亲委派, 实际上就是绕过AppClassLoader, 让java自带的类加载器无法加载classpath下的类, catch住ClassNotFoundException后, 去执行我们自己的读取class文件的方法, 但是这样做的意义又是什么呢? 看下面一个例子:
类名: com.darkness.comcattest.classloader.MyClassLoader
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader1 = new MyClassLoader(MyClassLoader.class.getClassLoader(), "myClassLoader");
MyClassLoader myClassLoader2 = new MyClassLoader(MyClassLoader.class.getClassLoader(), "myClassLoader2");
Class<?> aClass1 = myClassLoader1.loadClass("com.darkness.comcattest.classloader.Test");
Class<?> aClass2 = myClassLoader2.loadClass("com.darkness.comcattest.classloader.Test");
System.out.println(aClass1 == aClass2);
}
运行结果:
false
上面我new了2个我自定义的类加载器, 去加载同一个类Test, 执行的同一套方法, F盘上的Test.class文件也是同一个, 但是加载到的Class对象却不是同一个. 这就说明: ClassLoader的对象不同, 加载同一个类的Class对象也一定不是同一个. 这区别就很明显了, 如果我们使用AppClassLoader, 让它去加载classpath下的类, 由于它是java自带的系统级的类加载器, 固然是单例的, 那么每次我们获取到的AppClassLoader对象都是同一个, 如果让它去加载同一个类, 每次得到的Class对象也都会是同一个(因为它会去检查是否加载过), 而AppClassLoader还是Lanucher类的一个受保护的static class(静态内部类), 外部也是无法去建立它的其它对象的. 所以我们只能自定义类加载器, 能够无限创建自定义类加载器对象去加载类, 并且要绕过AppClassLoader. tomcat也就是这么做的. 它会给每个Context创建一个自定义的类加载器去加载类(WebAppClassLoader), 目的就是防止版本冲突.