java字节码以及ClassLoader类加载机制

一、什么是Java的“字节码”

严格来说,Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储在.class文件中。

二、ClassLoader 基本知识

那么字节码交给jvm之后,怎么来加载这些字节码呢,此时就需要ClassLoader。ClassLoader是什么呢?它就是一个“加载器”,告诉Java虚拟机如何加载这个类。

在 JVM 中存在如下几种 ClassLoader

  • BootstrapClassLoader: 由 C++ 实现, 负责加载 %JAVA_HOME%\lib 目录中的 java 核心类库比如rt.jar、resources.jar、charsets.jar和class等, 路径也可由 -Xbootclasspath 参数指定

  • ExtensionClassLoader: 由 sun.misc.Launcher$ExtClassLoader 实现, 负责加载 %JAVA_HOME\lib\ext目录中的 java 扩展库, 路径也可由 -Djava.ext.dirs 参数指定

  • AppClassLoader: 由 sun.misc.Launcher$AppClassLoader 实现, 负责加载当前 classpath 下的 class 文件, 路径也可由 -Djava.class.path 参数指定

  • UserDefineClassLoader: 为开发者自行编写, 通过继承 java.lang.ClassLoader 并重写相关方法来自定义 ClassLoader

一般情况下, 如果不指定 ClassLoader, 我们编写的 Java 类在加载时默认会使用 AppClassLoader (可以通过 ClassLoader.getSystemClassLoader() 来获取)

1、ClassLoader非继承关系

其中不同 ClassLoader 会有父子关系 (非继承关系), 其本质是在 Java.lang.ClassLoader 内部定义了指向父加载器的的常量 parent, 可以通过调用 getParent() 方法获取父加载器

AppClassLoader 的父加载器为 ExtensionClassLoader, 而 ExtensionClassLoader 的父加载器 BoostrapClassLoader 是由 C++ 实现的, 无法在 Java 中获取对应的引用, 所以显示 null

2、ClassLoader的继承关系

所有的 ClassLoader 都继承自 java.lang.ClassLoader 这个抽象类, 而 ExtClassLoader 和 AppClassLoader 继承自 URLClassLoader,之所以这样继承的原因是 URLClassLoader 既可以加载本地的字节码, 也可以加载远程的字节码, 而 ExtClassLoader 和 AppClassLoader 是对加载本地字节码这一功能的更为具体的实现

3、URLClassLoader

上面提到URLClassLoader 实际上是我们平时默认使用的AppClassLoader 的父类,所以,我们解释URLClassLoader 的工作过程实际上就是在解释默认的Java类加载器的工作流程。

正常情况下,Java会根据配置项sun.boot.class.pathjava.class.path 中列举到的基础路径(这些路径是经过处理后的java.net.URL类)来寻找.class文件来加载,而这个基础路径又分为三种情况:

  • URL未以 !/ 结尾,则认为是一个JAR文件,使用JarLoader 来寻找类,即为在Jar包中寻找.class文件

  • URL以斜杠/ 结尾,且协议名是file ,则使用FileLoader 来寻找类,即为在本地文件系统中寻找.class文件

  • URL以斜杠/ 结尾,且协议名不是file ,则使用最基础的Loader 来寻找类,我们正常开发的时候通常遇到的是前两者,那什么时候才会出现使用Loader 寻找类的情况呢?当然是非file 协议的情况下,最常见的就是http 协议。

作为攻击者,如果我们能够控制目标Java ClassLoader的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码了。

4、主要方法

因为 java.lang.ClassLoader 是所有 ClassLoader 的基石, 所以在这个抽象类中定义了几个比较重要的方法,不管是加载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用

  • loadClass(): 作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行findClass

  • findClass(): 根据基础URL指定的方式来加载类的字节码,就像上面说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,并调用 defineClass 方法, 具体实现由子类重写

  • defineClass(): 把前面传入byte 数组形式的字节码转换成对应的 Class 对象 (真正加载字节码的地方)

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
// name 为类名 (可设置为 null)
// b 为前面传入的字节码数组
// off 为数组的偏移值 (从第几位开始为字节码数据)
// len 为数组的长度

所以可见,真正核心的部分其实是defineClass ,他决定了如何将一段字节流转变成一个Java类,Java默认的ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中。

注意一点,在defineClass 被调用的时候,类对象是不会被初始化的只有这个对象显式地调用其构造函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的static块中,在defineClass 时也无法被直接调用到。所以,如果我们要使用defineClass 在目标机器上执行任意代码,需要想办法调用构造函数

三、ClassLoader 加载流程 (双亲委派机制)

1、类加载流程

Java 类的加载方式为"动态加载", 即程序不会一开始就将所有的 class 都加载进 JVM, 而是根据程序运行的需要, 一步一步加载所需的 class

