Java热更新失败常见原因总结

Arthas Retransform热更新实战:从插件到命令行的避坑指南 本文详细解析了Arthas的retransform命令如何实现Java应用热更新,提供了通过Arthas Idea插件和原生命令行两种实战路径的完整操作指南。重点剖析了导致retransform失败的常见陷阱,如JDK版本不一致、类结构变更、Lombok注解副作用等,并给出了具体的避坑方案和排查思路,帮助开发者安全高效地掌握这项线上调试与紧急修复的关键技术。 阅读详情

前言

热更新是Java开发者经常需要考虑的一个问题,无论是游戏还是互联网应用,都需要尽量做到运行时代码修复,以避免重启给用户体验带来的负面影响。目前主流的热更新方案是基于Java的Attach和Instrumentation API。热更新时需要满足不改变方法签名或者类的字段。在普通情况下我们比较容易通过diff看出是否有上述改动,但是在一些特殊情况下失败原因却藏得很深。本文就是通过总结这些特殊情况,避免大家踩坑。

基础知识

无论哪种热更新方案都离不开Java本身的底层支持。Java提供了JVMTI(Java Virtual Machine Tool Interface),作为底层工具来支持对Java程序做调试和监控。目前主流的热更新方案就是基于其中的Attach和Instrumentation API。这两个功能在Java 6中引入,Attach提供了从外部连接到JVM并执行代码的功能,而Instrumentaion能够在运行时改变类的运行逻辑。

下面一起看下实现热更新的具体逻辑:

此处主要是让大家对热更新的流程有一个直观的认识,因此对于细节不做过多展开,需要代码实现的读者可自行搜索相关文章。

  1. 从外部连接到Java进程上:调用VirutalMachine.attach(pid)。
  2. 加载代理jar包:调用loadAgent(jar, agentArgs),注意此处必须是jar包形式而不能是class文件。
  3. 调用agentmain方法:代理类中需包含agentmain方法,该方法会作为代理类的入口方法,在连接到对象JVM后立即执行。
  4. 这里有两种实现热更新方法:
    4.1 调用Instrumentation类的redefineClasses()方法:该方法可用于重定义一个类的实现,它是通过从流读取的形式来更新,因此可以将新的class文件加载到字节流,以实现字节码在类加载器中的替换。
    4.2 调用Transformer类的retransformClasses()方法:Transformer也叫拦截器,它可以自定义拦截行为,来实现字节码的增强或替换。触发时机是在类加载时或者调用retransformClasses()主动触发。

通过Transformer类实现热更新的具体流程是:

  1. 实现自己的Transformer类:在transform()方法中自定义处理逻辑,比如ASM会在其中加入字节码增强的逻辑,当然你也可以加入热更新的逻辑,替换原始的字节码。
  2. 调用addTransformer()方法添加拦截器。
  3. 调用retransformClasses()主动触发拦截器。

通过这种方法实现热更新需满足条件限制。如存在以下情况其中之一,则热更新会失败:

  1. 修改了方法签名:包括增减方法,或者是修改方法的参数列表,也就是说修改只能来自于方法内部。
  2. 修改了类中字段:包括增减类中字段,或者修改字段类型。

当热更新失败时,会看到类似如下的异常:

java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method

常见失败原因

通常我们都能通过diff前后版本判断热更新能否成功,但是在一些特殊情况下失败原因却藏得很深。它们本质上都是违反了上述限制,只不过因为代码嵌套或者编译器黑魔法的关系,使了个障眼法让我们疏忽。借此机会,我们正好也可以学习一些Java编译相关的底层知识。Let’s go!

Stream中新增filter()

在业务逻辑中使用Stream和Lambda表达式可以让代码更加精简并提升可读性。用起来很爽,可要热更新时却经常会失败,这时就傻眼了。一种典型的情况是,在Stream序列中我们需要添加一个filter()。失败的原因是方法的底层实际上是增加了一个匿名内部类。

    @Override
    public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
        Objects.requireNonNull(predicate);
        return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
                                     StreamOpFlag.NOT_SIZED) {
            @Override
            Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
                return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
                    @Override
                    public void begin(long size) {
                        downstream.begin(-1);
                    }

                    @Override
                    public void accept(P_OUT u) {
                        if (predicate.test(u))
                            downstream.accept(u);
                    }
                };
            }
        };
    }

