JVM系列(八) -运行期的几种优化技术

一、摘要

在之前的文章中我们谈到过,相比 C/C++ 语言,Java 语言在运行效率方面要稍逊一些,因为 Java 应用程序是在虚拟机上运行,而 C/C++ 程序是直接编译成平台相应的机器码来运行程序。

从虚拟机对外发布开始,开发团队一直在努力试图缩小 Java 与 C/C++ 语言在运行效率上的差距。从实际的结果来看,确实成果显著。

本文就来聊聊 HotSpot 虚拟机为了提升 Java 程序的执行效率,都实现了哪些激动人心的优化技术。

二、JIT 编译器的引入

JIT 编译器,也称为即时编译器,它是 JVM 的重要组成部分。与我们经常用的生成 Java 字节码的javac编译器不同,JIT 编译器是实现 Java 程序执行效率提升的核心利器。

经常有面试官会提出这样的一个问题:Java 程序是解释执行还是编译执行?

刚开始学习 Java 的同学,大概率会认为 Java 是编译执行,其执行流程类似于如下图。

源码程序.java文件,通过javac命令编译成.class字节码,最后通过java命令在虚拟机中利用解释器来执行代码。其中虚拟机的解释器作用,就是将字节码的操作指令和真正的平台体系之间的指令建立映射,比如把 Java 的load指令转换成native codeload指令,以此来完成程序的执行。

其实,准确的说,Java 既有解释执行,也有编译执行,其工作流程大致可以用如下图来描述。

其中,JIT 编译器会将热点代码编译成本地平台相关的机器码,并进行各种层次的优化,从而实现程序执行效率的提升

JIT 编译器的出现,可以说补强了虚拟机边运行边解释的低性能问题。

也许有的同学会提出这样的疑问,既然引入了 JIT 编译器可以显著提升程序执行效率,那 HotSpot 为什么不直接采用 JIT 编译器来执行呢?

简单的说,解释器和编译器各有优势。

  • 当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,可以立即执行
  • 当程序运行后,随着时间的推移,JIT 编译器可以发挥作用,能把越来越多的代码编译成本地机器码,进一步提升程序的执行效率

这就是为什么 Java 程序既有解释执行,也有编译执行的原因

当然,能触发即时编译请求的条件比较多,比如方法调用,OSR 编译请求等。在默认设置下,无论是哪种场景,虚拟机在代码编译器还未完成的时候,都仍然按照解释器来继续执行,而编译动作则是在后台的编译线程中运行。

用户可以通过-XX:-BackgroundCompilation参数来禁止后台编译,此时所有的编译请求会等待,直到编译完成后再开始执行本地机器码。

2.1、Client 模式与 Server 模式

在 HotSpot 虚拟机中内置了两款即时编译器,分别是Client CompilerServer Compiler,也称为 C1 编译器与 C2 编译器。

在目前的 HotSpot 虚拟机中,默认采用的是解释器与其中一个即时编译器直接配合的工作方式,用户也可以使用-client或者-server参数来指定解释器与具体的某个编译器配合工作。

它们之间的区别,可以用如下内容简要概括:

  • Client Compiler(C1编译器):它是一个简单快速的编译器,主要关注点在于局部性的优化,而放弃了许多耗时间长的全局优化手段
  • Sever Compiler(C2编译器):它是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,它会执行所有经典的优化动作,如无用代码消除、循环展开、常量传播、基本块重排序等,还会实施一些与 Java 语言特性密切相关的优化技术,如范围检查消除、空值检查消除等,另外,还有可能根据解释器或 Client Compiler 提供的性能监控信息,进行一些不稳定的激进优化,如守护内联、分支频率预测等

Sever Compiler 即时编译器,无疑是比较缓慢的,但它的编译速度依然远超传统的静态优化编译器,而且它相对于 Client Compiler 编译器输出的代码质量更高,可以减少本地代码的执行时间,从而抵消额外的编译时间开销,因此很多非服务端的虚拟机选择-server模式来运行。

