JVM虚拟机内存结构详解,一文带你学习完80%的知识

news2024/12/23 15:04:17

程序计数器

定义

Program Counter Register 程序计数器(寄存器)

  • 作用:是记录下一条 jvm 指令的执行地址行号
  • 特点:
    • 是线程私有的,随着线程创建而创建,随着线程销毁而销毁
    • 不会存在内存溢出
    • 是一块较小的内存空间、

作用

  • 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条jvm指令的执行地址,这样下一次解释器会从程序计数器拿到指令地址,获取对应指令然后进行解释执行。

程序计数器在物理上是通过寄存器来实现的,因为寄存器是整个cpu中读取速度最快的一个单元,我们读取指令地址这个动作是非常频繁的,所以java虚拟机在设计的时候把cpu中的寄存器当作程序计数器,用它来存储地址,将来来读取这个地址

虚拟机栈

定义

Java Virtual Machine Stacks (Java 虚拟机栈)

  • 每个线程运行需要的内存空间,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

问题辨析:

  1. 垃圾回收是否涉及栈内存?
    • 不会。栈内存是方法调用产生的,方法调用结束后会弹出栈 。
  2. 栈内存分配越大越好吗?
    • 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
  3. 方法内的局部变量是否线程安全
    1. 如果方法内部的变量没有逃离方法的作用访问,它是线程安全的
    2. 如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。

栈内存溢出

  • 栈帧过多导致栈内存溢出
  • 栈帧过大导致栈内存溢出

栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!一般都是栈帧过多导致内存溢出

线程运行诊断

案例一:cpu 占用过多

解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

  • top 命令,查看是哪个进程占用 CPU 过高
  • ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号(通过 ps 命令进一步查看是哪个线程占用 CPU 过高)
  • jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。

生产环境不推荐jstack,因为打印线程信息jvm会暂停其他线程

案例二:程序运行很长时间没有结果

可能是由于多个线程发生了死锁

我们同样可以使用jstack进行问题的定位

本地方法栈

img

jvm调用一些本地方法时需要给这些本地方法提供的一个内存空间。一些带有 native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。

  • 程序计数器、虚拟机栈、本地方法栈都是线程私有的
  • 堆、方法区时线程共享的区

定义

Heap 堆

  • 通过new关键字创建的对象都会被放在堆内存

特点

  • 它是线程共享,堆内存中的对象都需要考虑线程安全问题
  • 有垃圾回收机制

堆内存溢出

java.lang.OutofMemoryError :java heap space. 堆内存溢出 可以使用 -Xmx8m 来指定堆内存大小。

堆内存诊断

  1. jps 工具
    • 查看当前系统中有哪些 java 进程
  2. jmap 工具
    • 查看堆内存占用情况 jmap -heap 进程id
  3. jconsole 工具
    • 图形界面的,多功能的监测工具,可以连续监测
  4. jvisualvm 工具

方法区

定义

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的!

官方解释:JVM规范-方法区定义

Oracle的HotSpot虚拟机在jdk1.8之前的实现叫做永久代,永久代就是使用堆内存的一部分作为方法区;但是在jdk1.8之后把永久代移除了,换了个实现,这个实现叫原空间,原空间用的不是堆的内存,用的是本地内存,也就是操作系统的内存

方法区是规范,永久代和原空间是实现

组成

方法区内存溢出

  • 1.8 之前会导致永久代内存溢出
    • 使用 -XX:MaxPermSize=8m 指定永久代内存大小
    • 演示永久代内存溢出java.lang.OutOfMemoryError: PermGen space
  • 1.8 之后会导致元空间内存溢出
    • 使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
    • 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace

场景:

  • spring
  • mybatis

运行时常量池

二进制字节码包含(类的基本信息,常量池,类方法定义,包含了虚拟机的指令) 首先看看常量池是什么,编译如下代码(javac Test.java):

 public class Test {



 



     public static void main(String[] args) {



         System.out.println("Hello World!");



     }



 



 }




然后使用 javap -v Test.class 命令反编译查看结果。

img

每条指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。

常量池: 就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息 运行时常量池: 常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

StringTable

先看几道面试题:

 String s1 = "a";



 String s2 = "b";



 String s3 = "ab";



 String s4 = s1 + s2;



 String s5 = "a" + "b";



 String s6 = s4.intern();



 // 问



 System.out.println(s3 == s4); //false



 System.out.println(s3 == s5);//true



 System.out.println(s3 == s6);//true



 String x2 = new String("c") + new String("d");



 String x1 = "cd";



 x2.intern();



 // 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢



 System.out.println(x1 == x2);//false



 //如果调换最后两行代码的位置就是true



 //如果是jdk1.6,调换两行代码的位置,x2.intern();会产生一个"cd"副本,将副本入常量池,而x2还是指向的堆中的new string("ab"),而x1指向的是常量池中的"cd",所以为false