从源码中看出,filter()方法底层增加了一个 StatelessOp类型的匿名内部类。这个新的类显示没有办法被动态加载,那调用它的外部类理所当然也会热更新失败了。
事实上,不只filter(),Stream提供的大部分操作方法底层都会涉及匿名内部类的添加,因此想通过热更新给Stream流添加一个新的处理操作是非常不可靠的。不过Stream毕竟只是一个语法糖,我们总可以找到另外的实现途径,用普通遍历和条件判断实现同样的效果。

增加Lambda表达式

当我们满怀希望地试图通过热更新增加一个Lambda表达式时,总会被现实无情地泼一桶冷水。咋一看百思不得其解,搞不清哪里违反了热更新条件。这时候我们就需要祭出一大利器了——javap!

反汇编利器——javap

Oracle官网上对javap的介绍是JDK自带的反汇编工具,实际上它也有反编译的功能。
反汇编、反编译是容易搞混的两个概念,为此我制作了下面的图以便详细说明:
在这里插入图片描述

  1. 反编译:是相对于编译的反操作,是将字节码(class文件)重新转成源代码(Java代码)。
  2. 反汇编:是指把机器码或字节码转换成人类可读的形式。机器码和字节码有个共同点,就是都不是人类可读的,而反汇编可以将其转换成基础的操作指令序列。不同于在具体平台执行的机器码,Java的字节码可被视作一种虚拟的中间状态的机器码,JVM通过提供一个公共的字节码指令集合,屏蔽了不同平台的差异性。javap提供的反汇编功能,就是指把不可读的字节码转换成可读的字节码指令序列。我们可以借此一窥Java在编译过程中的奥秘。

小试牛刀

我们通过一个例子来展示javap的用法。
先编写如下的一个类:

public class LambdaTest {
	
	private void func() {

	}
	
    public static void main(String args[]) {
    	new LambdaTest().func();
    }

}

javap 默认的是只展示非private的属性和方法,因此为了显示private方法,我们要加上选项 -p:

// javap -p LambdaTest.class 
public class leetcode.LambdaTest {
  public leetcode.LambdaTest();
  private void func();
  public static void main(java.lang.String[]);
}

以上是反编译的内容,如果需要展示反汇编的内容,我们需要加上-c,这样就可以看到每个方法内部具体调用的指令集:

// javap -c -p LambdaTest.class
public class leetcode.LambdaTest {
  public leetcode.LambdaTest();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  private void func();
    Code:
       0: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #1                  // class leetcode/LambdaTest
       3: dup
       4: invokespecial #17                 // Method "<init>":()V
       7: invokespecial #18                 // Method func:()V
      10: return
}

一探究竟

我们在LambdaTest类中加入一条Lambda语句:

import java.util.ArrayList;
import java.util.List;

public class LambdaTest {
	
	private void func() {
    	List<Integer> list = new ArrayList<>();
    	list.stream().filter(i -> i > 0);
	}
	
    public static void main(String args[]) {
    	new LambdaTest().func();
    }
    


}

然后用javap看看发生了什么:

// javap -p LambdaTest.class
public class leetcode.LambdaTest {
  public leetcode.LambdaTest();
  private void func();
  public static void main(java.lang.String[]);
  private static boolean lambda$0(java.lang.Integer);
}

原来Java在编译过程中,会把Lambda表达式转换成一个static方法 :lambda$0()。这个方法的参数和返回值正好与Lambda表达式所代表的函数完美匹配。由于存在新增方法,所以热更新怪不得会失败了。

外部类使用内部类的private字段或方法

这也是一种容易被忽略,但是会造成热更新失败的情况。其实反过来,内部类使用外部类的private字段或方法,也会导致热更失败。原理类似,所以我们只挑前一种情况加以分析说明。

我们在LambdaTest中加入一个内部类:

public class LambdaTest {
	
	private void func() {
	}
	
    public static void main(String args[]) {
    	new LambdaTest().func();
    }
    
    class InnerClass{
    	
    	private void innerFunc() {
    		
    	}
    }

}