2.2、编译对象与触发条件

在上文我们有提到,JIT 编译器会将热点代码编译成本地平台相关的机器码。

哪些代码会被 JIT 编译器判断为“热点代码”呢?主要有两类:

  • 被多次调用的方法
  • 被多次执行的循环体

这两种情况都会使即时编译器以整个方法作为编译对象。

比较难以理解的可能是第二种情况,对于被多次执行的循环体,可以理解成以一个方法可能只被调用一次或者少量的几次,但是方法体内部存在循环次数较多的循环体问题,这样循环体的代码也会被重复执行多次,因此这些代码也被认为是“热点代码”。

上面提到的都是概念知识,虚拟机如何判断一段代码是否是“热点代码”呢?主要有两种办法:

  • 基于采样的热点探测
  • 基于计数器的热点探测

HotSpot 虚拟机中使用的是第二种基于计数器的热点探测方法,它为每个方法准备了两类计数器:方法调用计数器和回边计数器。

在确认虚拟机运行参数的前提下,这两类计数器都有一个确认的的阀值,当计数器超过阀值时,就会触发即时编译器。

下面我们一起来看看这两类计数器的实现。

2.2.1、方法调用计数器

方法调用计数器,通常用于统计方法被调用的次数。它的默认阈值在Client模式下是 1500 次,在Server模式下是 10000 次,这个阈值可以通过-XX:CompileThreshold参数来人为设定。

当一个方法被调用时,会检查方法是否存在被 JIT 编译过的版本,如果存在,则优先使用编译后的本地机器码来执行;如果不存在,将此方法的调用计数器值加 1,然后判断方法调用计数器和回边计数器之和是否超过方法调用计数器的阈值,如果超过,向即时编译器提交一个该方法的代码编译请求,在默认不设置的情况下,不会同步等待编译请求完成,而时直接以解释方式执行方法。

具体流程,可以用如下图来概括。

如果不设置阀值的情况下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。

当超过一定的时间限制,如果方法的调用次数不足以让它提交给即时编译器编译,那这个方法的调用计数器就会少一半,这个过程称为方法的调用计数器热度衰减,而这段时间就称为此方法统计的半衰周期。

进行热度衰减的动作是在虚拟机进行垃圾回收时顺便进行的,可以通过-XX:-UseCounterDecay参数来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样一来,只要系统运行时间足够长,基本上绝大部分方法都会被编译成本地机器码。

此外,用户也可以通过-XX:CounterHalfLifeTime参数来设置半衰周期的时间,单位是秒。

2.2.2、回边计数器

回边计数器,通常用于统计一个方法中循环体代码执行的次数。在字节码方法循环体中,遇到控制流向后跳转的指令成为"回边",这个过程会产生“栈上替换”的行为,也就是方法栈帧还在栈上,只是方法被替换了,HotSpot 把这个过程触发的即时编译,称之为 OSR 编译。

关于回边计数器的阈值设置,虚拟机没有明确给出对应的参数,但是可以通过-XX:OnStackReplacePercentage参数来间接的调整回边计数器的阈值,这个参数也称为 ORS 比率,回边计数器的阈值计算公式如下:

  • Client 模式:方法调用计数器阈值 × OSR 比率 / 1000,其中 OSR 比率默认值933,如果都取默认值,回边计数器的阈值应该是 13995
  • Server 模式:方法调用计数器阈值 × ( OSR 比率 - 解释器监控比率) / 100,其中 OSR 比率默认 140,解释器监控比率默认33,如果都取默认值,回边计数器阈值应该是 10700

当解释器遇到一条回边指令时,会先查找需要执行的代码片段中是否有已经编译的版本,如果有,会优先执行已编译好的代码;如果没有,就会把回边计数器的值加 1,然后判断方法调用计数器和回边计数器值之和是否超过回边计数器的阈值,如果超过,就会向即时编译器提交一个 OSR 编译请求,并且把回边计数器的值降低一些,以便继续在解释器中执行循环。

