Java类加载器探索与简单的线上热部署思路

Java类加载器探索与简单的线上热部署思路

1 Java代码的执行过程

第一节包含一些「枯燥」的前置基础知识,如果您已经烂熟于心可选择跳过阅读。

写了这么多代码,有没有想过我们的代码是怎么执行的?或者说定义了那么多类,我们的class是怎么加载到内存的?Java语言属于一种高级语言,而cpu能执行的只有机器码,所以Java代码的运行离不开jvm虚拟机的编译,下面用一张图说明在HotSpot虚拟机中Java代码加载到cpu执行的过程。

image-20240808183932746

1.2 类的生命周期与加载过程

在Java中类的生命周期分为5部分:加载、链接、初始化、使用和卸载。其中,使用指的是new一个对象时,卸载是指没有此类的对象实例存在,在垃圾回收时会卸载该类。其中,系统类加载器始终是不会被卸载,只有自定义加载器会被卸载。

具体的类卸载判定条件为:

1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2.加载该类的 classLoader 已经被回收。
3.该类对应的 java.lang.class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

而类的加载过程则包括:1.加载 2.链接(验证、准备、解析) 3.初始化

image-20240808184315029

①加载阶段:

1.通过一个类的全限定名来获取定义此类的二进制字节流。

2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3.在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。

②验证阶段:

1.文件格式验证(是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理)

2.元数据验证(对字节码描述的信息进行语意分析,以保证其描述的信息符合Java语言规范要求)

3.字节码验证(保证被校验类的方法在运行时不会做出危害虚拟机安全的行为)

4.符号引用验证(虚拟机将符号引用转化为直接引用时,解析阶段中发生)

③准备阶段:

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。将对象初始化为“零”值

④解析阶段:

解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。包括运行时常量池和字符串常量池。

⑤初始化阶段:

初始化阶段时加载过程的最后一步,而这一阶段也是真正意义上开始执行类中定义的Java程序代码。

1.3 类加载器的层级与分类

1 启动类加载器(Bootstrap ClassLoader):

该加载器位于JVM内部使用C/C++语言实现,用来加载Java核心类库,如:JAVA_HOME/jre/lib/rt.jarresources.jarsun.boot.class.path路径下的包,用于提供jvm运行所需的包。

并不是继承自java.lang.ClassLoader,所以没有父类加载器。由它加载Extension ClassLoaderApplication Classloader,并成为他们的父类加载器。并且出于安全考虑,启动类只加载包名为java、javax、sun开头的类,且这些类也只能由其加载。

2 扩展类加载器(Extension ClassLoader):

Java语言编写,继承自java.lang.ClassLoader,加载java.ext.dirsjre/lib/ext下的类。可以将自己的类放在该位置就会被自动加载进来。

3 应用程序类加载器(Application Classloader):

Java语言编写,继承自java.lang.ClassLoader,加载classpathjava.class.path路径下的类,同时也是Java中的默认类加载器,程序中的类,都是由它加载完成的。通过ClassLoader#getSystemClassLoader()获取的就是这个加载器。

4 自定义加载器(Custom ClassLoader):

当AppClassLoader不能满足我们的需求时,就可以继承ClassCloader类,自定义类加载器。

image-20240808191029896

1.4 双亲委派机制

每⼀个类都有⼀个对应它的类加载器。在类加载的时候,系统会⾸先判断当前类是否被加载过,已经被加载的类会直接返回,否则才会尝试加载。加载的时候,⾸先会把该请求委派给⽗类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当⽗类加载器⽆法处理时,才由⾃⼰来处理。当⽗类加载器为null时,会使⽤启动类加载器 BootstrapClassLoader 作为⽗类加载器。总结:自底向上检查是否加载,自顶向下尝试加载类。

双亲委派机制有什么好处呢?为什么要这么设计?

  1. 保证JDK核心类的优先加载;
  2. 避免类的重复加载;
  3. 使得Java程序的稳定运⾏,也保证了 Java 的核⼼ API 不被篡改。如果不⽤没有使⽤双亲委派模型,⽽是每个类加载器各自加载的话就会出现⼀些问题,⽐如我们编写⼀个称为 java.lang.Object 类的话,那么程序运⾏的时候,系统就会出现多个不同的Object 类。

