作者:动力节点
链接:https://zhuanlan.zhihu.com/p/68089617
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
什么是类加载
- 类加载指的是将类Class文件读入内存,并为之创建一个java.lang.Class对象,class文件被载入到了内存之后,才能被其它class所引用
- jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载
- java.类加载器是jre的一部分,负责动态加载java类到java虚拟机的内存
- 类的唯一性由类加载器和类共同决定
还了解到系统的三种类加载器
java加载器
- AppClassLoader : 也称为SystemAppClass 加载当前应用的classpath的所有类。
- ExtClassLoader : 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录
- BoostrapClassLoader : 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中
瞄一眼源码,在Launcher类中
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
// 创建ExtClassLoader
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
//创建AppClassLoader
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);
}
}
public ClassLoader getClassLoader() {
return this.loader;
}
public static URLClassPath getBootstrapClassPath() {
return Launcher.BootClassPathHolder.bcp;
}
//AppClassLoader
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
}
//ExtClassLoader
static class ExtClassLoader extends URLClassLoader {
private static volatile Launcher.ExtClassLoader instance;
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
}
//创建ExtClassLoader
private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {}
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
这段源码有以下几点
- Launcher类在构造函数初始化了 ExtClassLoader和AppClassLoader并设置AppClassLoader为线程上下文类加载器
- 代码里面没有告诉我们BoostrapClassLoader从哪里来的,但却为其指定了要加载class文件的路径sun.boot.class.path
- BoostrapClassLoader是由c++编写的,内嵌在jvm中,所以不能显示的看到他的存在【这个不是从源码中得到】
验证
类加载器的父子关系
public class Test {
public static void main(String[] args) {
System.out.println(Test.class.getClassLoader());
System.out.println(Test.class.getClassLoader().getParent());
System.out.println(Test.class.getClassLoader().getParent().getParent());
}
}
段代码我们可以看到类加载器的父子关系, APPClassLoader->ExtClassLoader->BoostrapClassLoader
, 但是 BoostrapClassLoader
无法显示的获取到,只能看到是个 null
。
源码中的路径到底加载哪些目录
·sun.boot.class.path
public static void main(String[] args) {
String property = System.getProperty("sun.boot.class.path");//BoostrapClassLoader
String[] split = property.split(";");
Arrays.asList(split).forEach(s -> System.out.println(s));
}
可以看到是jre/lib
目录下一些核心jar
- java.ext.dirs
public static void main(String[] args) {
String property = System.getProperty("java.ext.dirs");//ExtClassLoader
String[] split = property.split(";");
Arrays.asList(split).forEach(s -> System.out.println(s));
}
·java.class.path
public static void main(String[] args) {
String property = System.getProperty("java.class.path");//AppClassLoader
String[] split = property.split(";");
Arrays.asList(split).forEach(s -> System.out.println(s));
}
可以看到,各个加载器加载的对应路径和前面的介绍是吻合的
前面提到过
类加载的双亲委托机制
这里直接给一张图
如果看不太懂可以看下以下解释
- 一个class文件发送请求加载,会先找到自定义的类加载器,当然这里没画出来
- APPClassLoader得到加载器请求后,向上委托交给ExtClassLoader,ExtClassLoader同理会交给BoostrapClassLoader,这是向上委托方向
- 最终到达BoostrapClassLoader,会先在缓存中找,没有就尝试在自己能加载的路径去加载,找不到就交给ExtClassLoader,同理一直到用户自定义的ClassLoader,这就是向下查找方向
- 前面说的类的唯一性由类和类加载器共同决定, 这样保证了确保了类的唯一性
弄清楚这些,我们可以开始验证自定义的类加载器是否可以加载我们自定义的这个System类了
自定义类加载器
新建一个MyClassLoader继承ClassLoader,并重写loadclass方法
package org.apder;
import java.io.InputStream;
/**
* @Description 自定义ClassLoader
* @Author apdoer
* @Date 2019/5/28 15:27
* @Version 1.0
*/
public class MyClassLoader extends ClassLoader{
public MyClassLoader(){
super(null);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String className = null;
if (name != null && !"".equals(name)){
if (name.startsWith("java.lang")){
className = new StringBuilder("/").append(name.replace('.','/')).append(".class").toString();
}else {
className = new StringBuffer(name.substring(name.lastIndexOf('.')+1)).append(".class").toString();
}
System.out.println(className);
InputStream is = getClass().getResourceAsStream(className);
System.out.println(is);
if (is == null) return super.loadClass(name);
byte[] bytes = new byte[is.available()];
is.read(bytes);
return defineClass(name,bytes,0,bytes.length);
}
}catch (Exception e){
e.printStackTrace();
throw new ClassNotFoundException();
}
return super.loadClass(name);
}
}
这里的代码很容易看懂,就不赘述了,
- 测试一下,由于System需要用于打印获取结果,这里就用同属lang包的Long类
public class Long {
public void testClassLoader(){
System.out.println("自定义Long类被"+Long.class.getClassLoader()+"加载了");
}
public static void main(String[] args) {
System.out.println("Long");
}
}
运行自定义Long类中main方法 报错如下
原因很简单,这个自定义的Long类申请加载后,会被委托到BoostrapClassLoader,BoostrapClassLoader会在向下查找的过程中找到rt.jar中的java.lang.Long类并加载,执行main方法时,找不到main方法,所以报找不到main方法
我们再定义一个自定义的java.lang.MyLong类,执行main方法,报错如下
很明显的堆栈信息,禁止使用的包名java.lang
,我们点进去preDefineClass
看看
/* Determine protection domain, and check that:
- not define java.* class,
- signer of this class matches signers for the rest of the classes in
package.
*/
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.*"
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;
}
可以看到,当如果类的全路径名以
java.
开头时,就会报错,看到这里,开头的答案你是否有了结果呢,
我们梳理一下过程,如果用自定义的类加载器加载我们自定义的类
- 会调用自定义类加载器的
loadClass
方法 - 而我们自定义的classLoader必须继承ClassLoader,loadClass方法会调用父类的defineClass方法
- 而父类的这个defineClass是一个final方法,无法被重写
- 所以自定义的classLoader是无论如何也不可能加载到以
java.
开头的类的
到这里,最开始的问题已经有了答案
思考
如果我把MyLong打成jar放到BoostrapClassLoader的加载路径呢,让BoostrapclassLoader去加载,具体操作如下,在jdk的jre目录下创建classes目录,然后把MyLong.jar复制进去,再通过vmOptions追加这个classes目录到BoostrapClassLoader加载
可以看到仍然加载不了,如果能加载在 下面是会有load信息的,如果不是java.lang.Long,是可以跨过APPClassLoader和ExtClassLoader来让boostraPClassloader来加载的,这里就不演示了,操作很简单
下面是vm参数
-Xbootclasspath/a:c:\classloader.jar -verbose
由一个面试题引起的类加载器思考,既然已经写到这里,干脆把线程上下文类加载器也一并学习了
拓展线程上下文类加载器
为什么不和前面三种类加载器放在一起说呢,这个线程上下文类加载器只是一个概念,是一个成员变量,而前三种是确切存在的,是一个类,我们来看一下Thread
的源码
public
class Thread implements Runnable {
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
@CallerSensitive
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
}
特点
- 线程上下文类加载器是一个成员变量,可以通过相应的方法来设置和获取
- 每个线程都有一个线程类加载器,默认是
AppClassLoader
- 子线程默认使用父线程的
ClassLoader
,除非子线程通过上面的setContextClassLoader
来设置
测试
针对以上两点简单测试一下
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(()->{});
System.out.println(thread.getContextClassLoader());
thread.setContextClassLoader(Test.class.getClassLoader().getParent());
System.out.println(thread.getContextClassLoader());
}
}
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(()->{
});
Thread.currentThread().setContextClassLoader(Test.class.getClassLoader().getParent());
thread.setContextClassLoader(Test.class.getClassLoader().getParent());
System.out.println(thread.getContextClassLoader());
}
}