山田杏奈
山田杏奈
发布于 2025-08-19 / 39 阅读
0
0

JAVA安全基础 -- 类加载机制

0x01 概念

Java是一个依赖于JVM实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader会调用JVM的native方法(defineClass0/1/2)来定义一个java.lang.Class实例。

Java类加载器(Java Classloader)是Java运行时环境的一部分,负责动态加载Java类到Java虚拟机的内存空间中,用于加载系统、网络或者其他来源的类文件。Java源代码通过javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序。

0x02 类文件编译流程

这张图解释了类加载的流程,当我们创建一个ClassLoaderTest.java文件,并经过javac编译成ClassLoaderTest.class字节码文件,这个java文件和生成的class文件都是存储在我们的磁盘当中。但如果我们需要将磁盘中的class文件在java虚拟机内存中运行,需要经过一系列的类的生命周期(加载、连接(验证-->准备-->解析)和初始化操作)。

  1. 在生命周期的加载阶段,JVM会找到需要加载的类并把类的信息加载到JVM的方法区中(InstanceKlass对象),然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。因为方法区中的对象是用C++编写的,java并不能操作这个对象,所以JVM创建一个java.lang.Class对象来包装,可以让开发者在写代码时获取到。同时java.lang.Class对象中的字段内容少于InstanceKlass对象,因为InstanceKlass对象中的一部分字段内容仅面向JVM,所以在堆区实例化一个java.lang.Class对象,将InstanceKlass对象拷贝过来,并剔除掉开发者不需要的内容。

  2. 连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证准备解析

    • 验证阶段JVM会检查字节码的合法性,确保文件内容符合JVM规范,防止恶意代码。

    • 准备阶段JVM会分配内存空间,为类静态变量(static)分配内存并设默认值(0/null),静态常量(final static)的默认值为程序中设定的值。这些静态字段的数据会存放在堆区。

    • 解析的任务就是把常量池中的符号引用转换为直接引用,举个例子,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,JVM就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。所以在解析阶段,JVM会将所有的类或接口名、字段名、方法名转换为具体的内存地址。

  3. 如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有:

    • 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。(final修饰的并且等号右边是常量不会触发初始化)

    • 通过反射方式执行以上三种行为。

    • 初始化子类的时候,会触发父类的初始化。

    • 作为程序入口直接运行时(也就是直接调用main方法)。

类初始化完成后,​堆区的 Class对象通过内部指针引用方法区的字节码数据。

0x03 类加载器分类

一切的Java类都必须经过JVM加载后才能运行,而ClassLoader的主要作用就是Java类文件的加载。在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)Extension ClassLoader(扩展类加载器)App ClassLoader(系统类加载器)AppClassLoader是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader加载类,ClassLoader.getSystemClassLoader()返回的系统类加载器也是AppClassLoader

值得注意的是某些时候我们获取一个类的类加载器时候可能会返回一个null值,如:java.lang.Srting.class.getClassLoader()将返回一个null对象,因为java.lang.Srting类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null

3.1启动类加载器

引导类加载器(Bootstrap ClassLoader),底层原生代码是C++语言编写,属于JVM一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心java库(即JVM本身),存储在/jre/lib/rt.jar目录当中。(同时处于安全考虑,Bootstrap ClassLoader只加载包名为javajavaxsun等开头的类)。

3.2扩展和应用程序类加载器

扩展类加载器(Extensions ClassLoader),由sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载java的扩展库。

App类加载器(App ClassLoader),由sun.misc.Launcher$AppClassLoader实现,一般通过通过(java.class.path或者Classpath环境变量)来加载Java类,也就是我们常说的Classpath路径,通常我们是使用这个加载类来加载Java应用类。

扩展类加载器和App类加载器源码都位于sun.misc.Launcher 中,是一个静态内部类,继承自URLClassloader,具备通过目录或指定jar包将字节码文件加载到内存中。

0x04 双亲委派机制

4.1概念

通常情况下,我们就可以使用JVM默认三种类加载器进行相互配合使用,且是按需加载方式,就是我们需要使用该类的时候,才会将生成的class文件加载到内存当中生成class对象进行使用,且加载过程使用的是双亲委派模式,即把需要加载的类交由父加载器进行处理。

双亲委派机制有两个作用:

  • 保证类加载的安全性,避免恶意代码替换JDK中的核心类库,比如java.lang.Srting ,确保核心类库的完整性和安全性。

  • 避免重复加载,双亲委派机制可以避免一个类被多次加载。

双亲委派机制:每个类加载器都有一个父类加载器,在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则将加载请求委派给父类加载器。如果所有的父类加载器都无法加载该类,则由当前类加载器自己尝试加载,看上去是自顶向下加载。一句话概括就是:自底向上查询是否加载过,再由定向下进行加载。

