JVM核心知识体系

​ 1.问题 1、如何理解类文件结构布局? 2、如何应用类加载器的工作原理进行将应用辗转腾挪? 3、热部署与热替换有何区别,如何隔离类冲突? 4、JVM如何管理内存,有何内存...

1.问题

  • 1、如何理解类文件结构布局?

  • 2、如何应用类加载器的工作原理进行将应用辗转腾挪?

  • 3、热部署与热替换有何区别,如何隔离类冲突?

  • 4、JVM如何管理内存,有何内存淘汰机制?

  • 5、JVM执行引擎的工作机制是什么?

  • 6、JVM调优应该遵循什么原则,使用什么工具?

  • 7、JPDA架构是什么,如何应用代码热替换?

  • 8、JVM字节码增强技术有哪些?

2.关键词

类结构,类加载器,加载,链接,初始化,双亲委派,热部署,隔离,堆,栈,方法区,计数器,内存回收,执行引擎,调优工具,JVMTI,JDWP,JDI,热替换,字节码,ASM,CGLIB,DCEVM

3.全文概要

作为三大工业级别语言之一的JAVA如此受企业青睐有加,离不开她背后JVM的默默复出。只是由于JAVA过于成功以至于我们常常忘了JVM平台上还运行着像Clojure/Groovy/Kotlin/Scala/JRuby/Jython这样的语言。我们享受着JVM带来跨平台“一次编译到处执行”台的便利和自动内存回收的安逸。本文从JVM的最小元素类的结构出发,介绍类加载器的工作原理和应用场景,思考类加载器存在的意义。进而描述JVM逻辑内存的分布和管理方式,同时列举常用的JVM调优工具和使用方法,最后介绍高级特性JDPA框架和字节码增强技术,实现热替换。从微观到宏观,从静态到动态,从基础到高阶介绍JVM的知识体系。

4.类的装载

1. 类的结构

我们知道不只JAVA文本文件,像Clojure/Groovy/Kotlin/Scala这些文本文件也同样会经过JDK的编译器编程成class文件。进入到JVM领域后,其实就跟JAVA没什么关系了,JVM只认得class文件,那么我们需要先了解class这个黑箱里面包含的是什么东西。

JVM规范严格定义了CLASS文件的格式,有严格的数据结构,下面我们可以观察一个简单CLASS文件包含的字段和数据类型。

详细的描述我们可以从JVM规范说明书里面查阅类文件格式(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html),类的整体布局如下图展示的。

在我的理解,我想把每个CLASS文件类别成一个一个的数据库,里面包含的常量池/类索引/属性表集合就像数据库的表,而且表之间也有关联,常量池则存放着其他表所需要的所有字面量。了解完类的数据结构后,我们需要来观察JVM是如何使用这些从硬盘上或者网络传输过来的CLASS文件。

2. 加载机制

类的入口

在我们探究JVM如何使用CLASS文件之前,我们快速回忆一下编写好的C语言文件是如何执行的?我们从C的HelloWorld入手看看先。

#include <stdio.h>

int main() {
  /* my first program in C */
  printf("Hello, World! n");
  return 0;
}
 

编辑完保存为hello.c文本文件,然后安装gcc编译器(GNU C/C++)

$ gcc hello.c
$ ./a.out
Hello, World!

这个过程就是gcc编译器将hello.c文本文件编译成机器指令集,然后读取到内存直接在计算机的CPU运行。从操作系统层面看的话,就是一个进程的启动到结束的生命周期。

下面我们看JAVA是怎么运行的。学习JAVA开发的第一件事就是先下载JDK安装包,安装完配置好环境变量,然后写一个名字为helloWorld的类,然后编译执行,我们来观察一下发生了什么事情?

先看源码,有够简单了吧。

package com.zooncool.example.theory.jvm;
public class HelloWorld {
   public static void main(String[] args) {
       System.out.println("my classLoader is " + HelloWorld.class.getClassLoader());
   }
}

编译执行