具体流程,可以用如下图来概括。

与方法计数器不同,回边计数器没有热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数,当回边计数器溢出的时候,虚拟机还会把方法计数器的值也调整成溢出状态,这样下次再进入该方法的时候,就会执行标准的编译过程。

三、运行期优化技术

HotSpot 虚拟机设计团队为了实现程序更快的执行效率,列出了很多的优化手段,比如方法内联、冗余访问消除、复写传播、无用代码消除、公共子表达式消除、数组边界检查消除、逃逸分析等。

下面我们抽取几个最常见的优化技术,一起来看看相关的实现。

3.1、公共子表达式消除

公共子表达式消除是一个普遍应用于各种编译器的经典优化技术。

如果一个子表达式已经计算过了,且表达式中变量的值不曾发生变化,那么这个子表达式就可以当做公共子表达式。

对于这种表达式,没有必要再花时间去对它进行计算,直接用前面计算过的表达式结果替代就可以了。如果这种优化仅限于程序的基本块内,便称为局部公共子表达式消除;如果这种优化的范围涵盖了多个基本块,便称为全局公共子表达式消除。

举个简单的例子,假设存在以下代码。

// 原始代码
int d = (c * b) * 12 + a + (a + b * c);

如果这段代码交给 Javac 编译器则不会进行任何优化,但是这段代码进入到虚拟机即时编译器之后,它将会进行如下优化。

// 将 c*b 和 b*c 用 E 表示,消除公共子表达式
int d = E * 12 + a + (a + E);

即时编译器还可能进行另一种叫做代数简化的优化,把表达式变为:

// 代数简化
int d = E * 13 + a + a;

表达式变换之后,再次计算可以节省一些时间。

3.2、数组边界检查消除

数组边界检查消除也是一个经典优化技术。

Java 语言作为一门动态安全的语言,会自动对数组的读写访问索引合法性做检查,当超出地址范围,会抛出java.lang.ArrayIndexOutOfBoundsException异常,这对软件开发很友好,但对 JVM 却是一个性能负担。

如果能在编译期根据数据流分析判定索引一直在数组边界内,就可以消除数组上下边界的检测,从而节省很多次条件判断操作。

类似的消除手段还有空指针检查(NullPointException)、除数为零检查(ArithmeticException)、自动装箱消除(Autobox Elimination)、安全点消除(Safepoint Elimination)、消除反射(Dereflection)等等,针对这些检查的消除方式,可能会采用隐式异常处理的思路。

举个简单的例子,假设存在以下代码。

// 原始伪代码
if(foo != null) {return foo.value;
} else {throw new NullPointException();
}

优化后的伪代码。

// 隐式异常消除后的伪代码
try {return foo.value;
} catch (segment_fault) {uncommon_trap();
}

JVM 会注册一个 Segment Fault 信号的异常处理器(uncommon_trap() 是一个针对进程层面的异常处理器,与 try-catch 的线程级异常处理器不同),当 foo 不为空,可以省去判空的开销;如果 foo 真为空,会转到异常处理器恢复中断并抛出NullPointException异常。

借助 JVM 在运行期收集的性能监控信息,判定 foo 极少为空时,采用这样的优化方式可以提升程序的执行效率。

3.3、方法内联

方法内联是 JVM 最重要的优化手段之一,它可以去除方法调用的成本(如减少建立栈帧等),为其它优化建立了良好的基础。

虚拟机如果探测到某个方法是热点方法并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置。

举个例子!

public final static void method1(){handleA();handleB();
}
public static void main(String[] args) {handle1();method1();handle2();
}

优化之后可能变成:

public static void main(String[] args) {handle1();handleA();handleB();handle2();
}

从效果上看,就是把method1()方法拷贝到main()方法中。未拷贝之前,优化空间非常小,但是合并到一个方法之后,就有了很大的优化空间了。

再比如下面这个例子!

private final  static int method1(final int i) {return i * i;
}
public static void main(String[] args) {System.out.println(method1(10));
}

