详解Java类动态加载和热替换

前言

最近,遇到了两个和Java类的加载和卸载相关的问题:

1) 是一道关于Java的判断题:一个类被首次加载后,会长期留驻JVM,直到JVM退出。这个说法,是不是正确的?

2) 在开发的一个集成平台中,需要集成类似接口的多种工具,并且工具可能会有新增,同时在不同的环境部署会有裁剪(例如对外提供服务的应用,不能提供特定的采购的工具),如何才能更好地实现?

针对上面的第2点,我们采用Java插件化开发实现。上面的两个问题,都和Java的类加载和热替换机制有关。

1. Java的类加载器和双亲委派模型

1.1 Java类加载器

类加载器,顾名思义,就是用来实现类的加载操作。每个类加载器都有一个独立的类名称空间,就是说每个由该类加载器加载的类,都在自己的类名称空间,如果要比较两个类是否“相等”,首先这两个类必须在相同的类命名空间,即由相同的类加载器加载(即对于任何一个类,都必须由该类本身和加载它的类加载器一起确定其在JVM中的唯一性),不是同一个类加载器加载的类,不会相等。

在Java中,主要有如下的类加载器:

下面,简单介绍上面这几种类加载器:

  • 启动类加载器(Bootstrap Class Loader):这个类使用C++开发(所有的类加载器中,唯一使用C++开发的类加载器),用来加载<JAVA_HOME>/lib目录中jar和tools.jar或者使用 -Xbootclasspath 参数指定的类。
  • 扩展类加载器(Extension Class Loader):定义为misc.Launcher$ExtClassLoader,用来加载<JAVA_HOME>/lib/ext目录或者使用java.ext.dir指定的类。
  • 应用程序类加载器(Application Class Loader):定义为misc.Launcher$AppClassLoader,用来加载用户类路径下面(classpath)下面所有的类,一般情况下,该类是应用程序默认的类加载器。
  • 用户自定义类加载器(User Class Loader):用户自定义类加载器,一般没有必要,后面我们会专门来一部分介绍该类型的类加载器。

1.2 双亲委派模型

双亲委派模型,是从 Java1.2 开始引入的一种类加载器模式,在Java中,类的加载操作通过java.lang.ClassLoader中的loadClass()方法完成,咱们首先看看该方法的实现(直接从Java源码中捞出来的):

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 {
                        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);

                    // 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;
        }
    }

我们结合上面的注释,来解释下双亲委派模型的内容:

1) 接收到一个类加载请求后,首先判断该类是否有加载,如果已经加载,则直接返回;

2) 如果尚未加载,首先获取父类加载器,如果可以获取父类加载器,则调用父类的loadClass()方法来加载该类,如果无法获取父类加载器,则调用启动器加载器来加载该类;

3) 判断该类是否被父类加载器或者启动类加载器加载,如果已经加载完成则返回,如果未成功加载,则自己尝试来加载该类。

上面的描述,说明了loadClass()方法的实现,我们进一步对上面的步骤进行解释:

  • 因为类加载器首先调父类加载器来进行加载,从loadClass()方法的实现,我们知道父类加载器会尝试调自己的父类加载器,直到启动类加载器,所以,任何一个类的加载,都会最终委托到启动类加载器来首先加载;
  • 在前面有进行介绍,启动类加载器、扩展类加载器、应用程序类加载器,都有自己加载的类的范围,例如启动类加载器只加载JDK核心库,因此并不是父类加载器就可以都加载成功,父类加载器无法加载(一般如上面代码,抛出来ClassNotFoundException),此时会由自己加载。

最后啰嗦一下,再进行一下总结:

双亲委派模型:如果一个类加载器收到类加载请求,会首先把加载请求委派给父类加载器完成,每个层次的类加载器都是这样,最终所有的加载请求都传动到最根的启动类加载器来完成,如果父类加载器无法完成该加载请求(即自己加载的范围内找不到该类),子类加载器才会尝试自己加载。

这样的双亲委派模型有个好处:就是所有的类都尽可能由顶层的类加载器加载,保证了加载的类的唯一性,如果每个类都随机由不同的类加载器加载,则类的实现关系无法保证,对于保证Java程序的稳定运行意义重大。

2. Java的类动态加载和卸载

2.1 Java类的卸载