$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java
$ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld
my classLoader is sun.misc.Launcher$AppClassLoader@2a139a55

对比C语言在命令行直接运行编译后的a.out二进制文件,JAVA的则是在命令行执行java classFile,从命令的区别我们知道操作系统启动的是java进程,而HelloWorld类只是命令行的入参,在操作系统来看java也就是一个普通的应用进程而已,而这个进程就是JVM的执行形态(JVM静态就是硬盘里JDK包下的二进制文件集合)。

学习过JAVA的都知道入口方法是public static void main(String[] args),缺一不可,那我猜执行java命令时JVM对该入口方法做了唯一验证,通过了才允许启动JVM进程,下面我们来看这个入口方法有啥特点。

  • 去掉public限定

    $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java
    $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld
    错误: 在类 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 请将 main 方法定义为:
      public static void main(String[] args)
    否则 JavaFX 应用程序类必须扩展javafx.application.Application

说名入口方法需要被public修饰,当然JVM调用main方法是底层的JNI方法调用不受修饰符影响。

  • 去掉static限定

    $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java
    $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld
    错误: main 方法不是类 com.zooncool.example.theory.jvm.HelloWorld 中的static, 请将 main 方法定义为:
      public static void main(String[] args)

我们是从类对象调用而不是类创建的对象才调用,索引需要静态修饰

  • 返回类型改为int

    $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java
    $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld
    错误: main 方法必须返回类 com.zooncool.example.theory.jvm.HelloWorld 中的空类型值, 请
    将 main 方法定义为:
      public static void main(String[] args)

void返回类型让JVM调用后无需关心调用者的使用情况,执行完就停止,简化JVM的设计。

  • 方法签名改为main1

  • $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java
    $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld
    错误: 在类 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 请将 main 方法定义为:
      public static void main(String[] args)
    否则 JavaFX 应用程序类必须扩展javafx.application.Application

这个我也不清楚,可能是约定俗成吧,毕竟C/C++也是用main方法的。

说了这么多main方法的规则,其实我们关心的只有两点:

  • HelloWorld类是如何被JVM使用的

  • HelloWorld类里面的main方法是如何被执行的

关于JVM如何使用HelloWorld下文我们会详细讲到。

我们知道JVM是由C/C++语言实现的,那么JVM跟CLASS打交道则需要JNI(Java Native Interface)这座桥梁,当我们在命令行执行java时,由C/C++实现的java应用通过JNI找到了HelloWorld里面符合规范的main方法,然后开始调用。我们来看下java命令的源码就知道了

类加载器

上一节我们留了一个核心的环节,就是JVM在执行类的入口之前,首先得找到类再然后再把类装到JVM实例里面,也即是JVM进程维护的内存区域内。我们当然知道是一个叫做类加载器的工具把类加载到JVM实例里面,抛开细节从操作系统层面观察,那么就是JVM实例在运行过程中通过IO从硬盘或者网络读取CLASS二进制文件,然后在JVM管辖的内存区域存放对应的文件。我们目前还不知道类加载器的实现,但是我们从功能上判断无非就是读取文件到内存,这个是很普通也很简单的操作。

如果类加载器是C/C++实现的话,那么大概就是如下代码就可以实现

char *fgets( char *buf, int n, FILE *fp );

如果是JAVA实现,那么也很简单

InputStream f = new FileInputStream("theory/jvm/HelloWorld.class");
 

从操作系统层面看的话,如果只是加载,以上代码就足以把类文件加载到JVM内存里面了。但是结果就是乱糟糟的把一堆毫无秩序的类文件往内存里面扔,没有良好的管理也没法用,所以需要我们需要设计一套规则来管理存放内存里面的CLASS文件,我们称为类加载的设计模式或者类加载机制,这个下文会重点解释。