反编译之后的分析:

 // StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容



 // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象



 // ldc #2 会把 a 符号变为 "a" 字符串对象



 // ldc #3 会把 b 符号变为 "b" 字符串对象



 // ldc #4 会把 ab 符号变为 "ab" 字符串对象



 



 public static void main(String[] args) {



     String s1 = "a"; // 懒惰的



     String s2 = "b";



     String s3 = "ab";



     String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")



     String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab



     System.out.println(s3 == s5);//true



 }




 //  ["ab", "a", "b"]



 public static void main(String[] args) {



 



     String x = "ab";



     String s = new String("a") + new String("b");



 



     // 堆  new String("a")   new String("b") new String("ab")



     String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回



     System.out.println( s2 == x);//true



     System.out.println( s == x );//false



 }




 //  ["a", "b", "ab"]



 public static void main(String[] args) {



 



     String s = new String("a") + new String("b");



 



     // 堆  new String("a")   new String("b") new String("ab")



     String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回



     System.out.println( s2 == "ab");//true



     System.out.println( s == "ab" );//true



 }




  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
    • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回

无论放入是否成功,都会返回串池中的字符串对象

StringTable的位置

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

永久代的回收效率低,只有在Full GC的时候才会触发垃圾回收,而Full GC要等到老年代的空间不足时才会触发

StringTable 垃圾回收

-Xmx10m 指定堆内存大小 -XX:+PrintStringTableStatistics 打印字符串常量池信息 -XX:+PrintGCDetails -verbose:gc 打印 gc 的次数,耗费时间等信息

 /**



  * 演示 StringTable 垃圾回收



  * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc



  */



 public class Code_05_StringTableTest {



 



     public static void main(String[] args) {



         int i = 0;



         try {



             for(int j = 0; j < 10000; j++) { // j = 100, j = 10000



                 String.valueOf(j).intern();



                 i++;



             }



         }catch (Exception e) {



             e.printStackTrace();



         }finally {



             System.out.println(i);



         }



     }



 



 }



 




StringTable 性能调优

  • 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间

-XX:StringTableSize=桶个数(最少设置为 1009 以上)

  • 考虑是否需要将字符串对象入池,可以通过 intern 方法减少重复入池

直接内存

定义

直接内存(Direct Memory)就是系统内存

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

使用直接内存的好处

文件读写流程:

因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。

使用了 DirectBuffer 文件读取流程

直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。

直接内存回收原理

 public class Code_06_DirectMemoryTest {



 



     public static int _1GB = 1024 * 1024 * 1024;



 



     public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {



 //        method();



         method1();



     }



 



     // 演示 直接内存 是被 unsafe 创建与回收



     private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {



 



         Field field = Unsafe.class.getDeclaredField("theUnsafe");



         field.setAccessible(true);



         Unsafe unsafe = (Unsafe)field.get(Unsafe.class);



 



         long base = unsafe.allocateMemory(_1GB);



         unsafe.setMemory(base,_1GB, (byte)0);



         System.in.read();



 



         unsafe.freeMemory(base);



         System.in.read();



     }



 



     // 演示 直接内存被 释放



     private static void method() throws IOException {



         ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);



         System.out.println("分配完毕");



         System.in.read();



         System.out.println("开始释放");



         byteBuffer = null;



         System.gc(); // 手动 gc



         System.in.read();



     }



 



 }




直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。 第一步:allocateDirect 的实现

 public static ByteBuffer allocateDirect(int capacity) {



     return new DirectByteBuffer(capacity);



 }




底层是创建了一个 DirectByteBuffer 对象。 第二步:DirectByteBuffer 类

 DirectByteBuffer(int cap) {   // package-private



    



     super(-1, 0, cap, cap);



     boolean pa = VM.isDirectMemoryPageAligned();



     int ps = Bits.pageSize();



     long size = Math.max(1L, (long)cap + (pa ? ps : 0));



     Bits.reserveMemory(size, cap);



 



     long base = 0;



     try {



         base = unsafe.allocateMemory(size); // 申请内存



     } catch (OutOfMemoryError x) {



         Bits.unreserveMemory(size, cap);



         throw x;



     }



     unsafe.setMemory(base, size, (byte) 0);



     if (pa && (base % ps != 0)) {



         // Round up to page boundary



         address = base + ps - (base & (ps - 1));



     } else {



         address = base;



     }



     cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // 通过虚引用,来实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。



     att = null;



 }




这里调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。

