JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统

深入理解JVM - 类加载机制

2020-01-20  本文已影响0人  xiaolyuh

类加载过程

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking),如图:

image.png

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始。

加载

在加载阶段,Java虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,不要混淆了。

验证

验证的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,如:

元数据验证

对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求,如:

字节码验证

通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的,如:

符号引用验证

符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,即解析阶段。主要目的是检查该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源,如:

准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,当类变量被final修饰时,在准备阶段就直接会被复制,不是使用初始值,如:

public static int a = 123;
public static final int B = 123;

在准备阶段a的值是0,B的值是123。

下面是,基本数据类型的初始值:

类型 默认值
int 0
long 0L
byte (byte)0
short (short)0
char '\u000'
float 0.0f
double 0.0d
boolean false
reference null

解析阶段

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化

初始化阶段就是执行类构造器 <clinit>()方法的过程,它是真正开始执行Java代码的阶段,比如给类属性赋真实的值。

public static int a = 123;

在初始化阶段后,a的值才等于123。

  • <clinit>() 方法是由编译器自动收集类中的所有类变量(static变量)的赋值动作和静态语句块(static{}块)中的语句合并产生的,收集顺序是源文件中的代码顺序;
  • <clinit>() 方法不是必须的,如果我们的源文件中没有静态语句块和静态属性的赋值,那么久不会有<clinit>() 方法。
  • <clinit>() 方法在多线程情况下会通过加锁的方式来保证同步,并且只会被执行一次
  • 子类 <clinit>() 方法执行之前需要保证先执行父类的 <clinit>() 方法,所以Object类的 <clinit>() 方法是第一个执行的

类的加载时机

类的初始化时机

虚拟机没有规定什么时候开始加载一个类,但是严格规定了什么时候需要初始化一个类,有且仅有6种情况:

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如:
    • 使用new关键字实例化对象;
    • 读取或设置一个类型的静态字段(final除外);
    • 调用一个类的静态方法;
  2. 使用java.lang.reflect包的方法对类型进行反射调用的时候;
  3. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类;
  5. 当使用JDK 7新加入的动态语言支持时;
  6. 当一个接口中定义了JDK 8新加入的默认方法时(被default关键字修饰的接口方法);

除此之外,所有引用类型的方式都不会触发初始化,称为被动引用

被动引用

通过子类引用父类的静态字段,不会导致子类初始化:

/**
 * 被动使用类字段演示一:
 * 通过子类引用父类的静态字段,不会导致子类初始化
 **/
public class SuperClass {

    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;
}

public class SubClass extends SuperClass {

    static {
        System.out.println("SubClass init!");
    }
}

/**
 * 非主动使用类字段演示
 **/
public class NotInitialization {

    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }

}

运行结果:

SuperClass init!
123

上述代码运行之后,只会输出“SuperClass init!”,而不会输出“SubClass init!”。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

通过数组定义引用类,不会触发此类的初始化:

/**
 * 被动使用类字段演示二:
 * 通过数组定义来引用类,不会触发此类的初始化
 **/
public class NotInitialization2 {

    public static void main(String[] args) {
        SubClass[] subClasses = new SubClass[1];
    }
}

运行过后什么都没输出。

常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的:

/**
 * 被动使用类字段演示三:
 * 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的
 * 类的初始化
 **/
class ConstClass {

    static {
        System.out.println("ConstClass init!");
    }

    public static final String HELLOWORLD = "hello world";
}

/**
 * 非主动使用类字段演示
 **/
public class NotInitialization3 {

    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }
}

运行结果:

hello world

通过javap -verbose NotInitialization3我们可以发现hello world已经在当前类的常量池了:

PS E:\> javap -verbose NotInitialization3.class
...
Constant pool:
   #1 = Methodref          #7.#21         // java/lang/Object."<init>":()V
   #2 = Fieldref           #22.#23        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #24            // com/xiaolyuh/ConstClass
   #4 = String             #25            // hello world
  ...
  #25 = Utf8               hello world
 ...

反编译NotInitialization3.class结果,可以发现ConstClass.HELLOWORLD被优化了,如下:

package com.xiaolyuh;

public class NotInitialization3 {
   public static void main(String[] args) {
        System.out.println("hello world");
    }
}

接口的初始化时机

编译器仍然会为接口生成“<clinit>()”类构造器,用于初始化接口中所定义的成员变量。

接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

类加载器

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间

类加载器类型

双亲委派模型

image.png