可以用javap看到内部类中包含对外部类的引用this 0 (注意在 0(注意在 0(注意在InnerClass前要加\,否则会解析失败):

// javap -p LambdaTest\$InnerClass.class
class leetcode.LambdaTest$InnerClass {
  final leetcode.LambdaTest this$0;
  leetcode.LambdaTest$InnerClass(leetcode.LambdaTest);
  private void innerFunc();
}

然后我们在外部类的方法func()中调用内部类的方法innerFunc():

public class LambdaTest {
	
	private void func() {
		InnerClass inner = new InnerClass();
		inner.innerFunc();
	}
	
    public static void main(String args[]) {
    	new LambdaTest().func();
    }
    
    class InnerClass{
    	
    	private void innerFunc() {
    		
    	}
    }

}

再用javap看看发生了什么:

// javap -p LambdaTest\$InnerClass.class
class leetcode.LambdaTest$InnerClass {
  final leetcode.LambdaTest this$0;
  leetcode.LambdaTest$InnerClass(leetcode.LambdaTest);
  private void innerFunc();
  static void access$0(leetcode.LambdaTest$InnerClass);
}

神奇的事情出现了!反编译的结果是多了个access$0()方法。原来为了不违反private的私有性,Java编译器在处理外部类引用内部类的private字段或方法时,会为内部类自动添加一个access这样的方法,再通过该方法间接调用private字段或方法。这样既没有破坏private私有性原则,也能方便地让内外部类实现private字段方法的互相调用。

因此,如果一个内部类未来有可能做热更新,那么需要在编写代码时就尽量注意,避免出现内外部类相互调用private字段或方法的情况。还有一种做法是干脆弃用内部类,因为内部类必定有与之等效的外部类的写法。不过内部类的好处是能带来更好的可读性和封装,这就需要编写者做个权衡了。

总结

本文介绍了几种常见的Java热更新失败情况,对这些情况的理解和掌握有助于读者避免踩坑。本文还介绍了相应的基础知识:包括Java热更新的原理以及反汇编利器javap。熟练掌握javap的用法,不仅有助于判断热更新能否成功,还能让我们对Java编译过程更加了解,方便排查一些底层的问题和做代码优化。

Arthas实现代码热更新 一、背景 有时我们在项目中正在运行的线上代码出了问题,由于是线上项目无法重启或更新代码,此时只能选择热更新的方式进行,但是热更新有风险,操作需谨慎。 这里:默认大家已经会使用arthas或者arthas已经安装完成,如果没有安装请点击:Arthas-java在线调试工具的使用 二、步骤 测试代码: package com.tiandy.testdemo; @SpringBootApplication public class TestDemoApplication { public static 阅读详情

相关推荐

Reactnative实现远程热更新的原理是什么

利用 RN “JS 层与原生层解耦”的架构,通过“远程下发更新包 → 本地缓存 → 切换加载路径”的流程,实现 JS 业务代码和资源的动态更新,核心目标是绕开应用商店审核,快速迭代。其限制也明确:仅能更新 JS 层,原生层变更仍需走应用商店流程。

weixin_42283818的博客 778

JAVA实现代码热更新

JAVA实现代码热更新

m0_53157173的博客 4786

Java服务依赖的5大热更新魔法:为什么你的代码还在重启?

摘要:Java热更新技术实战指南 本文深入探讨Java服务热更新技术,通过五大核心步骤实现零停机动态更新: 核心原理:类加载器隔离、依赖解耦和版本回滚三大定律,解决传统更新"开颅手术"问题 实现步骤: 自定义类加载器创建"平行宇宙" 依赖模块热加载实现"乐高积木"式替换 版本控制系统提供"时光机"回滚能力 进阶方案:结合Spring Cloud实现配置热加载,完成动态更新的"终极魔法阵" 包含完整代码示例,从

java专栏 1665

Java热更新原理与代码实现

热更新指的是项目应用在运行的过程中,修改后的代码文件无需进行应用重启就能够被JVM加载并使用。该方式能够提高开发,减少项目频繁的重启。使用IDEA的可以使用插件实现更加完善的项目热更新。热更新的实现主要是依赖加载需要进行热更新的类(加载的是.class文件),接着只需在使用该类的时候,用自定义的类加载器加载指定class文件,最后使用反射调用对应的方法。当我们修改文件后,只需要重新编译该文件生成新的class文件,接着就会被自定义类加载器加载使用,实现文件的热更新。流程如下图所示: 需要定义一个自定义的类

Belove1ong的博客 3725

电赛经验分享——赛前准备

⏩ 大家好哇!我是小光,想要成为系统架构师的嵌入式爱好者。⏩在之前的电赛中取得了省一的成绩,本文对电赛比赛前需要准备什么做一个经验分享。加入小光嵌入式交流群(qq群号:737327353)免费获取博主所有资料哦!

qq_52608074的博客 7779

MFC 修改CheckBox的文本颜色方法

设置主题 这一步是必须的,不可省略。 BOOL CMFCApplication1Dlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动 // 执行此操作 SetIcon(m_hIcon, TRUE); // 设置大图标 SetIcon(m_hIcon, FALSE); // 设置小图标 // TODO: 在此添加额外的初始化代码 // 设置主题 HWND

Kim的博客 2249

JAVA热更新

引言 知识储备先看这篇文章:JAVA Instrument 在这个案例中我们会利用Instrument机制实现一个简单的热更新案例。 总体来说,步骤如下: 创建一个带permain方法的jar包。这个方法定时检测某个文件然后进行热更新。 命令行启动业务类时使用参数-javaagent,例如java -javaagent:jarpath[=选项] Main。 网上有很多案例都是用Maven打jar包的,但是这里我讲的是纯命令行的做法。 代码实现 结构如下 这个是编译后的结构,这里先从逻辑实现说起。 1.

与望 4242

告别TI官网迷路!手把手教你用C2000Ware为TMS320F28027搭建Proteus仿真工程模板

本文详细指导如何使用C2000Ware为TMS320F28027搭建Proteus仿真工程模板,帮助DSP开发者快速上手。从资源获取、工程目录设计到关键文件配置,再到CCS与Proteus的联调技巧,全面解析仿真工程搭建的每个步骤,特别适合初学者和工程师参考。

weixin_42715608的博客 473

java 热更

java 热更

weixin_51438987的博客 1255

风控IV、AUC、KS python计算代码

df_temp:数据集,Y:是否逾期标签,feas_list:需要计算IV的 变量。2、 按月和不同Y 下的 IV计算。

暮雨听轩的专栏 1618

绕线机的制作与研究

本文详细阐述了绕线机的制作过程,涵盖从设计规划到实际制作的各个环节。通过对绕线机工作原理的深入分析,确定了关键技术参数,并依此完成机械结构与控制系统的设计。在制作阶段,运用多种加工工艺完成零件制造与设备组装。经测试,自制绕线机性能达到预期,能够满足特定线圈绕制需求,为相关领域的生产实践与技术研究提供了参考。

道亦无名 1437

win10 java更新失败_win10系统安装不了java的解决方法

很多小伙伴都遇到过win10系统安装不了java的情况,想必大家都遇到过win10系统安装不了java的情况吧,那么应该怎么处理win10系统安装不了java呢?我们依照1、查看自己的电脑是32位的还是64位的(x86代表32位的)。java安装要求比较高,楚除了查看系统是windows还是linux等等,如果是windows系统,还要查看是32位的还是64位的。我的是64的。2、下载java,进...

weixin_42508792的博客 1986

dex2oat编译优化调试

dex2oat编译调试的一些技巧和方法

jidapenglin28的专栏 5045

Java 匿名类热更新失败的原因

一般来说,现在游戏服务器都实现了java代码热更新的机制,这样就可以不用重启服务器,在服务器运行的状态中修改一些Java代码,修改一些小Bug,这给我们带来了很好的方便,但是Java服务器热更新也有一些限制,比如不能添加新方法,不能修改原方法的签名(不能修改返回类型,不能原方法修改参数)等。但是有时候在不知不觉中就可能违反了这些限制,导致热更新失败。 在服务器热更新对象的时候,一定要注意匿名类的热更新,它最容易引起方法参数不一致问题,导致热更新失败。即使强制替换了class文件,也会导致报错:java.la

王广帅--游戏开发-服务器开发 1625

TensorRT量化实战教程:从PTQ到QAT,让你的模型推理速度飞起来

无论是PTQ的“快速落地”,还是QAT的“精度极致”,TensorRT量化技术都能成为你模型部署路上的“性能引擎”。通过本教程的实战项目,你已掌握从技术原理到工程落地的全流程能力——现在,不妨将你的模型代入其中,亲眼见证它在速度与精度的双重提升中“焕发生机”。若你在实操中遇到任何卡点,随时可以针对具体问题交流,我会尽全力帮你突破量化部署的每一个瓶颈。

FJN110的博客 239

剪映专业版关键帧完全教程:从原理到实战的动画必修课

关键帧是视频动画的核心原理。视频由连续静止画面(帧)构成,关键帧记录素材属性变化的起始与结束状态,中间过渡帧由软件自动计算生成。剪映专业版中,视频、音频、文本、贴纸等素材均支持关键帧动画,可调节位置、缩放、旋转、不透明度、音量等参数。操作为:在起始位置点击参数旁菱形按钮添加关键帧,移动播放头后调整参数,系统自动生成第二个关键帧。多属性可独立控制并叠加,实现放大同时淡出等复合效果。通过设定变化起点与终点,关键帧能让静止元素产生移动、缩放、淡入淡出等丰富动态,是视频剪辑从基础拼接进阶创意表达的核心技能。掌握关键

深耕计算机专业知识与技术。 2092

异常(no providers installed)

问题描述 最近遇见了一个异常,将java项目打完jar包后在windows运行的时候报异常提示: java.util.ServiceConfiguretionError: com.sun.tools.attach.spi.AttachProvider: Provider Sun.tools.attach.WindowsAttachProvider could not be instantiate...

qq_24557827的博客 4764

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

Arthas Retransform实战:规避Lambda表达式热更新陷阱

本文深入探讨了Arthas Retransform在热更新Lambda表达式时遇到的常见问题及解决方案。通过分析JDK版本差异导致的Lambda方法签名不兼容问题,提供了环境对齐和字节码手术两种实用方案,并强调环境一致性的重要性。文章还分享了监控与回滚机制,帮助开发者规避热更新陷阱。

weixin_29248611的博客 186

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

arthas 线上更新代码不生效的问题Memory compiler error, exception message: Compilation Error

如果线上出了问题,如果小问题的情况可以使用arthas 工具去热更新判断,官网也说了方法没有结束的循环,或者没有结束的这种方法,没有返回值的这种,也可以理解为资源一直占用,没有释放的程序代码,也不能新加方法去更新因为不生效,所以慎用这个方式去更新代码,还是通过重新部署修改代码最稳定,也最安全。具体的原因更加服务器环境配置,java环境决定。————没有与生俱来的天赋,都是后天的努力拼搏。

5551

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

为什么你的Java车载服务在-40℃冷启动失败?温度敏感型ClassLoader加载异常的12小时紧急修复路径

解决Java车载系统调试中-40℃冷启动失败难题,通过定制ClassLoader、预加载类路径与温度自适应初始化策略实现稳定启动。适用于车规级Linux环境,12小时定位并修复类加载时序异常。值得收藏

VarIsle的博客 400

Java热更新

游戏版本更新没个星期都要进行更新,每次都更新都要在夜深人静的时候进行,这样才能影响到最少的玩家。但是有时候线上出了一些小bug,停服维护又不值得,所以如果可以进行热更新,不停服解决问题,那样就完美了。 我这里用到的也是java.lang.instrument ,关于这个介绍网上有很多,就不介绍了,直接来说我们的例子。 1、这里是我们的测试用的程序,主要就是循环打印出一个数字,然后我们通过热更...

hoohyouxi的博客 6396

ik分词热更新失败的问题

ES 7.7.0 版本 ik分词字典更新,按照官方文档,采用本地词库+热更新方式 <?xml version="1.0" encoding="UT...

aben_sky的专栏 850

热更新你都知道哪些?

热更新你都知道哪一些?本篇博客介绍热更新基本原理和作用,以及几种流行热更新的框架对比和优劣,快来看看吧。

luo_boke的博客 9931

arthas命令redefine实现Java热更新

Arthas非常重要的命令redefine,主要作用是加载外部的.class文件,用来替换JVM已经加载的类,总结起来就是实现了Java的热更新。 redefine在一下几种情况中会失败:1、增加了field;2、增加了method;3、替换正在运行的方法。 前两个比较好理解,第三个意思就是这个方法必须结束之后才会被替换,如果有个方法开始运行之后就不会跳出,那么这个方法所在的类是无法被替换的,类似无限循环的方法。 场景需求 举个例子,在线上代码某个class的方法添加日志输出,修改方法返回结果,但是,又.

Bertram的博客 2039

Arthas 入门到实战(二)在线热更新

Arthas 使用 jad/mc 命令在线热更新

m0_47743175的博客 2011

java lua热更新_Cocos2dx Lua 热更新

在我没接触热更新之前,我对热更新是一脸懵逼。后来看了些资料,自己动手试验了下,基本上了解了它的过程。所谓的热更新,就是在线更新代码和资源。热更新的过程首先,客户端向服务器发送请求,服务器告诉客户端,没更新啦,你是最新的啦,那就直接跳过喽。但如果是告诉你有更新,那就要告诉我哪些需要更新对吧,你可能需要更新的东西,放在一个文件里,一并发送给客户端,客户端拿到这个文件,就一个一个去向服务器要,最后把要更...

weixin_42299728的博客 153
上一篇: Redis使用心得
下一篇: Netty 5新Buffer API详解
长三月
博客等级 码龄9年 228粉丝 80原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值