public void clean() {



       if (remove(this)) {



           try {



           // 都用函数的 run 方法, 释放内存



               this.thunk.run();



           } catch (final Throwable var2) {



               AccessController.doPrivileged(new PrivilegedAction<Void>() {



                   public Void run() {



                       if (System.err != null) {



                           (new Error("Cleaner terminated abnormally", var2)).printStackTrace();



                    }



 



                    System.exit(1);



                    return null;



                }



            });



        }



 



    }



}




可以看到关键的一行代码, this.thunk.run(),thunk 是 Runnable 对象。run 方法就是回调 Deallocator 中的 run 方法

 public void run() {



     if (address == 0) {



         // Paranoia



         return;



     }



     // 释放内存



     unsafe.freeMemory(address);



     address = 0;



     Bits.unreserveMemory(size, capacity);



 }




直接内存的回收机制总结

  • 使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法
  • ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法调用 freeMemory 来释放内存

注意:

 /**



  * -XX:+DisableExplicitGC 显示的



  */



 private static void method() throws IOException {



     ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);



     System.out.println("分配完毕");



     System.in.read();



     System.out.println("开始释放");



     byteBuffer = null;



     System.gc(); // 手动 gc 失效



     System.in.read();



 }




一般用 jvm 调优时,会加上下面的参数:

-XX:+DisableExplicitGC  // 静止显示的 GC




意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CVPR 2019|APCNet:基于全局引导的局部匹配度自适应金字塔上下文网络

&#x1f3c6;本篇论文发现了一种全局引导的局部匹配度&#xff08;Global-guided Local Affinity (GLA))特征&#xff0c;用于构造上下文语义信息。基于此特性&#xff0c;作者设计了自适应上下文模块&#xff0c;构建自适应金字塔上下文网络&#xff08;APCNet&#xff09;。…

手工测试2年面临职场危机,3个月进阶自动化测试后,老板终于留我了...

​前言 从学校到职场已经3年时间了&#xff0c;大学学的计算机专业&#xff0c;最开始事项从事java开发的&#xff0c;最终被现实打败&#xff0c;然后就从事了软件测试&#xff0c;现在已近过去了2年&#xff0c;为什么说是2年了&#xff0c;大学毕业有一段空窗期&#xff0c…

大数据面试之HDFS常见题目

大数据面试之HDFS常见题目 HDFS常见题目 1 HDFS读流程和写流程 1.1 读流程&#xff08;下载&#xff09; 文字描述&#xff1a; ​ 客户端将要读取的文件路径发送给 NameNode&#xff0c;NameNode 获取文件的元信息&#xff08;主要是 block 的存放位置信息&#xff09;返回…

美团外卖推荐智能流量分发的实践与探索

总第548篇2022年 第065篇美团外卖推荐团队在推荐算法的长期落地实践中&#xff0c;针对外卖业务情境化特点对排序模型进行深入探索与优化。本文介绍了面向情境化建模的“情境细分统一模型”建模思路&#xff0c;通过用户行为序列建模以及专家网络两个模块的优化&#xff0c;实现…

简单实用:css+html绘制常见图表

提到绘制图表&#xff0c;大家可能想到ECharts&#xff0c;其实&#xff0c;一些简单的图表可以直接通过csshtml实现&#xff0c;下面手把手带大家绘制&#xff0c;初学者也能轻松掌握。 1 csshtml绘制柱形图 我们先写一个超简单的html文件。 <div class"bargraph&q…

多维数组地址映射问题的求解(3维、4维为例)——数据结构

在上篇我大概介绍了多维数组的地址映射问题&#xff0c;但是不够完善&#xff0c;很多朋友还没有彻底学会&#xff0c;表示很头疼。这一方面的总结确实比较少&#xff0c;而且也很麻烦&#xff0c;但是不要怕&#xff0c;看完我的总结&#xff0c;相信你一定会有一直醍醐灌顶的…

《2022中国PaaS市场研究及选型评估报告》正式发布

《中智观察》第1741篇推送作者&#xff1a;海比研究院编辑&#xff1a;晓晓编审&#xff1a;赵满头图来源&#xff1a;中国软件网从2006年概念兴起至今&#xff0c;云计算已经在国内走过整整十五年的历程。云计算的三大模式SaaS、PaaS、IaaS从陌生到熟悉&#xff0c;从研发到应…

ML Journal6—OpenCV中的GUI功能