如图中展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(ParentsDelegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程是:所有类的加载都委托给父加载器去完成,当父加载器无法加载这个类的时候,子加载器才会尝试加载。

双亲委派模型最大的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系,保证同一个类只会被一个加载器加载。

双亲委派模型的实现

// 使用synchronized来保证线程安全
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 首先,检查请求的类是否已经被加载过了
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
        if (parent != null) {
            c = parent.loadClass(name, false);
        } else {
            c = findBootstrapClassOrNull(name);
        }
        } catch (ClassNotFoundException e) {
            // 如果父类加载器抛出ClassNotFoundException
            // 说明父类加载器无法完成加载请求
        }
        if (c == null) {
            // 在父类加载器无法加载时
            // 再调用本身的findClass方法来进行类加载
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。

破坏双亲委派模型

双亲委派模型主要出现过3次较大规模“被破坏”的情况:

  1. 在1.2之前,由于实现自定义类加载器只有覆盖loadClass()方法,导致了双亲委派模型的破坏,在1.2之后引入了findClass()方法之后得以解决。
  2. 基础类型无法调用回用户的代码,如JNDI、JDBC、JCE、JAXB和JBI等,他们的接口定义是基础类型,但是他们的实现是各各厂商,这就导致了基础类型需要调用用户代码。后来引入线程上下文类加载器(Thread Context ClassLoader)得以解决。
  3. 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(HotDeployment)等。

自定义类加载器

自定义类加载器需要继承ClassLoader类,为了不破坏双亲委派模型,自定义类加器建议覆盖findClass()方法,不建议覆盖loadClass()方法。下面是我实现的一个加载加密class文件、防止反编译核心代码的类加载器。

加密Class文件

加密class文件的代码:

/**
 * 加密Class文件
 *
 * @author yuhao.wang3
 * @since 2020/1/20 10:39
 */
public class EncryptionClassFileTask extends RecursiveAction {

    Logger logger = LoggerFactory.getLogger(EncryptionClassFileTask.class);

    /**
     * 公钥
     */
    String publicKey;

    /**
     * 需要加密的目录
     */
    private File file;

    /**
     * 需要加密的包名
     */
    private List<String> packages;

    /**
     * 需要排除的Class名称
     */
    private List<String> excludeClass;


    /**
     * @param file      需要加密文件的目录
     * @param packages  需要加密的包名
     * @param publicKey 公钥
     */
    public EncryptionClassFileTask(File file, List<String> packages, String publicKey) {
        this(file, packages, null, publicKey);
    }

    /**
     * @param file         需要加密文件的目录
     * @param packages     需要加密的包名
     * @param excludeClass 需要排除的类名称
     * @param publicKey    公钥
     */
    public EncryptionClassFileTask(File file, List<String> packages, List<String> excludeClass, String publicKey) {
        this.file = file;
        this.excludeClass = excludeClass;
        this.publicKey = publicKey;
        this.packages = new ArrayList<>();

        if (Objects.nonNull(packages)) {
            for (String packageName : packages) {
                this.packages.add(packageName.replace('.', File.separatorChar));
            }
        }

        if (Objects.isNull(excludeClass)) {
            this.excludeClass = new ArrayList<>();
        }

        this.excludeClass.add("RsaClassLoader");
    }

    @Override
    protected void compute() {
        if (Objects.isNull(file)) {
            return;
        }

        File[] files = file.listFiles();
        List<EncryptionClassFileTask> fileTasks = new ArrayList<>();
        if (Objects.nonNull(files)) {
            for (File f : files) {
                // 拆分任务
                if (f.isDirectory()) {
                    fileTasks.add(new EncryptionClassFileTask(f, packages, excludeClass, publicKey));
                } else {
                    if (f.getAbsolutePath().endsWith(".class")) {
                        if (!CollectionUtils.isEmpty(excludeClass) && excludeClass.contains(f.getName().substring(0, f.getName().indexOf(".")))) {
                            continue;
                        }
                        // 如果packages为空直接加密文件夹下所有文件
                        if (CollectionUtils.isEmpty(packages)) {
                            encryptFile(f);
                            return;
                        }
                        // 如果packages不为空则加密指定报名下面的文件
                        for (String packageName : packages) {
                            if (f.getPath().contains(packageName.replace('.', File.separatorChar))) {
                                encryptFile(f);
                                return;
                            }
                        }
                    }
                }
            }
            // 提交并执行任务
            invokeAll(fileTasks);
            for (EncryptionClassFileTask fileTask : fileTasks) {
                // 等待任务执行完成
                fileTask.join();
            }
        }
    }

    private void encryptFile(File file) {
        try {
            logger.info("加密[{}] 文件 开始", file.getPath());
            byte[] bytes = RSAUtil.encryptByPublicKey(RSAUtil.toByteArray(file), publicKey);

            try (FileChannel fc = new FileOutputStream(file.getPath()).getChannel()) {
                ByteBuffer bb = ByteBuffer.wrap(bytes);
                fc.write(bb);
                logger.info("加密[{}] 文件 结束", file.getPath());
            }
        } catch (IOException e) {
            logger.error("加密文件 {} 异常:{}", file.getPath(), e.getMessage(), e);
        }
    }
}

加密class文件的测试类:

public class EncryptionClassFileTaskTest {
    public static void main(String[] args) throws Exception {
        String testPublicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCE7vuntatVmQVp6xGlBa/U/cEkKtFjyhsTtn1inlYtw5KSasTfa/HMPwJKp1QchsGEt0usOkHHC9HuD8o1gKx/Dgjo6b/XGu6xhinyRjCJWLSHXGOq9VLryaThwZsRB4Bb5DU9NXkl8WE2ih8QEKO1143KeJ5SE38awi74im0dzQIDAQAB";

        List<String> excludeClass = Lists.newArrayList("MainController");
        List<String> packages = Lists.newArrayList("com.xiaolyuh.controller");

        encryptionClassFile("D:\\aes_class", packages, excludeClass, testPublicKey);
    }

    private static void encryptionClassFile(String filePath, List<String> packages, List<String> excludeClass, String publicKey) throws Exception {
        ForkJoinPool pool = new ForkJoinPool(16);

        File classFile = new File(filePath);
        if (!classFile.exists()) {
            throw new NoSuchFileException("File does not exist!");
        }
        pool.invoke(new EncryptionClassFileTask(classFile, packages, excludeClass, publicKey));
    }
}

输出:

16:38:03.396 [ForkJoinPool-1-worker-9] INFO com.xiaolyuh.utils.EncryptionClassFileTask - 加密[D:\aes_class\spring-boot-student-jvm-0.0.1-SNAPSHOT\BOOT-INF\classes\com\xiaolyuh\controller\UserController.class] 文件 开始
16:38:04.530 [ForkJoinPool-1-worker-9] INFO com.xiaolyuh.utils.EncryptionClassFileTask - 加密[D:\aes_class\spring-boot-student-jvm-0.0.1-SNAPSHOT\BOOT-INF\classes\com\xiaolyuh\controller\UserController.class] 文件 结束

解密Class文件加载器

/**
 * RSA 加密的ClassLoader
 *
 * @author yuhao.wang3
 * @since 2020/1/19 17:06
 */
public class RsaClassLoader extends ClassLoader {
    private static final int MAGIC = 0xcafebabe;

    Logger logger = LoggerFactory.getLogger(RsaClassLoader.class);

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            // 先使用父加载器加载
            return getParent().loadClass(name);
        } catch (Throwable t) {
            return findClass(name);
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = name.replace('.', File.separatorChar) + ".class";
        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName)) {
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

            logger.warn("请输入解密私钥,否则无法启动服务");
            System.out.print("请输入解密私钥::");
            String privateKey = br.readLine();
            logger.info("解密[{}] 文件 开始", name);
            byte[] bytes = RSAUtil.decryptByPrivateKey(RSAUtil.toByteArray(inputStream), privateKey);
            logger.info("解密[{}] 文件 结束", name);
            return this.defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            logger.info("解密 [{}] 文件异常: {}", name, e.getMessage(), e);
            throw new ClassNotFoundException(String.format("解密 [%s] 文件异常: %s", name, e.getCause()));
        }
    }
}