根据官网的定义A class loader is an object that is responsible for loading classes. 类加载器就是负责加载类的。我们知道启动JVM的时候会把JRE默认的一些类加载到内存,这部分类使用的加载器是JVM默认内置的由C/C++实现的,比如我们上文加载的HelloWorld.class。但是内置的类加载器有明确的范围限定,也就是只能加载指定路径下的jar包(类文件的集合)。如果只是加载JRE的类,那可玩的花样就少很多,JRE只是提供了底层所需的类,更多的业务需要我们从外部加载类来支持,所以我们需要指定新的规则,以方便我们加载外部路径的类文件。

系统默认加载器

  • Bootstrap class loader

    作用:启动类加载器,加载JDK核心类

    类加载器:C/C++实现

    类加载路径: /jre/lib

    URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
    /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar
    ...
    /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar

    实现原理:本地方法由C++实现

  • Extensions class loader

    作用:扩展类加载器,加载JAVA扩展类库。

    类加载器:JAVA实现

    类加载路径:/jre/lib/ext

    System.out.println(System.getProperty("java.ext.dirs"));
    /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext:

    实现原理:扩展类加载器ExtClassLoader本质上也是URLClassLoader

    Launcher.java

    //构造方法返回扩展类加载器
    public Launcher() {
        //定义扩展类加载器
        Launcher.ExtClassLoader var1;
        try {
            //1、获取扩展类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
        ...
    }
    
    //扩展类加载器
    static class ExtClassLoader extends URLClassLoader {
         private static volatile Launcher.ExtClassLoader instance;
         //2、获取扩展类加载器实现
         public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
               if (instance == null) {
                    Class var0 = Launcher.ExtClassLoader.class;
                    synchronized(Launcher.ExtClassLoader.class) {
                        if (instance == null) {
                            //3、构造扩展类加载器
                            instance = createExtClassLoader();
                        }
                    }
                }
                return instance;
         }
        //4、构造扩展类加载器具体实现
        private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
            try {
                return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                    public Launcher.ExtClassLoader run() throws IOException {
                        //5、获取扩展类加载器加载目标类的目录
                        File[] var1 = Launcher.ExtClassLoader.getExtDirs();
                        int var2 = var1.length;
                        for(int var3 = 0; var3 < var2; ++var3) {
                            MetaIndex.registerDirectory(var1[var3]);
                        }
                        //7、构造扩展类加载器
                        return new Launcher.ExtClassLoader(var1);
                    }
                });
            } catch (PrivilegedActionException var1) {
                throw (IOException)var1.getException();
            }
        }
        //6、扩展类加载器目录路径
        private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 != null) {
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];
    
                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }
            return var1;
        }
        //8、扩展类加载器构造方法
        public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }
    }
  • System class loader

    作用:系统类加载器,加载应用指定环境变量路径下的类

    类加载器:sun.misc.Launcher$AppClassLoader

    类加载路径:-classpath下面的所有类

    实现原理:系统类加载器AppClassLoader本质上也是URLClassLoader

    Launcher.java

     
    //构造方法返回系统类加载器
    public Launcher() {
       try {
           //获取系统类加载器
           this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
       } catch (IOException var9) {
           throw new InternalError("Could not create application class loader", var9);
       }
    }
    static class AppClassLoader extends URLClassLoader {
       final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
       //系统类加载器实现逻辑
       public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
           //类比扩展类加载器,相似的逻辑
           final String var1 = System.getProperty("java.class.path");
           final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
           return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
               public Launcher.AppClassLoader run() {
                   URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                   return new Launcher.AppClassLoader(var1x, var0);
               }
           });
       }
       //系统类加载器构造方法
       AppClassLoader(URL[] var1, ClassLoader var2) {
           super(var1, var2, Launcher.factory);
           this.ucp.initLookupCache(this);
       }
    }

通过上文运行HelloWorld我们知道JVM系统默认加载的类大改是1560个,如下图

自定义类加载器

内置类加载器只加载了最少需要的核心JAVA基础类和环境变量下的类,但是我们应用往往需要依赖第三方中间件来完成额外的业务,那么如何把它们的类加载进来就显得格外重要了。幸好JVM提供了自定义类加载器,可以很方便的完成自定义操作,最终目的也是把外部的类文件加载到JVM内存。通过继承ClassLoader类并且复写findClass和loadClass方法就可以达到自定义获取CLASS文件的目的。