所有的类加载都符合双亲委派机制吗?也不尽然,有些情况下需要打破双亲委派机制,下面介绍打破双亲委派机制的情况和方法。

2 打破双亲委派

2.1 SPI:反向委派机制

在利用SPI加载第三方实现类时,其SPI接口属于核心库位于rt.jar中,由Bootstrap类加载器加载,但是Bootstrap却无法直接加载其实现类,如JDBC,jdbc.jar存在于classpath路径,无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载(Context Classloader)器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式打破了双亲委派机制。

image-20240808202831306

2.2 自定义类加载器

自定义类加载器只需要继承ClassLoader类并重写findClass()方法即可。但是想要打破双亲委派机制需要重写loadClass()方法,因为在抽象类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);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
              	// 不为空则调用父类加载器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                // 为空说明是Bootstrap ClassLoader,调用native方法找启动类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
              	// 如果三层加载器都不能加载,那就调用findClass方法
              	// findClass是LoadClass抽象类的空方法,需要子类去实现,也就是自定义类加载器的扩展点
                // 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();
            }
        }
        // 如果resolve为false,则不执行resolveClass方法,即跳过解析步骤,同时也不会进行初始化
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

简单验证resolve=false时是否会跳过初始化阶段:

image-20240809122508520

这里Demo类中的static块代码没有被执行(static块和static变量在初始化阶段执行),则说明跳过了初始化阶段。

获取classloader的方式有以下三种:

// 方式一:获取当前类的 ClassLoader
clazz.getClassLoader()
// 方式二:获取当前线程上下文的 ClassLoader
Thread.currentThread().getContextClassLoader()
// 方式三:获取系统的 ClassLoader
ClassLoader.getSystemClassLoader()

通过上文分析得知双亲委派的机制体现在抽象类ClassLoader#loadClass方法中,因此打破双亲委派只需重写此方法。

public Class<?> loadClass(String name) throws ClassNotFoundException {
        byte[] data = loadClassData(name);
  			// 类名校验,并执行native方法将字节码信息加载到内存
        return defineClass(name, data, 0, data.length);
}

注意,加载Demo类后会加载其父类Object(应该是先加载其父类Object,再加载Demo,但是因为压栈弹栈,所以java.lang.Object的加载顺序在其后),所以会报错FileNotFoundException(如下图),但是以java开头的类都应该是Bootstrap ClassLoader加载,此时就算把Object类拷贝到当前路径也会因为保护措施不能加载,所以应该让parent加载以java开头的类。

image-20240809134524530

所以应该改为:

