大家线上Java应用的热更新都是怎么实现的?

热更新的时候需要注意哪些点呢?
下面是从网上找到的两种解决方法:

解决方案一 : 自定义类加载器。

首先需要明白一点,class相等的判断条件不仅仅是类名相同,还需要加载它的ClassLoader相同。JVM内部规定一个ClassLoader不可以重复定义类,也就是说想要重定义一个类,就必须使用一个全新的ClassLoader。

JVM内部class被卸载的条件及其苛刻,甚至没有明确的方法可以直接调用,只有当加载该类型的类加载器实例为unreachable状态时,也就是没有任何实例,class才有可能被卸载。(启动类加载器实例永远为reachable状态,由启动类加载器加载的类型可能永远不会被卸载)

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = null;
        // 首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则继续。
        if (name.startsWith("com.wafer") || name.contains("Service")) {
            if (resolve) {
                resolveClass(clazz); // 链接指定的 Java 类
            }
            // 如果class类被修改过,则重新加载
            MoeLoader hcl = new MoeLoader(url);
            clazz = customLoad(name, hcl);
            return (clazz);
        }
        // 如果类的包名为"java."开始,则有系统默认加载器加载
        try {
            // 得到系统默认的加载cl
            ClassLoader system = ClassLoader.getSystemClassLoader();
            clazz = system.loadClass(name); // 加载名称为 name的类
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
        return customLoad(name, this);
        }


此范例的核心在于缓存自己已经加载的class,当再次需要加载时,如果发生变更,则可以new一个ClassLoader,这样新的字节码便可以即时生效。

JRbel是一种热更新的方案,它实现的方式是通过在启动参数中添加javaagent,即JVM底层提供的Instrumentation技术,来改变生成对象的方式。

解决方法二:
java.lang.instrument这个类很早就出了,redefineClasses这个方法可以更新方法级别的代码,但是不会触发一个类的初始化方法。游戏服务器的bug基本上只需要方法级别的更新就可以了,因为很多重大的bug基本在测试阶段被修复了,少量偶线的bug出现之后有些时候仅仅只需要改动一行代码却有时不得不需要重启所有应用程序,代价都似乎有点大。
现在开始从instrument入手

public static void premain(String agentArgs, Instrumentation inst);

public static void agentmain(String agentArgs, Instrumentation inst);

这两个方法都可以获取到Instrumentation对象,通过redefineClasses方法就可以更新对应的方法了
如果是调用premain这个方法 则需要在程序启动的时候指定对应的jar 同时项目里必须引用这个jar 因为获取到这个引用
java -javaagent:agent.jar -jar xx.jar 例如这样 执行这条命令后程序会查找 agent.jar 里的MANIFEST.MF文件里的Premain-Class参数 设置对应的代理类路径就行。例如:Premain-Class: com.test.JavaAgent 还需要加上 Can-Redefine-Classes: true这个参数才能调用redefineClasses方法。同时 可以拦截对应的类添加标记 做性能分析
agentmain 是通过指定对应的进程pid来加载对应的agent.jar 很典型的jconsule jvisualvm都是通过选择java进程来做一个简单的内存 和cpu分析 ,线程dump .Agent-Class 和上面一样

package com.test;

import java.lang.instrument.Instrumentation;

public class JavaAgent {
    public static Instrumentation INST = null;

    public static void premain(String agentArgs, Instrumentation inst){
        INST = inst;
    }
}

这里保存下引用就可以了 ,单独打成agent.jar

package com.test;

import java.io.FileInputStream;
import java.lang.instrument.ClassDefinition;

public class Test {

    public static void main(String[] args) {
        getInfo();
        testhot();
    }

    public final static void testhot(){
        new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                while(true){
                    try {
                    if(JavaAgent.INST != null){
                        FileInputStream is = new FileInputStream("/Users/xxxx/Downloads/Student.class");
                        byte[] array = new byte[is.available()];
                        is.read(array);
                        is.close();
                        Class cls = Class.forName("com.test.Student");
                        JavaAgent.INST.redefineClasses(new ClassDefinition(cls,array));
                    }
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }




    public final static void getInfo(){
        new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                while(true){
                    //System.out.println("=============="+JavaAgent.INST);
                    new Student().getName();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
        }).start();

    }
}

上面就是一个很简单的例子,一个线程在不停的循环检测更新这个类,另外的一个线程在不停的输出这个对象对应的方法输出信息。
测试之后可以发现 ,方法的输出信息已经改变了。

2个回答

部署两个应用负载均衡,部署时停掉其中一个更新另一个。这个启动了再更新另一个,中间始终有一个提供服务

如果你是单点部署的话,就用你找的这种方法。如果你是集群部署,就按照楼上说的,部署一台服务器启动一台,没有部署的可以一直处于运行状态。

sinat_32430939
油焖大虫下 要是是socket长连接就不能用集群的方法了吧
22 天之前 回复
Csdn user default icon
上传中...
上传图片
插入图片
抄袭、复制答案,以达到刷声望分或其他目的的行为,在CSDN问答是严格禁止的,一经发现立刻封号。是时候展现真正的技术了!
立即提问