图像入门这是将在本教程中使用的图像borz.jpgimport cv2 as cv import sysimg cv.imread(borz.jpg) if img is None:sys.exit("Could not read the file.") cv.imshow("Display Window", img) k cv.waitKey(0) if k ord("s"):cv.imwrite(&quo…

《自己动手写CPU》学习记录(5)——第5章/Part 1

目录 引言 致谢 流水线的数据相关问题 问题分析 RAW类型 1、相邻指令数据相关 2、间隔1条指令数据相关 3、间隔2条指令数据相关 修改后的代码 译码模块 指令执行模块 顶层模块 测试 测试代码 生成.data初始化文件 仿真结果 引言 随章节进度继续推进&#xff0c…

【视频】马尔可夫链原理可视化解释与R语言区制转换MRS实例|数据分享

原文链接&#xff1a;http://tecdat.cn/?p12280马尔可夫链是从一个“状态”&#xff08;一种情况或一组值&#xff09;跳到另一个“状态”的数学系统。本文介绍了马尔可夫链和一种简单的状态转移模型&#xff0c;该模型构成了隐马尔可夫模型&#xff08;HMM&#xff09;的特例…

近期方案研究总结(那些你用的到的排列组合)

方案一 这个方案只一个位置一个号码&#xff0c;标的物即一个位置八十期不重即可以切入&#xff0c;以这样的思路去进行扩充。 纬度一 两期重复形态为&#xff1a;11、22、33、44、55、66、77、88、99、1010。 数上升一形态为&#xff1a;12、23、34、45、56、67、78、89、910…

微服务框架 SpringCloud微服务架构 28 数据同步 28.6 测试同步功能

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 SpringCloud微服务架构 文章目录微服务框架SpringCloud微服务架构28 数据同步28.6 测试同步功能28.6.1 直接开干28 数据同步 28.6 测试同步…

云安全系列5:2023 年需要了解的 40个云安全术语

云安全具有广泛而复杂的特点&#xff0c;它有许多特定的术语和首字母缩略词。我们在这里整理了一些云安全术语。将其用作指南&#xff0c;可帮助您了解云安全的细微差别并更便于学习相关内容。 Agent 代理是专门的软件包或应用程序&#xff0c;部署到设备或机器上以完成与安全…

IDEA:Idea 集成 EasyYApi 插件实现接口文档的生成与更新

我是 ABin-阿斌&#xff1a;写一生代码&#xff0c;创一世佳话&#xff0c;筑一览芳华。如果小伙伴们觉得不错就一键三连吧~ 声明&#xff1a;文章原文来源处&#xff1a; https://blog.csdn.net/weixin_40863968/article/details/122432678https://www.jianshu.com/p/cd91590…

10个知识点让你读懂Spring MVC容器

DispatcherServlet 的配置参数 可以通过的属性指定配置参数&#xff1a; namespace参数&#xff1a;DispatcherServlet对应的命名空间&#xff0c;默认是WEB-INF/-servlet.xml。在显式配置该参数后&#xff0c;新的配置文件对应的路径是WEB-INF/.xml&#xff0c;例如如果将name…

Android 移植iperf3.13 测试网络

iperf,测试网络性能的&#xff0c;不同于 webrtc里面的gcc 算法预估网络带宽&#xff0c;iperf是实际占用网络来传输数据测量实际的网络性能。 官网&#xff1a;https://iperf.fr/ 官方就有 android 的下载 https://iperf.fr/iperf-download.php#android&#xff0c; 但是看起…

绕线机算法模型(Simulink仿真验证+PLC代码实现)

绕线机应用的详细内容大家可以参看下面这篇博客,这里不再赘述。本文主要借助Simulink仿真分析和解决工程实际问题。希望对大家学习Simulink有所帮助。限于水平和能力所限,文中难免出现错误和不足之处,诚恳的希望大家批评和指正。 S7-200 SMART PLC和V20变频器绕线机控制应用…

Linux(在 Linux 上搭建 java 部署环境(安装jdk/tomcat/mysql) + 将程序部署到云服务器上的操作)

目录 &#x1f432; 1. 在 Linux 上搭建 java 部署环境 &#x1f984; 1.1 安装 jdk &#x1f984; 1.2 安装 tomcat &#x1f984; 1.3 安装 mysql &#x1f432; 2. 将[博客系统]部署到云服务器上 &#x1f984; 2.1 先在云服务器上,创建和之前 windows 上一样的数据库…

德勤、阿里为数据智能时代造势,是风口还是韭菜?

前不久&#xff0c;德勤和阿里高调发布了业内首个DAAS报告《DAAS数字化新世代的最优解》&#xff0c;强调“企业不应该继续将数据智能建设当作‘选择题’&#xff0c;而是一道‘必答题’&#xff01;” 德勤管理咨询中国首席执行官戴耀华&#xff08;右&#xff09;与阿里巴巴集…

自学编程技术如何选择书籍?

首先自学编程的话&#xff0c;也是要先确定要选择适合自己的编程语言。然后再购买相应的编程教材&#xff0c;不知道怎么选择的话&#xff0c;可以直接选择黑马的原创书籍。 《C语言开发基础》 内容简介 本书是对《C语言开发入门教程》一书的全新改版&#xff0c;本次改版结合…