首先我们看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 {
               //先看是不是最顶层,如果不是则parent为空,然后获取父类
               if (parent != null) {
                   c = parent.loadClass(name, false);
               } else {
                   //如果为空则说明应用启动类加载器,让它去加载
                   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
               //如果还是没有就调用自己的方法,确保调用自己方法前都使用了父类方法,如此递归三次到顶
               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();
           }
       }
       if (resolve) {
           resolveClass(c);
       }
       return c;
   }
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
   throw new ClassNotFoundException(name);
}

通过复写loadClass方法,我们甚至可以读取一份加了密的文件,然后在内存里面解密,这样别人反编译你的源码也没用,因为class是经过加密的,也就是理论上我们通过自定义类加载器可以做到为所欲为,但是有个重要的原则下文介绍类加载器设计模式会提到。

一下给出一个自定义类加载器极简的案例,来说明自定义类加载器的实现。

执行结果如下,我们可以看到加载到内存方法区的两个类的包名+名称是一样的,而对应的类加载器却不一样,而且输出被加载类的值也是不一样的。

----------------class name-----------------
com.zooncool.example.theory.jvm.ClassIsolationPrinciple2$Demo
com.zooncool.example.theory.jvm.ClassIsolationPrinciple2$Demo
-----------------classLoader name-----------------
sun.misc.Launcher$AppClassLoader@18b4aac2
com.zooncool.example.theory.jvm.ClassIsolationPrinciple2$MyClassLoader@511d50c0
-----------------field value-----------------
1
0
 

设计模式

现有的加载器分为内置类加载器和自定义加载器,不管它们是通过C或者JAVA实现的最终都是为了把外部的CLASS文件加载到JVM内存里面。那么我们就需要设计一套规则来管理组织内存里面的CLASS文件,下面我们就来介绍下通过这套规则如何来协调好内置类加载器和自定义类加载器之间的权责。

我们知道通过自定义类加载器可以干出很多黑科技,但是有个基本的雷区就是,不能随便替代JAVA的核心基础类,或者说即是你写了一个跟核心类一模一样的类,JVM也不会使用。你想一下,如果为所欲为的你可以把最基础本的java.lang.Object都换成你自己定义的同名类,然后搞个后门进去,而且JVM还使用的话,那谁还敢用JAVA了是吧,所以我们会介绍一个重要的原则,在此之前我们先介绍一下内置类加载器和自定义类加载器是如何协同的。

  • 双亲委派机制

    定义:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

    实现:参考上文loadClass方法的源码和注释,通过最多三次递归可以到启动类加载器,如果还是找不到这调用自定义方法。

双亲委派机制很好理解,目的就是为了不重复加载已有的类,提高效率,还有就是强制从父类加载器开始逐级搜索类文件,确保核心基础类优先加载。下面介绍的是破坏双亲委派机制,了解为什么要破坏这种看似稳固的双亲委派机制。

  • 破坏委派机制

    定义:打破类加载自上而上委托的约束。

    实现:1、继承ClassLoader并且重写loadClass方法体,覆盖依赖上层类加载器的逻辑;

    2、”启动类加载器”可以指定“线程上下文类加载器”为任意类加载器,即是“父类加载器”委托“子类加载器”去加载不属于它加载范围的类文件;

    说明:双亲委派机制的好处上面我们已经提过了,但是由于一些历史原因(JDK1.2加上双亲委派机制前的JDK1.1就已经存在,为了向前兼容不得不开这个后门让1.2版本的类加载器拥有1.1随意加载的功能)。还有就是JNDI的服务调用机制,例如调用JDBC需要从外部加载相关类到JVM实例的内存空间。