优化之后可能变成:

public static void main(String[] args) {System.out.println(100);
}

虚拟机还能够进行常量折叠(constant folding)的优化,减少不必要的代码执行环节,从而提升代码执行效率。

再次想到一个问题,在 Java 编程规范里面,可能很多新人不能理解为什么推荐尽量将方法声明为final

我们知道 Java 是多态的特性,子类既可以调用父类方法,也可以重写父类方法,编程方面灵活性非常高,这样其实会导致一个问题,编译期间无法确定应该使用哪一个方法,只有在运行时才能确定,这就可能导致虚拟机很难对方法进行内联操作。

但是如果将方法声明为final,这些方法是无法被重写,方法 A 调用方法 B 基本上是可以完全确定的,可以进行方法内联操作。

3.4、逃逸分析

逃逸分析,在之前对象内存分配的文章中有所简单的介绍过,我们再次回顾一下相关的知识。

逃逸分析是一项比较前沿的优化技术,它并不是直接优化代码的手段,而是为其它优化手段提供了分析技术。

逃逸分析的基本行为是分析对象动态作用域。

当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸。

如果能证明一个对象不会逃移到方法外或者线程之外,换句话说就是别的方法或线程无法通过任何途径访问到这个对象,则可以通过一些途径为这个变量进行一些不同程度的优化。

3.4.1、栈上分配

在之前的对象创建文章中,我们提及过,对象会优先在堆上分配,垃圾收集器会定期回收堆空间中不再使用的对象,但这块的内存回收很耗时。如果确定一个对象不会逃逸出方法之外,让这个对象在栈上分配,对象所占用的内存空间就可以随着栈帧出栈而销毁,这样垃圾收集器的压力将会小很多。

3.4.2、同步消除

线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,此时虚拟机会对这个变量,实施的同步措施进行消除。

比如你定义的类的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后,虚拟机会去掉同步锁来运行。

3.4.3、标量替换

标量是指一个数据已经无法再分解成更小的数据来表示了,比如 Java 虚拟机中的原始数据类型(int,long 等数值类型以及 reference 类型)等都不能进一步分解,它们可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量。

Java 中最典型的聚合量是对象,如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替,拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。

关于逃逸分析的论文早在 1999 年就已经发表,但直到 Sun JDK1.6 才实现了逃逸分析,直到现在这项优化尚未足够成熟,仍有很大改进余地。

不成熟的原因主要是不能保证逃逸分析的性能收益必定能高于它的消耗。虽然在实际测试结果中,实施逃逸分析后的程序往往能运行出不错的成绩,但是在实际的应用程序,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况,或因分析过程耗时但却无法有效判别出非逃逸对象而导致性能有所下降。

如果有需要并且确认对程序运行有益,用户可以使用-XX:+DoEscapeAnalysis参数来手动开启逃逸分析,开启之后可以通过-XX:+PrintEscapeAnalysis参数来查看分析结果,用户还可以使用-XX:+EliminateAllocations参数来开启标量替换,使用-XX:+EliminatLocks参数来开启同步消除,使用-XX:+PrintEliminateAllocations参数查看标量的替换情况。

四、小结

最近对 JVM 技术知识进行了重新整理,再次献上 JVM系列文章合集索引,感兴趣的小伙伴可以点击如下地址快速查阅。

  • JVM系列(一) -什么是虚拟机
  • JVM系列(二) -类的加载过程
  • JVM系列(三) -内存布局详解
  • JVM系列(四) -对象的创建过程
  • JVM系列(五) -对象的内存分配流程
  • JVM系列(六) -运行期优化技术
  • JVM系列(七) -垃圾收集算法
  • JVM系列(八) -垃圾收集器
  • JVM系列(九) -GC日志分析
  • JVM系列(十) -常用调优命令汇总
  • JVM系列(十一) -常用调优工具介绍
  • JVM系列(十二) -常用调优参数总结

五、参考

1.https://www.cnblogs.com/xrq730/p/4857820.html

