什么是双亲委派?
双亲委派机制有4种类加载器为:
- 自定义(UserClassLoader)->应用/系统(App/SystemClassLoader)->扩展类(ExtClassLoader)->启动(BootstrapClassLoader)类加载器。
加载过程简述:
- 当一个类加载某个类.class(需要编译即javac Xx.java>>Xx.class)的时候,不会直接去加载,而是自定义会委托应用/系统,应用/系统会委托扩展,扩展会委托启动类加载器尝试去加载,如果启动类加载器不加载这个,就交给扩展,扩展不行就应用/系统,一层层的下去,然后最终加载到这个.class类。
加载的量:
- 不是一次性加载,而是按需动态加载,不然一次性加载内存可能会爆。
双亲委派优点?
1 安全,可避免用户自己编写的类动态替换Java的核心类,如java.lang.String
2 避免全限定命名的类重复加载(使用了findLoadClass()判断当前类是否已加载)
双亲委派的面试题
题目:可不可以自己写个String类(也是自定义的String为何没加载到?) - 阿里
不可以。因为在类加载中,会根据双亲委派机制去寻找当前java.lang.String是否已被加载。由于启动类加载器已在启动时候加载了所以不会再次加载,因此使用的String是已在java核心类库加载过的String,而不是新定义的String。
代码:
//这里为了测试,将其的包名改成与jdk的rt.jar中的java.lang.String一致。
package java.lang;
public class String {
static {
System.out.println(11);
}
private String(int i) {
System.out.println(i);
}
//注意:核心类库的String是没有main方法的,因为他找到的核心类库String, 所以报找不到main()方法错误.
public static void main(java.lang.String[] args) {
String s = new String(1);
System.out.println(s);
}
}
双亲委派的加载过程?
看完上面的, 我好奇它的每个类加载器有什么区别?代码里是怎么加载的?流程是怎么加载的?
1 每个类加载器有什么区别?
BootstrapClassLoader:
- 最顶层类加载器,加载java核心类库即%JRE_HOME%\lib下的rt.jar、charsets.jar和class等, 可通过java -Xbootclasspath/a:path(追加)、-Xbootclasspath/p:path(优先加载)、-Xbootclasspath:bootclasspath(替换jdk的rt.jar的加载)指定。
ExtClassLoader:
- 扩展类加载器,加载%JRE_HOME%\lib\ext的jar和class文件,可用-Djava.ext.dirs=./plugin:$JAVA_HOME/jre/lib/ext (":"是作为分隔符,代表./plugin和ext目录的都被扩展类加载器加载)指定。
App/SystemClassLoader:
- 应用/系统类加载器,加载当前classpath的所有类。
XxxClassLoader:
- 用户自定义的类加载器,默认使用双亲委派,委托上级来加载。
2 代码里是怎么加载的?
首先找到AppClassLoader、ExtClassLoader,这两个类均在Launcher.java内。从代码分析得如下:
---------------------------------------------------------------------------------------
...................
private static String bootClassPath = System.getProperty("sun.boot.class.path"); //35行
...................
String var0 = System.getProperty("java.ext.dirs"); //297行
...................
final String var1 = System.getProperty("java.class.path"); //164行
...................
---------------------------------------------------------------------------------------
然后测试以上三个,代码及输出如下:
System.out.println(System.getProperty("sun.boot.class.path")
.replaceAll("C:\\\\software\\\\programme\\\\Java\\\\jdk1.8.0\\\\jre", ""));//启动类加载路径
System.out.println(System.getProperty("java.ext.dirs"));//扩展类加载路径
System.out.println(System.getProperty("java.class.path"));//应用/系统类加载路径
注:结合1中每个类加载器的区别,就可以知道他们的加载目录究竟在哪里。
知道了他们的加载路径,接下来我们探讨下加载的顺序。
我们先看如下代码:
ClassLoader classLoader = Demo06.class.getClassLoader();
使用debug模式调试可见当前类加载器先找到的parent属性(上级类加载器)为Launcher下AppClassLoader,然后AppClassLoader的parent属性(上级类加载器)为ExtClassLoader, ExtClassLoader的parent为null。
那么他们的这个parent属性是如何赋值的?详情分析如下代码:
//首先AppClassLoader、ExtClassLoader都在Launcher下,其结构如下:
public class Launcher {
..................................
static class AppClassLoader extends URLClassLoader {.......}
..................................
static class ExtClassLoader extends URLClassLoader {.......}
..................................
}
此上面的类我们可以分析双亲委派是如何设置上级类加载器的,过程如下:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//第一步 获取Ext类加载器,Ext类加载器构造方法中初始化了其上级(这里下面代码讲解)
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//第二步 获取App类加载器,App类加载器会将var1(即Ext类加载器)传入,然后最终也是传入到其构造方法进行初始化其上级(这里下面代码讲解)
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//第三步 将获取到的App类加载器设置为当前线程类加载器
Thread.currentThread().setContextClassLoader(this.loader);
..........................
}
看完上面的代码,我们来细讲一下第一步、第二步究竟是怎样初始化parent,可先见其继承体系图如下:
注:从继承体系可见AppClassLoader、ExtClassLoader都继承了URLClassLoader、ClassLoader。而ClassLoader里面正定义了这个this.parent。
public abstract class ClassLoader {
...................
private final ClassLoader parent; //上级的类加载器
...................
protected ClassLoader() { //第一种初始化方法(无参),直接传入系统类加载器
this(checkCreateClassLoader(), getSystemClassLoader());
}
...................
//第二种有参,传入对应的parent,比如AppClassLoader传入的parent是ExtClassLoader实例,ExtClassLoader传入的是null。
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
...................
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent; //然后无论是哪种,最后都会传入到这个构造方法,然后赋值给this.parent
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains = Collections.synchronizedSet(new HashSet());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
}
"如何设置上级类加载器"分析总结:在launcher中app、ext类加载器已经初始化对应的构造方法,然后其对应的构造方法都会调用super(parent)然后分别将ext、null传入最终传到ClassLoader的构造方法中的this.parent = parent。其传递给过程取ExtClassLoader作为示例如下:
//1 Launcher下的ExtClassLoader
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
//2 URLClassLoader下
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
super(parent);
...............
}
//3 SecureClassLoader下
protected SecureClassLoader(ClassLoader parent) {
super(parent);
........................
}
//4 ClassLoader下
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent; //然后无论是哪种,最后都会传入到这个构造方法,然后赋值给this.parent,然后设置好了上级类加载器
}
上面我们分析完了每个上级类加载器是怎么拿到的,接下来我们探讨下类加载器间是如何进行双亲委托的?
由图分析得:双亲委托机制采用的是"向上委托,向下查找",其步骤如下:
第一步(向上委托) 当前类加载对.class进行加载,先会找到上级类加载器AppClassLoader,然后去缓存查是否有已加载的类,如果没有则去上级ExtClassLoader缓存查找是否有已加载的类,如果没有则再往上Bootstrap缓存找是否有已加载的类,如果没有就会进入第二步,反之上面任何一步缓存查找有的话,都会直接返回缓存里加载了的.class,而不会继续往上级找。
第二步(向下查找) 第一步缓存找不到时就会进入第二步。此时已经到了Bootstrap,Bootstrap会先到其对应的加载目录(sun.mic.boot.class路径)去看看当前有没这个类加载,如果有就加载返回返回;没有则往下级ExtClassLoader的对应加载目录(java.ext.dirs路径)找,有就加载返回,无就往下走;走到AppClassLoader然后去其对应加载目录(java.class.path路径)加载,有就加载,没有则让子类找,如果还失败就抛异常,然后调用当前ClassLoader.findClass()方法加载。
注:findClass是子类实现的,所以是用来自定义类加载器的。
其中这个加载过程涉及到了几个重要方法:loadClass、findLoadClass、findClass、defindClass。
//1 loadClass分析步骤(ClassLoader.java中):
//- finadLoadClass检查当前class是否被加载过;
//- 执行父加载器loadClass,如果没加载到会一直loadClass到Bootstrap ClassLoader,此时parent=null;
//- 如果向上委托没加载成功就使用findClass向下查找;
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) { //loadClass加了synchronized,是同步的
// 检查当前全限定类名(截图如上)下的包是否已被加载过,最终调用本地方法中(native final Class> findLoadedClass0(String name))
Class> c = findLoadedClass(name);
//如果当前没被加载过,就重新加载一次
if (c == null) {
long t0 = System.nanoTime();
try {
//!!重点!!看当前有没上级类加载器,一般是AppClassLoader、ExtClassLoader,
//而且这个loadClass会调用上级的loadClass,一直调用到parent=null的loadClass()
if (parent != null) {
c = parent.loadClass(name, false);
} else {//!!重点!!如果当前parent为null,那应该就是Bootstrap classLoader(因为这个是底层是C++,所以不直接调)
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
long t1 = System.nanoTime();
//上面两个重点的父类加载器没找到,则调用findClass
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) {//为true调用resolveClass
resolveClass(c);
}
return c;
}
}
上面解析了系统自带的几个类加载器如Ext、bootstrap是怎么加载的,
但是为什么还需要自定义类加载器??
1 从非标准来源加载代码:由于系统提供的类加载器均加载的是指定目录,所以当我们需要加载非系统指定目录如C:/xx/xxxx.class时需要自定义类加载器、数据库、云端等。
2 加密:将编译后的代码加密,然后用自定义类加载器去先解密,然后再加载。
自定义类加载器过程如下(自定义默认parent为AppClassLoader):
1 继承ClassLoader抽象类
2 重写findClass()
3 重写的findClass()中调用defineClass()
其代码实现过程如下:
package com.lisam.test; //放在其他非规定位置时,加这个会报找不到
public class Test {
public void test(){
System.out.println("test成功");
}
}
// 1 继承ClassLoader抽象类
class MyClassLoader extends ClassLoader {
private String path;
MyClassLoader(String path) {
this.path = path;
}
// 2 重写findClass()
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
File file = new File(this.path);
try {
byte[] bytes = getClassBytes(file);
//3 defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
return this.defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
//这里反正当前文件的比特流即可
private byte[] getClassBytes(File file) throws IOException {
//使用字节流获取.class
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true) {
int i = fc.read(by);
if (i == 0 || i == -1)
break;
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
}
@Test
public void test01() throws Exception {
MyClassLoader mcl = new MyClassLoader("C:\\test\\Test.class");
Class> clazz = Class.forName("Test", true, mcl);
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("test", null);
method.invoke(obj, null);
System.out.println(obj);
System.out.println(obj.getClass().getClassLoader());//打印出我们的自定义类加载器
}
参考:
https://blog.csdn.net/u013206465/article/details/47170253
https://blog.csdn.net/huhui_cs/article/details/50344471
重点文章:https://blog.csdn.net/briblue/article/details/54973413
Java探针:http://www.cnblogs.com/aspirant/p/8796974.html
Java类加载机制:https://www.cnblogs.com/aspirant/p/7200523.html
双亲委派模式及优势:https://blog.csdn.net/weixin_38055381/article/details/80167881
如何自定义类加载器:https://blog.csdn.net/SEU_Calvin/article/details/52315125