介绍完内置类加载器和自定义类加载器的协同关系后,我们要重点强调上文提到的重要原则。

  • 唯一标识

    定义:JVM实例由类加载器+类的全限定包名和类名组成类的唯一标志。

    实现:加载类的时候,JVM 判断类是否来自相同的加载器,如果相同而且全限定名则直接返回内存已有的类。

    说明:上文我们提到如何防止相同类的后门问题,有了这个黄金法则,即使相同的类路径和类,但是由于是由自定义类加载器加载的,即使编译通过能被加载到内存,也无法使用,因为JVM核心类是由内置类加载器加载标志和使用的,从而保证了JVM的安全加载。通过缓存类加载器和全限定包名和类名作为类唯一索引,加载重复类则抛异常提示”attempted duplicate class definition for name”。

    原理:双亲委派机制父类检查缓存,源码我们介绍loadClass方法的时候已经讲过,破坏双亲委派的自定义类加载器在加载类二进制字节码后需要调用defineClass方法,而该方法同样会从JVM方法区检索缓存类,存在的话则提示重复定义。

加载过程

至此我们已经深刻认识到类加载器的工作原理及其存在的意义,下面我们将介绍类从外部介质加载使用到卸载整个闭环的生命周期。

加载

上文花了不少的篇幅说明了类的结构和类是如何被加载到JVM内存里面的,那究竟什么时候JVM才会触发类加载器去加载外部的CLASS文件呢?通常有如下四种情况会触发到:

  • 显式字节码指令集(new/getstatic/putstatic/invokestatic):对应的场景就是创建对象或者调用到类文件的静态变量/静态方法/静态代码块

  • 反射:通过对象反射获取类对象时

  • 继承:创建子类触发父类加载

  • 入口:包含main方法的类首先被加载

JVM只定了类加载器的规范,但却不明确规定类加载器的目标文件,把加载的具体逻辑充分交给了用户,包括重硬盘加载的CLASS类到网络,中间文件等,只要加载进去内存的二进制数据流符合JVM规定的格式,都是合法的。

链接

类加载器加载完类到JVM实例的指定内存区域(方法区下文会提到)后,是使用前会经过验证,准备解析的阶段。

  • 验证:主要包含对类文件对应内存二进制数据的格式、语义关联、语法逻辑和符合引用的验证,如果验证不通过则跑出VerifyError的错误。但是该阶段并非强制执行,可以通过-Xverify:none来关闭,提高性能。

  • 准备:但我们验证通过时,内存的方法区存放的是被“紧密压缩”的数据段,这个时候会对static的变量进行内存分配,也就是扩展内存段的空间,为该变量匹配对应类型的内存空间,但还未初始化数据,也就是0或者null的值。

  • 解析:我们知道类的数据结构类似一个数据库,里面多张不同类型的“表”紧凑的挨在一起,最大的节省类占用的空间。多数表都会应用到常量池表里面的字面量,这个时候就是把引用的字面量转化为直接的变量空间。比如某一个复杂类变量字面量在类文件里只占2个字节,但是通过常量池引用的转换为实际的变量类型,需要占用32个字节。所以经过解析阶段后,类在方法区占用的空间就会膨胀,长得更像一个”类“了。

初始化

方法区经过解析后类已经为各个变量占好坑了,初始化就是把变量的初始值和构造方法的内容初始化到变量的空间里面。这时候我们介质的类二进制文件所定义的内容,已经完全被“翻译”方法区的某一段内存空间了。万事俱备只待使用了。

使用

使用呼应了我们加载类的触发条件,也即是触发类加载的条件也是类应用的条件,该操作会在初始化完成后进行。

卸载

我们知道JVM有垃圾回收机制(下文会详细介绍),不需要我们操心,总体上有三个条件会触发垃圾回收期清理方法区的空间:

  • 类对应实例被回收

  • 类对应加载器被回收

  • 类无反射引用

本节结束我们已经对整个类的生命周期烂熟于胸了,下面我们来介绍类加载机制最核心的几种应用场景,来加深对类加载技术的认识。

3. 应用场景

通过前文的剖析我们已经非常清楚类加载器的工作原理,那么我们该如何利用类加载器的特点,最大限度的发挥它的作用呢?