2.https://juejin.cn/post/7236634386568527928

3.https://blog.csdn.net/ChaoMing_H/article/details/129179684

写到最后

最后。如果感觉文章内容不错,帮忙动动小指头点个赞,点赞对我真的非常重要!加个关注我会非常感激!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/447615.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

interwirelessac9560感叹号,电脑无法连接wifi,无法搜索到wifi

interwirelessac9560感叹号 电脑无法连接wifi,无法搜索到wifi 原因 这可能是wifl模块出现了问题。 解决方案 1、winx 打开,选择【设备管理器】 2、选择网络适配器 右键打开wireless-AC,选择【卸载设备】。 3、关机2分钟后&#xff0c…

JavaWeb 15.详解Servlet及其源码

目录 一、Servlet简介 1.动态资源和静态资源 静态资源 动态资源 生活举例 动/静态资源的响应过程 2.Servlet简介 Servlet执行流程 Servlet作用 二、Servlet开发流程 1.目标 2.开发流程 3.问题 ① Servlet-api.jar导入问题 ② Content-Type响应头的问题 4.Servlet_url-pattern的…

ORACLE SELECT INTO 赋值为空,抛出 NO DATA FOUND 异常

例子: DECLARE ORDER_NUM VARCHAR2(20); BEGIN SELECT S.ORDER_NUM INTO ORDER_NUM FROM SALES_ORDER S WHERE S.ID122344; DBMS_OUTPUT.PUT_LINE(单号: || ORDER_NUM); END; 在查询结果为空的情况下,以上代码会报错:未找到任何数据 解决方…

体育直播系统定制怎么做?掌握这些架构功能设计方案!

要开发一个高效、便捷、又充满乐趣的体育直播平台,我们需要依托先进的技术架构和丰富的功能模块,以提供卓越的观赛体验。如下参考“东莞梦幻网络科技”预先开发的体育直播系统源码,他们采用了哪些技术和模块来提升用户体验。 一、系统架构&am…

Redis-02 数据持久化

redis持久化即将数据从内存写入磁盘,Redis提供了两种持久化的方式:RDB和AOF。 1.RDB RDB持久化:Redis可以将内存中的数据定期快照保存到磁盘上的一个二进制文件中。RDB持久化是一种比较紧凑的文件格式,适用于备份和灾难恢复。通过…

【氮化镓】低温对p-GaN HEMT迁移率、阈值电压和亚阈值摆幅的影响

本期分享一篇低温对p-GaN HEMT 迁移率、阈值电压和亚阈值摆幅影响进行表征和建模的研究论文。文章作者Shivendra Kumar Singh、Thien Sao Ngo、Tian-Li Wu(通讯作者)和Yogesh Singh Chauhan,分别来资源中国台湾阳明交通大学国际半导体技术学院、印度理工学院坎普尔分校电气工…

爬虫之数据解析

数据解析 数据解析这篇内容, 很多知识涉及到的都是以前学习过的内容了, 那这篇文章我们主要以实操为主, 来展开来讲解关于数据解析的内容。 360搜索图片 请求的url大家不需要再找了, 相信大家都会找请求了, 寻找请求从我的第一篇爬虫的博客开始到现在一直都在写,这边的话, 我已…

CSS 图标和文本对齐

比如下面一段HTML代码&#xff0c;我们想在图标旁边显示文本或者数字 <body> <div><img src"smile.svg" alt"smile"><span>12</span></div> <div><img src"heartShape.svg" alt"…

光伏仿真系统在光伏项目开发中有哪些应用场景?

光伏仿真系统在光伏项目开发中的应用场景广泛&#xff0c;涵盖了从项目规划、设计优化到运维管理的全过程。 一、项目规划与选址 1、气象模拟与评估 光伏仿真系统能够基于历史气象数据和先进的预测模型&#xff0c;模拟不同地理位置、不同季节和时间段的光照强度、温度、湿度…

Fomality基础知识