public class TestClassLoader extends ClassLoader{
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if(name.startsWith("java.")){
            return super.loadClass(name);
        }
        byte[] data = loadClassData(name);
        return defineClass(name, data, 0, data.length);
    }

    private byte[] loadClassData(String name) {
        String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
        String basePath = "/Users/zhaobo/IdeaProjects/clear-sight/tracer-core/target/classes/";
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(basePath + tempName + ".class");
            return IOUtils.toByteArray(fis);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                assert fis != null;
                fis.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public static void main(String[] args) throws ClassNotFoundException {
      TestClassLoader classLoader = new TestClassLoader();
      Class<?> clazz = classLoader.loadClass("com.clear.Demo");
      System.out.println(clazz.getClassLoader());
}
image-20240809134515361

此时的自定义加载器就打破了双亲委派机制,但是这样的类加载器因为没有双亲检查机制会存在一些问题,比如一个class文件被两个相同的类加载器加载,得到的是两个不同的类(因为没有向上查找是否已经加载),正如前文提到,这样就导致了类的重复加载,以及加载多个类导致语义不明确。

3 由类加载器延伸的线上热更新思路

由上文可以自定义类加载器的实现不应该是重写loadClass,而是重写findClass作为扩展点。

protected Class<?> findClass(String name) throws ClassNotFoundException {
    try {
        byte[] classdata = loadClassData(name);
        if (classdata == null) throw new ClassNotFoundException(name);
        return defineClass(name, classdata, 0, classdata.length);
    } catch (IOException e) {
        throw new ClassNotFoundException(name);
    }
}

private byte[] loadClassData(String name) throws IOException {
    String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
    String classDir = basePath + tempName + ".class";
    Path path = Paths.get(classDir);
    if (Files.exists(path)){
        return Files.readAllBytes(path);
    }
    return null;
}

由自定义类加载器可以延伸出一个很直观的应用,就是线上不停机更新,即发布热补丁/热部署/热更新,即不进行重新启动程序的情况下,更新内存中的代码。下面提供一些实现思路,首先定义热部署的ClassLoader:

package com.clear.hotDeploy;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.regex.Matcher;

public class HotDeployClassLoader extends ClassLoader{

    private String basePath;

    public HotDeployClassLoader(String basePath) {
        this.basePath = basePath;
    }

    public HotDeployClassLoader(){}

    public void setBasePath(String basePath){
        this.basePath = basePath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classdata = loadClassData(name);
            if (classdata == null) throw new ClassNotFoundException(name);
            return defineClass(name, classdata, 0, classdata.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }

    private byte[] loadClassData(String name) throws IOException {
        String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
        String classDir = basePath + tempName + ".class";
        Path path = Paths.get(classDir);
        if (Files.exists(path)){
            return Files.readAllBytes(path);
        }
        return null;
    }
}

然后定义一个以class为单位热部署类:

package com.clear.hotDeploy;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;

public class HotDeploy {

    private String file_name;// 全限定文件名
    private String class_name;// 引用类名
    private String base_path;// 根路径

    private final AtomicBoolean modified = new AtomicBoolean(false);

    public HotDeploy() {
    }

    public HotDeploy(String name) {
    		// 去掉file:文件协议
        this.base_path = (getClass().getResource("/").toString()).substring(5);
        this.class_name = name;
        String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
        this.file_name = base_path + tempName + ".class";
    }

    public void start() throws InterruptedException, ClassNotFoundException {
    		// 监控class文件是否更新
        this.monitor();
        while (true) {
            TimeUnit.SECONDS.sleep(2);
            if (modified.get()) {
                Class<?> reload = this.reload();
                System.out.println("reload success, current class is : " + reload.toString());
            }
        }
    }

    private Class<?> reload() throws ClassNotFoundException {
        HotDeployClassLoader classLoader = new HotDeployClassLoader();
        classLoader.setBasePath(base_path);
        // reload后置标识位为false
        modified.compareAndSet(true,false);
        return classLoader.loadClass(class_name);
    }

    private void monitor() {
        Thread t = new Thread(()->{
            try {
                long lastModified = Files.getLastModifiedTime(Paths.get(file_name)).toMillis();
                while(true) {
                    TimeUnit.SECONDS.sleep(1);
                    long newLastModified = Files.getLastModifiedTime(Paths.get(file_name)).toMillis();
                    // 如果监视的class文件被修改
                    if(newLastModified != lastModified) {
                        modified.compareAndSet(false, true);
                        lastModified = newLastModified;
                    }
                }
            } catch (InterruptedException | IOException e) {
                e.printStackTrace();
            }
        });
        t.setDaemon(true); // 守护进程
        t.start();
    }
}

验证热更新:

package com.clear.hotDeploy;

public class TestHotDeploy {

    public static void main(String[] args) throws InterruptedException, ClassNotFoundException {

        HotDeploy hotDeploy = new HotDeploy("com.clear.Demo");
        hotDeploy.start();
    }
}

启动后,到源文件夹下,编译Demo.java文件并移动到target路径下:

image-20240809162201629

检测到文件更新,控制台打印当前类名,实际应用中可以通过反射打印当前版本。

image-20240809162105893

以上。


本博客内容仅供个人学习使用,禁止用于商业用途。转载需注明出处并链接至原文。

posted @ 2024-08-09 16:51  爱吃麦辣鸡翅  阅读(30)  评论(0编辑  收藏  举报