测试类:

/**
 * RSA 加密的ClassLoader
 *
 * @author yuhao.wang3
 * @since 2020/1/19 17:06
 */
public class RsaClassLoaderTest {
    public static void main(String[] args) throws Exception {
        RsaClassLoader loader = new RsaClassLoader();
        Object object = loader.loadClass("com.xiaolyuh.controller.UserController2").newInstance();
        System.out.println("使用默认类加载器: class :" + object.getClass() + "  ClassLoader:" + object.getClass().getClassLoader());

        Object object2 = loader.loadClass("com.xiaolyuh.controller.UserController").newInstance();
        System.out.println("使用自定义类加载器:class :" + object2.getClass() + "  ClassLoader:" + object2.getClass().getClassLoader());
        System.out.println(JSON.toJSONString(object2));
    }
}

运行提示输入解密公钥,输入后类加载成功:

使用默认类加载器: class :class com.xiaolyuh.controller.UserController2  ClassLoader:sun.misc.Launcher$AppClassLoader@18b4aac2
17:21:30.233 [main] WARN com.xiaolyuh.RsaClassLoader - 请输入解密私钥,否则无法启动服务
请输入解密私钥::MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAITu+6e1q1WZBWnrEaUFr9T9wSQq0WPKGxO2fWKeVi3DkpJqxN9r8c...
17:21:34.252 [main] INFO com.xiaolyuh.RsaClassLoader - 解密[com.xiaolyuh.controller.UserController] 文件 开始
17:21:34.737 [main] INFO com.xiaolyuh.RsaClassLoader - 解密[com.xiaolyuh.controller.UserController] 文件 结束
使用自定义类加载器:class :class com.xiaolyuh.controller.UserController  ClassLoader:com.xiaolyuh.RsaClassLoader@1c93084c

源码

https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-jvm工程

参考

《深入理解JAVA虚拟机》

上一篇下一篇

猜你喜欢

热点阅读