什么是双亲委派模型,我个人的理解就是:
类加载器加载一个类的时候,不会马上去加载,而是先找他爹(父加载器),他爹要是还有爹就继续向上找爹,直到爹没办法加载(自己的加载范围内找不到类,ClassNotFound),才会由子加载器加载。
或者说的简单一点,在“能够加载到类”的这个范围内,找最“爹”的那个加载器加载。
什么是类加载器:就是根据类全名定位,把class文件加载到JVM转成class对象。
JVM里,类加载器的关系是这样的:(转大佬图)
一般来讲,我们自己写的java,编译成class文件,应该都是通过Application ClassLoader来加载的,这个加载器就是加载classpath下的class文件。
这个Application ClassLoader上面还有俩爹,加载的就是java_home下的类了。这里摘抄一下大佬的说法:
启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
为什么要用双亲委派模型,这要从一道面试题说起。
大佬发来的一道面试题:
面试官:在项目中自定义java.lang.String类并使用它,会怎样?
于是我自己实践了一下:
由于双亲委派模型,这里父加载器已经加载了jdk自带的java.lang.String,我们写的这个String类,虽然全名一样,但是不会被加载,所以运行的时候,加载的还是jdk的String类,所以没有main方法。
大佬举了个例子:比如有个黑客,自己写了个java.lang.String类,里面加入了一些恶意代码,双亲委派机制就可以确保这个类不被加载,从而保证安全。
ClassLoader使用loadClass方法来进行类加载,jdk源码如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
基本过程是:先找已经加载的类,如果没有被加载过,则找父类加载器进行加载,没有父类加载器,则找最根部的bootstrap加载器加载。如果依然没有加载成功,则调用自己的findClass方法进行加载。
所以如果写一个自定义加载器的话,findClass方法需要被重写。
依旧转大佬图,比较清晰地阐述了loadClass方法的过程:
自己定义一个类加载器:
首先自己写一个类,把编译的class文件单独剪切出来,否则在classpath下就会去走Application ClassLoader,不会走自定义的加载器了。
package com.xx.hello;
public class Hello {
public void out() {
System.out.println("类加载器为:"+getClass().getClassLoader().getClass());
}
}
然后是自定义的加载器代码:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
public class MyClassLoader extends ClassLoader {
private String path;
public MyClassLoader(String path) {
this.path = path;
}
private byte[] loadByte(String path) {
path = path.replace(".", "/");
try {
FileInputStream fileInputStream = new FileInputStream(
new File(this.path + "/"+path+".class"));
byte[] clazzData = new byte[fileInputStream.available()];
fileInputStream.read(clazzData);
return clazzData;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] clazzData = loadByte(name);
return super.defineClass(name , clazzData, 0, clazzData.length);
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("D:/zztest");
Class<?> clazz = myClassLoader.findClass("com.xx.hello.Hello");
Object o = clazz.newInstance();
Method m = clazz.getDeclaredMethod("out", null);
m.invoke(o, null);
// Class<?> clazz = myClassLoader.findClass("java.lang.String");
// Object o = clazz.newInstance();
// Method m = clazz.getDeclaredMethod("hacker", null);
// m.invoke(o, null);
}
它会去读我在D:/zztest这个路径下存放的class文件并加载,通过反射调用out方法,输出结果为:
下面那一块被我注释的代码,是我同样把自己刚才写的那个java.lang.String类也放到了D:/zztest这个路径下,运行代码后,会报异常:
Exception in thread “main” java.lang.SecurityException: Prohibited package name: java.lang
无效报名,查了一下,类全名以java开头的好像都有这样的设置,直接抛出异常,不能加载。这说明jdk在这点上防范的还不错。
最后摘抄一段大佬的话,为什么在tomcat当中舍弃了双亲委派模型:
因为tomcat要解决以下问题
1. 一个web容器可能需要部署多个应用,不同的应用可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份
2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,很浪费空间
默认的类加载器是能够实现的,因为他的职责就是保证唯一性
3. web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
和第一个问题类似
4. web容器要支持jsp的修改
但是jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。