在Java中,每个类都有相应的Class Loader,同样的,每个实例对象也会有相应的类,当满足如下三个条件时,JVM就会卸载这个类:

1) 该类所有实例对象不可达

2) 该类的Class对象不可达

3) 该类的Class Loader不可达

那么,上面示例对象、Class对象和类的Class Loader直接是什么关系呢?

在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。而一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。所以,Class实例和加载它的加载器之间为双向引用关系。

一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。

Java虚拟机自带的类加载器(前面介绍的三种类加载器)在JVM运行过程中,会始终存在,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。因此,由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。

那么,我们是不是就完全不能在Java程序运行过程中,动态修改我们使用的类了吗?答案是否定的!根据上面的分析,通过Java虚拟机自带的类加载器加载的类无法卸载,我们可以自定义类加载器来加载Java程序,通过自定义类加载器加载的Java类,是可以被卸载的。

2.2 自定义类加载器

前面介绍到,类加载的双亲委派模型,是推荐模型,在loadClass中实现的,并不是必须使用的模型。我们可以通过自定义类加载器,直接加载我们需要的Java类,而不委托给父类加载器。

如上图所示,我们有自定义的类加载器MyClassLoader,用来加载类MyClass,则在JVM中,会存在上面三类引用(上图忽略这三种类型对象对其他的对象的引用)。如果我们将左边的三个引用变量,均设置为null,那么此时,已经加载的MyClass将会被卸载。

2.3 动态卸载存在的问题

动态卸载需要借助于JVM的垃圾收集功能才可以做到,但是我们知道,JVM的垃圾回收,只有在堆内存占用比较高的时候,才会触发。即使我们调用了System.gc(),也不会立即执行垃圾回收操作,而只是告诉JVM需要执行垃圾回收,至于什么时候垃圾回收,则要看JVM自己的垃圾回收策略。

但是我们不需要悲观,即使动态卸载不是那么牢靠,但是实现动态的Java类的热替换还是有希望的。

3. Java类的热替换

下面通过代码来介绍Java类的热替换方法(代码简陋,主要为了说明问题):

如下面的代码:

首先定义一个自定义类加载器:

package zmj;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class FileClassLoader extends ClassLoader {
    private String fileName;

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public Class loadClass(String name) throws ClassNotFoundException {
        if (name.startsWith("java")) {
            return getSystemClassLoader().loadClass(name);
        }
        Class cls = null;
        File classF = new File(fileName);
        try {
            cls = instantiateClass(name, new FileInputStream(classF), classF.length());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return cls;
    }

    private Class instantiateClass(String name, InputStream fin, long len) throws IOException {
        byte[] raw = new byte[(int) len];
        fin.read(raw);
        fin.close();
        return defineClass(name, raw, 0, raw.length);
    }
}

上面在loadClass时,先判断类name(包含package的全限定名)是否以java开始,如果是java开始,则使用JVM自带的类加载器加载。

然后定义一个简单的动态加载类:

package zmj;

public class SayHello {
    public void say() {
        System.out.println("hello ping...");
    }
}

在执行过程中,会动态修改打印内容,测试类的热加载。

然后定义一个调用类:

package zmj;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws InterruptedException, ClassNotFoundException,
            IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        while (true) {
            FileClassLoader fileClassLoader = new FileClassLoader();
            fileClassLoader.setFileName("D:/workspace/idea/test/class-loader-test/target/classes/zmj/SayHello.class");
            Object obj = null;
            obj = fileClassLoader.loadClass("zmj.SayHello").newInstance();
            Method m = obj.getClass().getMethod("say", new Class[]{});
            m.invoke(obj, new Object[]{});
            Thread.sleep(2000);
        }
    }
}

当我们运行上面Main程序过程中,我们动态修改执行内容(SayHello中,从 hello zmj... 更改为 hello ping...),最终展示的内容如下:

hello zmj...

hello zmj...

hello zmj...

hello ping...

hello ping...

hello ping...

以上就是详解Java类动态加载和热替换的详细内容,更多关于Java类动态加载和热替换的资料请关注我们其它相关文章!

(0)

