简单理解一下双亲委派机制
看文章标题就知道是比较难理解的知识点,所以原理先行,让大家先有个大体认识。
双亲委派分成两部分是双亲和委派,委派就是委托的意思,这里的双亲是递进关系,举个简单的例子,这里的双亲不是爹地妈咪,而是爷爷跟你爹。
既然与类加载过程扯到一起了,那一定与类加载有关了。没错双亲委派机制简单说就是一个类需要加载,会先委托给它的父亲(它爹地)加载,父亲会委托它父亲(它爷爷)加载。大概是这样:
类加载过程
我们来看看下面这段代码是如何加载的:
public class CreateObjectTest {
public static void main(String[] args) {
A a = new A();
}
}
class A{
static {
System.out.println("静态成员在构造前面执行");
}
public A() {
System.out.println("构造方法在静态后面执行");
}
}
下图是上述代码的大体流程图,我们先来看下左边红框中的五个步骤:
- 程序是运行在内存里的,Java是高级语言不能直接调用内存,所以由C++程序调用内存创建虚拟机
- 启动类加载器不是由Java自身创建的,是由C++代码创建的
- C++初始化的类是Launcher,一说到类加载,必定提到双亲委派,所以初始化启动类加载器时,会顺带把扩展类加载器与应用程序加载器一起初始化
- 学习反射时我们知道一个类在JVM中只能有一个Class实例,三个类加载器只能有一个加载(我们自己写的大都是应用程序加载器加载的),每个类加载器的职责可以查看第一篇文章或百度
- 这个加载过程咋往下看
第5步骤的详细过程如下(反射的文章里有提到):
- 加载:**JVM除了固定加载的一些类(比如八大基本类型的字节码对象)外,其他类都是按需加载(**用到了才会加载,也就是new的时候),比如上述代码我们new了一个A对象,那么后面就会去加载这个类
- 验证:这个过程主要是来验证字节码文件的正确性(需要补补,应该是前面一些数字是一样的)
- 准备:这个过程是隐式初始化,这个时候变量都会被赋予一个初始值(默认值),比如int类型是0等等
- 解析:这一步做的是将符号引用替换成直接引用。符号引用大家就可以理解为那些变量名啊,方法名啊都是英文符号组成的,实际这些符号都会指向内存中的某个地址,这个地址就是直接引用
- 初始化:int a = 10;这一步的初始化就是显示初始化了,将a的值从第三步的0改成10
扩展: 类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的 引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的 对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
进一步理解双亲委派机制
这里主要以源码展开,看看双亲委派用代码是怎么实现的。
public Launcher() {
// 声明一个扩展类加载器
Launcher.ExtClassLoader var1;
try {
// 初始化扩展类加载器,指定null为父加载器(启动类加载器)
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);
}
// 设置上下文加载器,默认是应用程序类加载器
Thread.currentThread().setContextClassLoader(this.loader);
// 沙箱加载的一些类???
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
第一次看这个源码我产生了两个问题:
- 这两个加载器如何认爹的呢?
- 扩展类加载器与应用程序加载器的初始化顺序可以改变吗?
我们看看源码来解释第一个问题,至于第二个问题请大家看完第一个问题的解答后自己思考。
加载器如何建立与父加载器的关系
首先我观察了Launcher的类结构,它有4个内部类,其中包括扩展类加载器与应用程序加载器,从初始化Launcher的代码中可以看出创建两个类加载器的工作的由具体加载器的方法来实现的。我们来看看两个类加载器的继承体系
(AppClassLoader继承了URLClassLoader,idea不给画线我就自己来)几个类之间的关系是扩展类加载器,应用程序加载器 -> URLClassLoader -> SecureClassLoader-> ClassLoader。
扩展类加载器
先从扩展类加载器的加载过程说起,从Launcher的构造方法中可以看到扩展类加载器是通过Launcher的静态内部类ExtClassLoader的_getExtClassLoade_r方法创建的_。_我们先来看下这个方法的大概执行流程图(建议看完图自己去点一点,再往下看):
从图里可以看出,扩展类加载器的构造经过多次调用,最终会调用ClassLoader的构造方法,而该构造方法的第一行就是this.**parent **= parent;这一行代码就是子类加载器与父类建立关系的关键代码。关键在于这个parent参数是多少,往回找我们会发现ExtClassLoader的构造中传递的parent的值是null,也就是说扩展类加载器的父加载器被设置成null。
设置成null的原因是启动类加载器是由C++创建的,所以拿不到这个对象,故而扩展类加载器的父加载器定义为null。
应用程序加载器
应用程序加载器的加载过程与扩展类加载器基本一致,从Launcher的构造方法中可以看到扩展类加载器是通过Launcher的静态内部类AppClassLoader的_getAppClassLoader_方法创建的_。_我们先来看下这个方法的大概执行流程图(建议看完图自己去点一点,再往下看):
流程与扩展类加载器是一致的,最终都是调用ClassLoader的构造方法,需要注意的是AppClassLoader的getAppClassLoader方法是需要传入参数的。这个参数传的是扩展类加载器,根据调用过程,应用程序加载器的父加载器是扩展类加载器。
保姆级源码讲解:类加载如何实现双亲委派
实现类加载时的双亲委派的主要方法是是ClassLoader中的loadClasss方法,大家可以看下代码及注释,注释应该解释清楚了
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 {
// 扩展类执行的oadClass也是这个方法,它的父加载器初始化的时候是null所以最终类会由下方代码加载
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;
}
}
我们说双亲委派时,如果已经加载过一次的类就不会再加载而是直接返回,那么程序中是如何判断一个类是否加载了呢?
在类加载之前都会调用ClassLoader#findLoadedClass方法,这个方法如下回去判断类是否加载。这个方法的作用是如果此加载器已被 Java 虚拟机记录为具有该二进制名称的类的启动加载器,则返回具有给定二进制名称的类。否则返回 null。
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
private native final Class<?> findLoadedClass0(String name);
除了主要方法外还有个方法是URLClassLoader#findClass,我翻译了下这个方法的注释,大家可以跟着看下代码:从 URL 搜索路径中查找并加载具有指定名称的类。任何引用 JAR 文件的 URL 都会根据需要加载和打开,直到找到该类。
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
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;
}
自定义类加载器
自定义类加载器需要我们继承抽象类ClassLoader,重写loadClass方法,当然,其他方法你也可以重写,比如这里的findClass方法。
public class Cat {
public void play(String name) {
System.out.println(name + "在玩游戏!");
}
}
public class MyClassLoader extends ClassLoader {
// 加载字节码文件的路径
private String loadClassPath;
public MyClassLoader(String loadClassPath) {
this.loadClassPath = loadClassPath;
}
/**
* 字符串转成byte数组
*
* @param className 字节码名称
* @return
*/
public byte[] stringToByte(String className) throws IOException {
String classPath = loadClassPath + "/" + className.replaceAll("\\.", "/") + ".class";
FileInputStream inputStream = new FileInputStream(classPath);
int lenth = inputStream.available();
byte[] bytes = new byte[lenth];
inputStream.read(bytes);
inputStream.close();
return bytes;
}
@SneakyThrows
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = stringToByte(name);
return defineClass(name, data, 0, data.length);
}
// 复制的ClassLoader的方法,删除了双亲委派部分代码
@Override
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();
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;
}
}
// 使用自己的类加载器加载Cat类
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
MyClassLoader myClassLoader = new MyClassLoader("E:/电子书");
Class<?> clazz = myClassLoader.loadClass("com.evader.evaderjvm.jvm.Cat");
Object obj = clazz.newInstance();
Method method = clazz.getMethod("play", String.class);
method.invoke(obj, "旺财");
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
大家经常听到一个名词叫沙箱安全机制,这里我们就演示一下
从gif图中可以看到我用自定义加载器加载java.lang.Object类(这个字节码文件拿的就是rt.jar包中的),发现加载不了。像这种核心API是不可以随便加载的,如果可以随便加载就会被侵入,后果非常严重,现在对沙箱安全有进一步的理解了吧。
Tomcat:打破双亲委派机制
Tomcat是web容器,我们的项目通常都是部署在web容器上,但是容器在大多数情况下都是部署多个服务的,并且web容器也有自己依赖的库(Tomcat也是一个应用程序)。如果Tomcat不打破双亲委派机制就产生一些问题:
- 多个服务不同版本,但是类只会被加载一次(由于类加载时只认包名与类名)
- Tomcat本身也有自己的依赖库,应该与运行在tomcat上的程序所依赖的库相互独立,实际跟第一条差不多
实际Tomcat也有自己的类加载器,也有自己的委派关系,这里不做说明,下面案例将模拟一个JVM加载两份实例(即模拟Tomcat打破双亲委派)。
在演示沙箱安全的代码基础下,修改了Cat.class并放到了一个新的目录下。然后用类加载器分别去实例化两个目录下的Cat类。
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
MyClassLoader2 myClassLoader = new MyClassLoader2("E:/电子书");
Class<?> clazz = myClassLoader.loadClass("com.evader.evaderjvm.jvm.Cat");
Object obj = clazz.newInstance();
Method method = clazz.getMethod("play", String.class);
method.invoke(obj, "第一只小狗");
System.out.println(clazz.getClassLoader().getClass().getName());
System.out.println(clazz.getClassLoader());
System.out.println("-------------------------------------------------------------");
MyClassLoader2 myClassLoader2 = new MyClassLoader2("E:/电子书/test");
Class<?> clazz2 = myClassLoader2.loadClass("com.evader.evaderjvm.jvm.Cat");
Object obj2 = clazz2.newInstance();
Method method2 = clazz2.getMethod("play", String.class);
method2.invoke(obj2, "第二只小狗");
System.out.println(clazz2.getClassLoader().getClass().getName());
System.out.println(clazz2.getClassLoader());
}
// 运行结果
第一只小狗在玩游戏!
com.evader.evaderjvm.jvm.MyClassLoader2
com.evader.evaderjvm.jvm.MyClassLoader2@5a07e868
-------------------------------------------------------------
这是修改后编译的文件!!!第二只小狗在玩游戏!
com.evader.evaderjvm.jvm.MyClassLoader2
com.evader.evaderjvm.jvm.MyClassLoader2@3fee733d
public class Cat {
public void play(String name) {
System.out.println("这是修改后编译的文件!!!"+name + "在玩游戏!");
}
}
这个案例我通过改变字节码文件内容模拟同一个类的不同版本,通过加载两个不同目录下的类来模拟同一个JVM加载两份同一路径下的类。
问题来了,我们如何来辨别在JVM中存在两份Cat实例呢?
JVM中,判断两个类对象是否是同一个,不光要看类的包名和类名是否都相同之,还需要他们的类
加载器对象也是同一个。从上述的执行结果中可以看出我们的类加载器对象是不一样的。