热部署

背景

热部署这个词汇我们经常听说也经常提起,但是却很少能够准确的描述出它的定义。说到热部署我们第一时间想到的可能是生产上的机器更新代码后无需重启应用容器就能更新服务,这样的好处就是服务无需中断可持续运行,那么与之对应的冷部署当然就是要重启应用容器实例了。还有可能会想到的是使用IDE工具开发时不需要重启服务,修改代码后即时生效,这看起来可能都是服务无需重启,但背后的运行机制确截然不同,首先我们需要对热部署下一个准确的定义。

  • 热部署(Hot Deployment):热部署是应用容器自动更新应用的一种能力。

首先热部署应用容器拥有的一种能力,这种能力是容器本身设计出来的,跟具体的IDE开发工具无关。而且热部署无需重启服务器,应用可以保持用户态不受影响。上文提到我们开发环境使用IDE工具通常也可以设置无需重启的功能,有别于热部署的是此时我们应用的是JVM的本身附带的热替换能力(HotSwap)。热部署和热替换是两个完全不同概念,在开发过程中也常常相互配合使用,导致我们很多人经常混淆概念,所以接下来我们来剖析热部署的实现原理,而热替换的高级特性我们会在下文字节码增强的章节中介绍。

原理

从热部署的定义我们知道它是应用容器蕴含的一项能力,要达到的目的就是在服务没有重启的情况下更新应用,也就是把新的代码编译后产生的新类文件替换掉内存里的旧类文件。结合前文我们介绍的类加载器特性,这似乎也不是很难,分两步应该可以完成。由于同一个类加载器只能加载一次类文件,那么新增一个类加载器把新的类文件加载进内存。此时内存里面同时存在新旧的两个类(类名路径一样,但是类加载器不一样),要做的就是如何使用新的类,同时卸载旧的类及其对象,完成这两步其实也就是热部署的过程了。也即是通过使用新的类加载器,重新加载应用的类,从而达到新代码热部署。

实现

理解了热部署的工作原理,下面通过一系列极简的例子来一步步实现热部署,为了方便读者演示,以下例子我尽量都在一个java文件里面完成所有功能,运行的时候复制下去就可以跑起来。

  • 实现自定义类加载器

参考4.2.2中自定义类加载器区别系统默认加载器的案例,从该案例实践中我们可以将相同的类(包名+类名),不同”版本“(类加载器不一样)的类同时加载进JVM内存方法区。

  • 替换自定义类加载器

既然一个类通过不同类加载器可以被多次加载到JVM内存里面,那么类的经过修改编译后再加载进内存。有别于上一步给出的例子只是修改对象的值,这次我们是直接修改类的内容,从应用的视角看其实就是应用更新,那如何做到在线程运行不中断的情况下更换新类呢?

下面给出的也是一个很简单的例子,ClassReloading启动main方法通过死循环不断创建类加载器,同时不断加载类而且执行类的方法。注意new MyClassLoader(“target/classes”)的路径更加编译的class路径来修改,其他直接复制过去就可以执行演示了。

package com.zooncool.example.theory.jvm;
import java.io.FileInputStream;
import java.lang.reflect.InvocationTargetException;
public class ClassReloading {
   public static void main(String[] args)
       throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InstantiationException,
       InvocationTargetException, InterruptedException {
       for (;;){//用死循环让线程持续运行未中断状态
           //通过反射调用目标类的入口方法
           String className = "com.zooncool.example.theory.jvm.ClassReloading$User";
            Class<?> target = new MyClassLoader("target/classes").loadClass(className);
           //加载进来的类,通过反射调用execute方法
           target.getDeclaredMethod("execute").invoke(targetClass.newInstance());
           //HelloWorld.class.getDeclaredMethod("execute").invoke(HelloWorld.class.newInstance());
           //如果换成系统默认类加载器的话,因为双亲委派原则,默认使用应用类加载器,而且能加载一次
           //休眠是为了在删除旧类编译新类的这段时间内不执行加载动作
           //不然会找不到类文件
           Thread.sleep(10000);
       }
   }
   //自定义类加载器加载的目标类
   public static class User {
       public void execute() throws InterruptedException {
           //say();
           ask();
       }
       public void ask(){
           System.out.println("what is your name");
       }
       public void say(){
           System.out.println("my name is lucy");
       }
   }
   //下面是自定义类加载器,跟第一个例子一样,可略过
   public static class MyClassLoader extends ClassLoader{
       ...
   }
}

