查看原文
其他

九维团队-绿队(改进)| Java ClassLoader机制解析(二)

aXpmYw 安恒信息安全服务 2022-11-05


上篇内容可点此回顾:九维团队-绿队(改进)| Java ClassLoader机制解析(一)



类加载器的初始化


 ExtClassLoader


通过`AppClassLoader`中的输出可以看出,`AppClassLoader`和`ExtClassLoader`就是Java代码中的两个类,但是这两个类都是在`Launcher`类中进行初始化的,下面看一下具体过程。

注意:在高版本的JDK中没有Launcher类,这里用的是JDK8。


首先声明了一个`ExtClassLoader`变量`var1`,从这里可以看出`ExtClassLoader`就是`Launcher`类中的一个内部类。


然后调用`getExtClassLoader`拿到`ExtClassLoader`对象,跟一下`getExtClassLoader`。


双重检查锁,`volatile`修饰可以保持内存可见性和防止指令重排序。

关于volatile,详可参见: https://juejin.cn/post/6844903502418804743

*左右滑动查看更多


首先判断`instance`是否为`null`,然后进行加锁,然后再次判断,最后利用`createExtClassLoader`拿到`ExtClassLoader`对象返回。


跟一下`createExtClassLoader`。


首先获取`ExtClassLoad`的扫描路径赋值给`File`数组`var1`,最后调用`Launcher.ExtClassLoader`创建一个对象返回,`ExtClassLoad`创建完成。


 AppClassLoader


AppClassLoader的创建:


调用`getAppClassLoader`方法的时候发现传入了`var1`,而`var1`就是`ExtClassLoad`。


跟一下`getAppClassLoader`。


`AppClassLoader`继承`URLClassLoader`。


从系统变量中获取扫描路径封装成`File`数组`var2`,`var2`再封装成`URL`数组`var1x`,最后调用`AppClassLoader`方法返回一个对象。`AppClassLoader`传入了两个参数`var1x和var0`,而`var0`就是传入的`ExtClassLoad`。


再跟一下`AppClassLoader`方法。


其中`var1`跟`var2`就是上一步的`var1x和var0`,再跟一下super。


进去之后发现`var2`对应的是`parent`,这也说明了系统类加载器(App ClassLoader)的父类加载器是扩展类加载器(Ext ClassLoader)。

在上面提到过`AppClassLoader`继承`URLClassLoader`,说明`AppClassLoader`的父类是`URLClassLoader`,但是父类跟父`类加载器`不是一个概念。


系统类加载器(App ClassLoader)的父类加载器是扩展类加载器(Ext ClassLoader),引导类加载器(Bootstrap ClassLoader)没有父类加载器,父类加载器就是加载此类加载器Java 类的类加载器。加载器的Java 类和其它Java 类一样,同样需要类加载器来加载。自定义类加载器的父类加载器是系统类加载器,类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。


双亲委派模式


如果一个类加载器要加载某个类,它并不会自己先去加载,而是让父类加载器去加载,如果父类加载器还有父类加载器,则进一步向上委托,以此递归,最终会到达顶层的启动类加载器(Bootstrap ClassLoader)。


  • 如果父类加载器可以完成加载任务,就成功返回。

  • 如果父类加载器无法完成加载任务,子加载器才会自己去加载。



在上面的流程图中`ExtClassLoader`会委派给`Bootstrap ClassLoader`进行加载,但是`ExtClassLoader`的`parent`字段是`null`。


加载过程


public class Test { public static void main(String[] args) { System.out.println(Test.class.getClassLoader()); }}
*左右滑动查看更多


类的加载是在`main`方法之前进行的,在上面的`Test`类中,在`main`方法之前肯定需要先把`Test`类加载到内存中,这个加载过程依旧在`Launcher`类中。


在`Launcher`类有一个`loadClass`方法,加载类第一步要经过这个方法。


首先拿到类,然后判断有没有权限去加载这个类,再判断这个类是否依旧被加载,当有权限加载并且这个类没有被加载过时会执行到`super.loadClass(var1, var2)`。


打个断点。


发现`var1`并不是`Test`,原因是在加载`Test`之前会先加载一些系统类,我们放过这些类找到`Test`。