formal主要用于等价性检查&#xff0c;检查步骤如下&#xff1a; 1.Read design 2.Set up 3.Matching&#xff1a;Map corresponding signals between pairs of designs 4.Verification&#xff1a;Compare the logic cones that drive the mapped signals 二&#xff1a;…

Hadoop生态圈三大组件:HDFS的读写流程、MapReduce计算流程、Yarn资源调度

文章目录 1. HDFS的读写流程1.1 HDFS读流程1.2 HDFS写流程 2. MapReduce计算流程3. Yarn资源调度一、客户端请求资源二、Resource Manager处理请求三、任务资源计算与申请四、Resource Manager分配资源五、Node Manager执行任务六、任务执行与监控 1. HDFS的读写流程 1.1 HDFS…

UTF8编码的PowerShell脚本中文乱码的解决方法

1.问题现象 使用VSCode(已安装Code Runner扩展)编写含有中文字符串的ps1脚本之后运行收到各种错误 例如&#xff1a;字符串缺少终止符: "。&#xff1b;表达式或语句中包含意外的标记“}”。&#xff1b;语句块或类型定义中缺少右“}”。 使用快捷键CtrlAltN触发Run Cod…

YOLO11 实例分割 | 导出ONNX模型 | ONNX模型推理

本文分享YOLO11中&#xff0c;从xxx.pt权重文件转为.onnx文件&#xff0c;然后使用.onnx文件&#xff0c;进行实例分割任务的模型推理。 用ONNX模型推理&#xff0c;便于算法到开发板或芯片的部署。 备注&#xff1a;本文是使用Python&#xff0c;编写ONNX模型推理代码的 目…

HDMI Type B 连接器的引脚分配与接口尺寸

连接器引脚分配 B 型连接器引脚分配&#xff1a; 连接器接触顺序 连接器接口尺寸 B 型插座接口尺寸&#xff1a; B 型插头接口尺寸&#xff1a; B 型插座与插头配合状态&#xff1a; 微信公众号&#xff1a;

CreateEvent 事件对象可以用来在线程之间同步操作 信号事件

CreateEvent 是一个 Windows API 函数&#xff0c;用于创建或打开一个事件对象&#xff0c;事件对象可以用来在线程之间同步操作。该函数的签名如下&#xff1a; HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性&#xff0c;可以为 NULLBOOL bManu…

使用XML实现MyBatis的基础操作

目录 前言 1.准备工作 1.1⽂件配置 1.2添加 mapper 接⼝ 2.增删改查操作 2.1增(Insert) 2.2删(Delete) 2.3改(Update) 2.4查(Select) 前言 接下来我们会使用的数据表如下&#xff1a; 对应的实体类为&#xff1a;UserInfo 所有的准备工作都在如下文章。 MyBatis 操作…

React国际化中英文切换实现

目录 概况 安装 文件结构 引入 使用 正常使用 传参使用 概况 react-intl-universal 是一个国际化库&#xff0c;专门为 React 应用提供多语言支持。与 React 原生的 react-intl 相比&#xff0c;react-intl-universal 支持从远程服务器加载语言包&#xff0c;动态切换语…

MySQL数据的导入

【图书推荐】《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;》-CSDN博客 《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;&#xff08;数据库技术丛书&#xff09;》(王英英)【摘要 书评 试读】- 京东图书 (jd.com) MySQL9数据库技术_夏天又到了…

简单使用webpack

下载依赖&#xff1a;npm install webpack webpack-cli --save-dev新建一个目录app。加入html&#xff1a; <html><head><meta charset"utf-8"></head><body><script type"text/javascript" src"./dist/main.js&quo…

05 django管理系统 - 部门管理 - 修改部门

04我们已经实现了新增部门的功能&#xff0c;下面开始修改部门模块的实现。 按道理来说&#xff0c;应该是做成弹框样式的&#xff0c;通过ajax悄咪咪的发数据&#xff0c;然后更新前端数据&#xff0c;但是考虑到实际情况&#xff0c;先用页面跳转的方式实现&#xff0c;后面…