ClassReloading线程执行过程不断轮流注释say()和ask()代码,然后编译类,观察程序输出。

如下输出结果,我们可以看出每一次循环调用都新创建一个自定义类加载器,然后通过反射创建对象调用方法,在修改代码编译后,新的类就会通过反射创建对象执行新的代码业务,而主线程则一直没有中断运行。读到这里,其实我们已经基本触达了热部署的本质了,也就是实现了手动无中断部署。但是缺点就是需要我们手动编译代码,而且内存不断新增类加载器和对象,如果速度过快而且频繁更新,还可能造成堆溢出,下一个例子我们将增加一些机制来保证旧的类和对象能被垃圾收集器自动回收。

what is your name
what is your name
what is your name//修改代码,编译新类
my name is lucy
my name is lucy
what is your name//修改代码,编译新类
 
  • 回收自定义类加载器

通常情况下类加载器会持有该加载器加载过的所有类的引用,所有如果类是经过系统默认类加载器加载的话,那就很难被垃圾收集器回收,除非符合根节点不可达原则才会被回收。

下面继续给出一个很简单的例子,我们知道ClassReloading只是不断创建新的类加载器来加载新类从而更新类的方法。下面的例子我们模拟WEB应用,更新整个应用的上下文Context。下面代码本质上跟上个例子的功能是一样的,只不过我们通过加载Model层、DAO层和Service层来模拟web应用,显得更加真实。

package com.zooncool.example.theory.jvm;
import java.io.FileInputStream;
import java.lang.reflect.InvocationTargetException;
//应用上下文热加载
public class ContextReloading {
   public static void main(String[] args)
       throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InstantiationException,
       InvocationTargetException, InterruptedException {
       for (;;){
           Object context = new Context();//创建应用上下文
           invokeContext(context);//通过上下文对象context调用业务方法
           Thread.sleep(5000);
       }
   }
   //创建应用的上下文,context是整个应用的GC roots,创建完返回对象之前调用init()初始化对象
   public static Object newContext()
       throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException,
       InvocationTargetException {
       String className = "com.zooncool.example.theory.jvm.ContextReloading$Context";
       //通过自定义类加载器加载Context类
        Class<?> contextClass = new MyClassLoader("target/classes").loadClass(className);
       Object context = contextClass.newInstance();//通过反射创建对象
       contextClass.getDeclaredMethod("init").invoke(context);//通过反射调用初始化方法init()
       return context;
   }
   //业务方法,调用context的业务方法showUser()
   public static void invokeContext(Object context)
       throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
       context.getClass().getDeclaredMethod("showUser").invoke(context);
   }
   public static class Context{
       private UserService userService = new UserService();
       public String showUser(){
           return userService.getUserMessage();
       }
       //初始化对象
       public void init(){
           UserDao userDao = new UserDao();
           userDao.setUser(new User());
           userService.setUserDao(userDao);
       }
   }
   public static class UserService{
       private UserDao userDao;
       public String getUserMessage(){
           return userDao.getUserName();
       }
       public void setUserDao(UserDao userDao) {
           this.userDao = userDao;
       }
   }
   public static class UserDa
						 
						                      
					
           
                    
  • 发表于 2020-04-05 22:46
  • 阅读 ( 136 )
  • 分类:网络文章

条评论

请先 登录 后评论
不写代码的码农
小编

篇文章

作家榜 »

  1. 小编 文章
返回顶部
部分文章转自于网络,若有侵权请联系我们删除