放过30多个类之后就是`Test`。这里跟一下`super.loadClass`,打下断点。


发现`name`就是需要加载的`Test`,继续看后面的代码。


首先调用`findLoadedClass`方法判断是否被加载过,没有被加载过`c == null`,继续判断`parent`是否为null,我们可以发现此时`this`为`AppClassLoader`,而`AppClassLoader`的`parent`就是`ExtClassLoader`,所以`parent`不等于null。


继续执行下去的`loadClass`方法就是当前的`loadClass`方法,所以还会再次执行一次。


再次执行时,`this`为`ExtClassLoader`,而`ExtClassLoader`没有`parent`,所以`parent`为null。


`parent = null`则会使用`Bootstrap ClassLoader`来加载`Test`,我们知道`Bootstrap ClassLoader`是负责加载JDK核心类库,显然JDK的核心类库中肯定没有我们自定义的`Test`类,所以这一步肯定会加载失败。


当`Bootstrap ClassLoader`加载失败会继续执行到下面的`if`。


此时的`this`还是`ExtClassLoader`,所以`ExtClassLoader`会调用`findClass`加载`Test`,而`ExtClassLoader`肯定也会加载失败。


跟一下`findClass`。


可以发现这个方法声明了一个异常,当搜索不到`Test`类时就会抛出一个异常。


过几步让其抛出异常。


虽然抛出异常,但是`catch`中并没有任何处理代码,所以最后还有走到`if`,此时的`this`为`AppClassLoader`。


因为`AppClassLoader`主要加载程序中的类文件,所以肯定会加载成功。


过程总结


1、`ClassLoader`会调用`public Class loadClass(String name)`方法加载`Test`类。

2、调用`findLoadedClass`方法判断`Test`类是否已经被加载过,如果被加载过直接返回对象。

3、没有被加载过,会委派给`ExtClassLoader`,`ExtClassLoader`再委派给`Bootstrap ClassLoader`进加载。

4、如果`Bootstrap ClassLoader`加载成功者返回对象,如果加载失败则`ExtClassLoader`调用`findClass`方法开始加载`Test`类。

5、如果`ExtClassLoader`加载成功者返回对象,如果加载失败则`AppClassLoader`开始加载`Test`类。

6、最后返回加载成功的对象。


优势之处


1、避免类的重复加载。

因为有三个加载器来加载,整个过程会判断类是否被加载过,有效的避免了类的重复加载。


2、保证JDK核心API不被随意替换。

例:因为`java.lang`是JDK的核心包,它里面就有一个类是`Integer`,此时我们自己创建一个`java.lang.Integer`。


package java.lang;
public class Integer {
public static void main(String[] args) { com(1, 2); }
public static int com(int a1, int a2){ System.out.println("Test"); return 0; }}

*左右滑动查看更多


我们自己创建的`Integer`中有一个`main`方法,但是JDK核心包中`java.lang`的`Integer`类没有`main`方法。


运行一下:


报错`在类 java.lang.Integer 中找不到 main 方法`,但是我们是有`main`方法的,为什么会报这个错。


再来看一下双亲委派模式。


当加载我们自定义的`Integer`类时首先是`AppClassLoader`,会向上委派,当委派到`Bootstrap ClassLoader`时,因为JDK的核心类库中有`Integer`类所以会被加载成功,会直接返回一个对象。


因为JDK核心包中`java.lang`的`Integer`类没有`main`方法,所以会报错。


可以发现通过这个模式有效的避免了JDK的核心API被替换掉。


文章参考及推荐阅读:https://javasec.org/https://zhuanlan.zhihu.com/p/51374915https://www.cnblogs.com/xrq730/p/4845144.html

*左右滑动查看更多




—  往期回顾  —



关于安恒信息安全服务团队安恒信息安全服务团队由九维安全能力专家构成,其职责分别为:红队持续突破、橙队擅于赋能、黄队致力建设、绿队跟踪改进、青队快速处置、蓝队实时防御,紫队不断优化、暗队专注情报和研究、白队运营管理,以体系化的安全人才及技术为客户赋能。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存