4.2ClassLoader核心方法

首先先分析ClassLoader的原理,ClassLoader 包含了四个核心方法,双亲委派机制的核心代码就位于loadClass中。

  • public Class<?> loadClass(String name):类加载的入口,提供了双亲委派机制,内部会调用findclass

  • protected Class<?> findClass(String name):由类加载器子类实现,获取二进制数据调用defineClass,比如URLClassLoader会根据文件路径去获取类文件中的二进制数据;

  • protected final Class<?> defineClass(String name, byte[] b, int off, int len):做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中;

  • protected final void resolveClass (Class<?> c):执行类生命周期中的连接阶段。

理解Java类加载机制并非易事,这里我们以一个Java的HelloWorld来学习ClassLoader

ClassLoader加载com.hspstudy.classloader.HelloWorldloadClass重要流程如下:

  1. ClassLoader会调用public Class<?> loadClass(String name)方法加载com.hspstudy.classloader.HelloWorld类。

  2. 调用findLoadedClass方法检查TestHelloWorld类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。

  3. 如果创建当前ClassLoader时传入了父类加载器就使用父类加载器加载TestHelloWorld类,直至父类加载器为null则使用JVM的Bootstrap ClassLoader加载。(向上委派)

  4. 如果上一步无法加载TestHelloWorld类,那么调用自身的findClass方法尝试加载TestHelloWorld类。(向下加载)

  5. 如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的com.hspstudy.classloader.HelloWorld类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类。

  6. 如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。

  7. 返回一个被JVM加载后的java.lang.Class类对象。

这里再对第五步做下解释,所有类加载器都继承自ClassLoader基类,而基类的findClass默认就是抛异常的。所以任何没有重写findClass的加载器(包括用户自定义的)在第五步都会失败。其次,像AppClassLoader这样的系统加载器其实重写了findClass,所以它们能在这一步从classpath找到类。比如我们用于加载jar包的java.net.URLClassLoader其本身通过继承java.lang.ClassLoader类,重写了findClass方法从而实现了加载目录class文件甚至是远程资源文件。同时在双亲委派链中,父加载器(如Bootstrap)根本没有findClass概念,只有到了子加载器(如AppClassLoader)才会用到这个方法。自定义加载器作为委派链最末端,必须自己实现findClass

0x05 自定义ClassLoader

既然已知ClassLoader具备了加载类的能力,我们不仅希望使用classpath中指定的类或JAR包,有时还需要通过自定义类加载器加载本地磁盘文件或网络资源。

package com.hspstudy.classloader;
//测试类
public class HelloWorld {
    public String hello() {
        return "Hello World~";
    }
}

com.hspstudy.classloader.HelloWorld 存在的情况下我们可以使用如下代码即可实现调用hello方法并输出:

HelloWorld t = new HelloWorld();
String str = t.hello();
System.out.println(str);

但是如果com.hspstudy.classloader.HelloWorld根本就不存在于我们的classpath,那么我们可以使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入HelloWorld类的字节码的方式来向JVM中定义一个HelloWorld类,最后通过反射机制就可以调用HelloWorld类的hello方法了。

import java.lang.reflect.Method;
public class TestClassLoader extends ClassLoader {
    // HelloWorld类名
    private static String testClassName = "com.hspstudy.classloader.HelloWorld";
    // HelloWorld类字节码
    private static byte[] testClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 52, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
            16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
            101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,
            1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
            101, 70, 105, 108, 101, 1, 0, 15, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 46,
            106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114,
            108, 100, 126, 1, 0, 35, 99, 111, 109, 47, 104, 115, 112, 115, 116, 117, 100, 121, 47,
            99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 72, 101, 108, 108, 111, 87, 111,
            114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101,
            99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29,
            0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0,
            3, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0,
            0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 1, 0, 11, 0, 0, 0, 2, 0, 12
    };
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        // 只处理HelloWorld类
        if (name.equals(testClassName)) {
            // 调用JVM的native方法定义HelloWorld类
            return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
        }

        return super.findClass(name);
    }
    public static void main(String[] args) {
        // 创建自定义的类加载器
        TestClassLoader loader = new TestClassLoader();
        try {
            // 使用自定义的类加载器加载HelloWorld类
            Class testClass = loader.loadClass(testClassName);

            // 反射创建HelloWorld类,等价于 HelloWorld t = new HelloWorld();
            Object testInstance = testClass.newInstance();

            // 反射获取hello方法
            Method method = testInstance.getClass().getMethod("hello");

            // 反射调用hello方法,等价于 String str = t.hello();
            String str = (String) method.invoke(testInstance);

            System.out.println(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


评论