相关推荐

  • java实现动态编译并动态加载

    在D盘test目录下有个java文件:AlTest.java public class AlTest { public String sayHello(){ System.out.println("AlTest类 sayHello()方法正在执行...."); return "hello word"; } } 现需要实现在工程已经运行过程中,进行java文件到class文件的编译操作,并运行AlTest类的方法 package com.piao.job; impor

  • java 利用java反射机制动态加载类的简单实现

    如下所示: ////////////////// Load.java package org.bromon.reflect; import java.util.ArrayList; import java.util.List; public class Load implements Operator { @Override public List<?> act(List<?> params) { // TODO Auto-generated method stub List<

  • 详解Java动态加载数据库驱动

    问题背景 在同一套系统中,要支持连接访问各种流行的数据库,以及同一数据库的不同版本,例如,oracle9i.oracle10g.oracle11g.oracle12c.sqlserver2000.sqlserver2005.sqlserver2008.sqlserver2012等,其中就会碰到一些问题,就是不同的数据库,数据库驱动肯定不同,对于这个问题到好解决,只需要将相应的驱动加入即可:然而对于同种数据库,不同版本时,而且不同版本的数据库驱动不仅不兼容,同时存在还会出现冲突,例如,能满足sql

  • Java 动态加载jar和class文件实例解析

    本文研究的主要是Java 动态加载jar和class文件的相关内容,具体如下. JAVA中类文件加载是动态的.也就是说当我们用到的时候才会去加载,如果不用的话,就不会去加载我们的类. JAVA为我们提供了两种动态机制.第一种是隐式机制.第二种是显示机制.如下: 两种方法: 隐式机制 :new一个对象 + 调用类的静态方法 显式机制 :由 java.lang.Class的forName()方法加载 由 java.lang.ClassLoader的loadClass()方法加载 1.Class.fo

  • Java反射之静态加载和动态加载的简单实例

    静态加载: package com.imooc.加载类; public class Office_Static { public static void main(String[] args) { //new 创建对象,是静态加载类,在编译时刻就需要加载所有的可能使用到的类 if("Word".equals(args[0])){ Word w = new Word(); w.start(); } if("Excel".equals(args[0])){ Excel

  • java动态加载插件化编程详解

    前言 对于java程序员来说,插件化是一件很酷的功能,小二有幸在工作中实现了此功能. 背景: 需要将mysql的数据通过canal同步至kafka/mysql/hdfs等 实现 /** * Created by shengjk1 on 2017/12/11 */ public class PluginManager { private final static Logger logger = LoggerFactory.getLogger(SendMessageFactory.class); p

  • Java动态加载类示例详解

    在讲解动态加载类之前呢,我们先弄清楚为什么要动态加载类,静态加载不行吗?我们可以看下面的实例: 我在文件夹里写了Office.java 类和 Word.java类,如下: Office.java class Office{ public static void main(String[] args){ if(args[0].equals("Word")){ Word w = new Word(); w.start(); } if(args[0].equals("Excel&q

  • java 动态加载的实现代码

    java 动态加载的实现代码 Java动态加载类的意义和目的: Java动态加载类主要是为了不改变主程序代码,通过修改配置文件就可以操作不同的对象执行不同的功能.主要有利于系统的扩展,例如当我要改变一个功能,只需要做一个类,然后编写相应的功能,通过配置文件就可以使用新的功能,不需要修改系统的任何地方,只需要添加一个类:充分实现了松散耦合.满足了开闭原则(对修改关闭,对添加或删除开放): public abstract class AbstractAction { public abstract

  • Java窗体动态加载磁盘文件的实现方法

    在使用图形界面操作系统时,当打开一个文件夹系统会自动列出该文件夹下的所有文件及子文件夹.本实例实现了类似的功能:首先让用户选择一个文件夹,程序会动态列出该文件夹下的所有文件:如果该文件是隐藏文件,就在属性栏中显示"隐藏文件".要用到进度条来表示是否完成. 思路如下: 首先是视图层.JTextField.JButton.JScrollPane.JTable神马的就不用再提了,都是老盆友了,在这里因为要用到进度条,所以会有一个新盆友:JProgressBar控件.控件们自然是要作为成员变量

  • 详解Java类动态加载和热替换

    前言 最近,遇到了两个和Java类的加载和卸载相关的问题: 1) 是一道关于Java的判断题:一个类被首次加载后,会长期留驻JVM,直到JVM退出.这个说法,是不是正确的? 2) 在开发的一个集成平台中,需要集成类似接口的多种工具,并且工具可能会有新增,同时在不同的环境部署会有裁剪(例如对外提供服务的应用,不能提供特定的采购的工具),如何才能更好地实现? 针对上面的第2点,我们采用Java插件化开发实现.上面的两个问题,都和Java的类加载和热替换机制有关. 1. Java的类加载器和双亲委派模

  • 详解Java 类的加载、连接和初始化

    系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类.本节将会详细介绍类加载.连接和初始化过程中的每个细节. JVM 和类 当调用 java 命令运行某个 Java 程序时,该命令将会启动一个 Java 虚拟机进程,不管该 Java 程序有多么复杂,该程序启动了多少个线程,它们都处于该 Java 虚拟机进程里.正如前面介绍的,同一个 JVM 的所有线程.所有变量都处于同一个进程里,它们都使用该 JVM 进程的内存区.当系统出现以下几种情况时,JVM 进程将被终止. 程序运行到最

  • 详解Java 类的加载机制

    一.类的加载机制 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构.类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接

  • 详解Java中类的加载顺序

    本文介绍的是Java中类的加载顺序,下面来看看详细的介绍: 1.虚拟机在首次加载Java类时,会对静态初始化块.静态成员变量.静态方法进行一次初始化 2.只有在调用new方法时才会创建类的实例 3.类实例创建过程:按照父子继承关系进行初始化,首先执行父类的初始化块部分,然后是父类的构造方法:再执行本类继承的子类的初始化块,最后是子类的构造方法 4.类实例销毁时候,首先销毁子类部分,再销毁父类部分 示例 public class Parent { public static int t = par

  • 详解webpack import()动态加载模块踩坑

    import webpack根据ES2015 loader 规范实现了用于动态加载的import()方法. 这个功能可以实现按需加载我们的代码,并且使用了promise式的回调,获取加载的包. 在代码中所有被import()的模块,都将打成一个单独的包,放在chunk存储的目录下.在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载. 这里是一个简单的demo. import('lodash').then(_ => { // Do something with lodash (a.k.

  • 详解VueJs异步动态加载块

    首先定义组件为异步加载 define(['jquery','vue'],function($,Vue){ Vue.component('comp1',function(resolve){ require(['component/comp1'],resolve); }); Vue.component('comp2',function(resolve){ require(['component/comp2'],resolve); }); var b = new Vue({ el:"#app"

  • classloader类加载器_基于java类的加载方式详解

    基础概念 Classloader 类加载器,用来加载 Java 类到 Java 虚拟机中.与普通程序不同的是.Java程序(class文件)并不是本地的可执行程序.当运行Java程序时,首先运行JVM(Java虚拟机),然后再把Java class加载到JVM里头运行,负责加载Java class的这部分就叫做Class Loader. JVM本身包含了一个ClassLoader称为Bootstrap ClassLoader,和JVM一样,BootstrapClassLoader是用本地代码实现

  • 详解java JDK 动态代理类分析(java.lang.reflect.Proxy)

    详解java JDK 动态代理类分析(java.lang.reflect.Proxy) /** * JDK 动态代理类分析(java.lang.reflect.Proxy使用) * * @author 张明学 * */ public class ProxyStudy { @SuppressWarnings("unchecked") public static void main(String[] args) throws Exception { // 动态代理类:通用指定类加载器,和接

  • 详解Java JDK动态代理

    今天来看看Java的另一种代理方式--JDK动态代理 我们之前所介绍的代理方式叫静态代理,也就是静态的生成代理对象,而动态代理则是在运行时创建代理对象.动态代理有更强大的拦截请求功能,因为可以获得类的运行时信息,可以根据运行时信息来获得更为强大的执(骚)行(操)力(作). 我们还是以上一个例子为例,这里的IStars接口和Stars类都不需要修改,只需要修改代理类. 创建JDK动态代理需要先实现InvocationHandler接口,并重写其中的invoke方法,具体步骤如下: 1. 创建一个类

  • Java类的加载连接和初始化实例分析

    本文实例讲述了Java类的加载连接和初始化.分享给大家供大家参考,具体如下: 一 点睛 1 类加载 当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载.连接.初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化. 类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说当程序使用任何类时,系统都会为之建立一个java.lang.Class对象. 2 类数据的来源 通过

随机推荐