class 的加载方法分为两种

  • 隐式加载: 通过 new 实例化类, 或通过 类名.方法名() 调用其静态方法, 或调用其静态属性

  • 显式加载: 通过反射的形式, 例如 Class.forName() 或者调用 ClassLoader 的 loadClass 方法

其中 Class.forName() 有两个重载方法 

public static Class<?> forName(String className);
public static Class<?> forName(String name, boolean initialize, ClassLoader loader);
  • 这里的 initialize 表示是否进行类初始化, 而 loader 用于指定加载该类的 ClassLoader 调用第一个方法时, initialize 默认为 true, 即进行类初始化 (加载 static 类型的属性, 并且执行 static {} 块中的代码), 如果不想初始化类, 可以调用第二个方法并手动指定 initialize 为 false。参考反射文章。

  • 而通过 ClassLoader.loadClass() 加载的类默认是不会进行类初始化的, 需要注意一下。

2、双亲委派

类加载基于一种叫做"双亲委派"的机制, 那么什么是双亲委派机制?

1. 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。

2. 递归,重复第1部的操作。直到最顶层Bootstrap ClassLoader加载器看自己缓存是否有,如果没有的话就去查找自己规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器ExtClassLoader自己去找。

3. ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器AppClassLoader找。

4.AppClassLoader自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。

我们可以发现委托是从下向上,然后具体查找过程却是自上至下。

看一下 java.lang.ClassLoader#loadClass 的源码会更容易理解这个流程

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);    //检查class是否已经被加载,也就是查找缓存
        if (c == null) {        //如果缓存没有就执行如下代码
            long t0 = System.nanoTime();
            try {
                if (parent != null) {         //如果存在父类的ClassLoader
                    c = parent.loadClass(name, false);    // 调用父类的 loadClass 方法, 进行委托,然后如上查找是否有缓存
                } else {        //没有父类的ClassLoader时,此时父加载器为 BootstrapClassLoader
                    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);        // 先从最顶层BootstrapClassLoader尝试调用 findClass 方法来加载 class,看是否能找到

                // 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;
    }
}
  • 可以看到, 其实 loadClass 的主要作用就是双亲委派, 至于如何获取字节码以及如何将字节码转换为 Class 对象, 都是在 findClass 以及 defineClass 中实现的

  • 另外, JVM 在判断两个 class 是否相同时, 不仅会判断两者的类名是否相同, 而且会判断两个类是否是由同一个 ClassLoader 加载的, 只有这两个条件同时满足, 才能说明这两个 class 相同

四、自定义ClassLoader

上面提到ClassLoader 能够加载字节码的关键就在于 loadClass findClass defineClass 这三个方法

  • 因为 loadClass 实现了双亲委派机制, Java 官方不推荐直接重写该方法 (除去一些特殊情况, 比如 tomcat 和 jdbc 就破坏了这种机制)

  • 而defineClass 是一个 native 方法, 底层由 C++ 实现

  • 所以我们的重点就是重写 findClass 方法, 并最终在里面调用 defineClass

一个 ClassLoader 在实例化时如果没有指定 parent, 那么它的默认 parent 为 AppClassLoader, 可以通过重写对应的带参构造方法来手动指定 parent ClassLoader

举例:

1.Hello.java 

public class Hello {
    public Hello() {
        System.out.println("hello world test");
    }
}
  • 通过javac Hello.java命令进行编译,然后放到了C:\tools\Test\目录下。

2.HelloClassLoaderTest.java

通过继承了ClassLoader类,重写了findClass方法,自定义path从C:\tools\Test\下开始作文根目录加载class文件。下面同样举例了通过内置的URLClassLoader加载当前目录下的Hello.class文件。

package com.huawei.ClassLoader;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class HelloClassLoaderTest extends ClassLoader{
    protected String path;
    public HelloClassLoaderTest(String path) {
        super();
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name)  {
        try {
            Path path1 = Paths.get(this.path, name + ".class");
            byte[] bytes = Files.readAllBytes(path1);
            return defineClass(name,bytes,0,bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) throws Exception {
        HelloClassLoaderTest helloClassLoaderTest = new HelloClassLoaderTest("C:\\tools\\Test\\");
        Class<?> hello = helloClassLoaderTest.loadClass("Hello");
        hello.newInstance();
        System.out.println("---------------");
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file://.\\")});
        Class<?> hello1 = urlClassLoader.loadClass("com.huawei.ClassLoader.Hello");
        hello1.newInstance();
    }
}

参考:

java中的ClassLoader详解_帅大大的架构之路的博客-CSDN博客_classloader的作用

T00ls | 低调求发展 - 潜心习安全

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thunderclap_

点赞、关注加收藏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值