深入探究arthas的命令

Arthas是一个Java诊断工具,用于在线诊断Java应用程序,以便更轻松地监控和分析应用程序的性能和行为。Arthas中有许多用于诊断Java应用程序的命令。

1. jvmdashboard 命令的区别:

  • jvm:该命令用于显示当前的JVM信息,包括类加载信息、内存使用情况、GC信息等。它提供了一个快照视图,详细地显示了JVM的内部状态。这有助于识别资源限制或配置问题。

  • dashboard:该命令用于显示应用程序的实时数据仪表板,包括JVM信息、线程状态和吞吐量等。它显示的信息比jvm命令更加概要和可视化,主要关注应用程序的整体性能和运行状况。

总结:jvm命令关注详细的JVM信息,而dashboard命令则提供了应用程序的整体性能概览。

1.1 一个机器只有一个jvm,一个jvm内部云可能运行有多个java应用程序?

一个机器上可以运行多个JVM实例,并且每个JVM实例都可以运行一个或多个Java应用程序。每个JVM实例都是一个独立的进程,它们各自有自己的内存空间、系统资源和垃圾回收等。因此,一台机器上可以运行多个JVM实例来运行多个Java应用程序,每个JVM实例都是独立的。

简单来说,JVM实例与操作系统进程一一对应。每启动一个Java应用程序,就会启动一个新的JVM实例(即操作系统进程)。每个JVM实例都是完全独立的,具有自己的堆内存、方法区、栈内存等。不同JVM实例之间不共享这些内存区域。

注意:一台机器上可以运行多个JVM实例来运行多个Java应用程序,并且每个JVM实例都是一个独立的进程。但是,一个JVM实例内部通常只运行一个Java应用程序。如果需要在一个JVM内部运行多个Java应用程序,通常会使用应用服务器或容器(例如Tomcat、Jetty、JBoss等)来部署和管理多个Java Web应用程序(此时一个jvm中运行一个tomcat,一个tomcat运行多个java web应用程序)。

2. syspropsysenv 的区别:

  • sysprop:该命令用于显示或修改Java系统属性。系统属性是JVM启动时由命令行参数、配置文件或代码设置的键值对。它们可以在运行时使用System.getProperty方法检索,并常常用于配置应用程序行为。

  • sysenv:该命令用于显示操作系统的环境变量。环境变量是操作系统提供的键值对,通常由操作系统或shell启动脚本设置。它们可以在运行时使用System.getenv方法检索,并常常用于配置应用程序环境。

总结:sysprop命令关注Java系统属性,而sysenv命令关注操作系统的环境变量。

3 那arthas的vmoption是干什么的

vmoption 命令在 Arthas 中用于查看和修改 JVM 启动选项。JVM 启动选项是在 JVM 启动时设置的一系列参数,它们可以用于配置 JVM 的行为,例如堆大小、垃圾收集器类型、启用或禁用某些特性等。

扫描二维码关注公众号,回复: 16430169 查看本文章

使用 vmoption 命令,你可以在不重启 JVM 的情况下动态地查看或修改某些 JVM 启动选项。请注意,不是所有的 JVM 启动选项都支持动态修改,某些选项只能在 JVM 启动时设置。

使用 vmoption 命令查看当前的 JVM 启动选项:

vmoption

使用 vmoption 命令修改 JVM 启动选项(例如,启用类数据共享):

vmoption -set UseSharedSpaces=true

请注意,在使用 vmoption 命令修改 JVM 启动选项时,应该小心谨慎,因为某些选项的修改可能会对 JVM 的行为产生重大影响。在修改之前,建议先仔细阅读相关的 JVM 文档和选项描述。

4 vmoption和sysprop的区别

vmoptionsysprop 是 Arthas 中的两个不同的命令,它们分别用于查看或修改 JVM 启动选项和 Java 系统属性。虽然它们都涉及到 Java 运行时的配置,但它们的用途和作用范围不同。

  1. vmoption:

    • 用途:查看或修改 JVM 启动选项。
    • 作用范围:影响整个 JVM 的行为和配置。
    • 设置时机:通常在 JVM 启动时通过命令行参数设置。一些选项可以在运行时使用 vmoption 命令动态修改。
  2. sysprop:

    • 用途:查看或修改 Java 系统属性。
    • 作用范围:影响 Java 应用程序的行为和配置。
    • 设置时机:可以在 JVM 启动时通过命令行参数(-Dkey=value)设置,也可以在运行时使用 System.setProperty 方法设置。
    • 示例:设置文件编码(file.encoding)、设置 Java 临时目录(java.io.tmpdir)、设置用户自定义的应用配置等。
    • 访问方式:可以在 Java 代码中使用 System.getProperty 方法访问。

总结:vmoption 主要用于配置 JVM 的行为,而 sysprop 主要用于配置 Java 应用程序的行为。它们的设置方式和访问方式也有所不同。

4.1 arthas的vmoption可以设置-xx xms,-xx xmx这些参数嘛

vmoption 命令在 Arthas 中用于查看和修改JVM的启动参数。然而,并不是所有的JVM启动参数都可以在运行时进行修改。一些参数,如 -Xms-Xmx ,用于设置堆内存的初始大小和最大大小,这些参数在JVM启动时设置,运行期间不能修改。这是因为这些参数对JVM的内存管理和分配有重要影响,运行时修改可能会导致不稳定的行为。

所以,虽然 vmoption 命令可以用于查看JVM的启动参数,但并不能用于修改 -Xms-Xmx 这类参数。如果需要修改这些参数,你需要在启动JVM时进行设置。

总结,vmoption 命令可以用来查看JVM启动参数,但并不是所有参数都可以在运行时修改。需要修改这些参数的话,最好是在启动JVM的时候进行设置。

以下是可以进行设置得参数
在这里插入图片描述

4.2 有的参数是-XX:+, 有的是-XX:-,有的不带后面的加减号,分别表示什么含义?

JVM 启动参数通常以 -XX: 开头,用来设置一些 JVM 内部的选项和行为。这些参数的形式有几种:

  1. -XX:+OptionName:表示开启或启用某个选项。
  2. -XX:-OptionName:表示关闭或禁用某个选项。
  3. -XX:OptionName=value:表示设置某个选项的值。

5 sysprop只能针对单个应用程序吗,然后vmoption的参数对这个jvm内的所有java应用都有影响?

是的,你的理解是正确的。

  1. sysprop(系统属性):

    • 针对单个应用程序:系统属性是在特定的Java应用程序上下文中设置的。每个Java应用程序都有自己的系统属性集,它们只对该应用程序的运行实例有效。设置或修改系统属性不会影响其他运行在同一JVM中的应用程序。
    • 访问方式:在Java代码中,可以使用System.getProperty方法读取系统属性,或使用System.setProperty方法设置系统属性。
  2. vmoption(JVM启动选项):

    • 对JVM中所有应用程序有效:JVM启动选项是在JVM启动时设置的,并且对JVM中所有运行的Java应用程序都有效。设置或修改这些选项会影响整个JVM及其中的所有应用程序。
    • 访问方式:通常在启动JVM时通过命令行参数设置,如java -Xms512m -Xmx1024m MyApp。一些选项可以在运行时使用Arthas的vmoption命令动态修改。

总结:sysprop是针对单个Java应用程序的,而vmoption对整个JVM及其中的所有应用程序都有效。

6 sysprop只能针对单个应用程序吗,然后vmoption的参数对这个jvm内的所有java应用都有影响?

是的,你的理解是正确的。

  1. sysprop(系统属性):

    • 针对单个应用程序:系统属性是在特定的Java应用程序上下文中设置的。每个Java应用程序都有自己的系统属性集,它们只对该应用程序的运行实例有效。设置或修改系统属性不会影响其他运行在同一JVM中的应用程序。
    • 访问方式:在Java代码中,可以使用System.getProperty方法读取系统属性,或使用System.setProperty方法设置系统属性。
  2. vmoption(JVM启动选项):

    • 对JVM中所有应用程序有效:JVM启动选项是在JVM启动时设置的,并且对JVM中所有运行的Java应用程序都有效。设置或修改这些选项会影响整个JVM及其中的所有应用程序。
    • 访问方式:通常在启动JVM时通过命令行参数设置,如java -Xms512m -Xmx1024m MyApp。一些选项可以在运行时使用Arthas的vmoption命令动态修改。

总结:sysprop是针对单个Java应用程序的,而vmoption对整个JVM及其中的所有应用程序都有效。

7 arthas的jad命令是干啥的,为什么这么叫

jad命令用于将Java类文件反编译成Java源代码。它可以帮助你查看正在运行的Java应用程序的源代码,即使你没有访问原始源代码。"jad"这个名字来自于早期的一个流行Java反编译器工具,该工具也叫做JAD(Java Decompiler)。

8 mc呢?

mc(Memory Compiler)命令用于内存编译和动态加载Java类。它允许你在不重新启动Java应用程序的情况下,直接在内存中修改和重新加载Java类。这可以方便地修复Bug、调试应用程序或者测试代码更改。

9 arthas和jprofile的区别

  • Arthas:是阿里巴巴开源的一个Java诊断工具,专注于在线诊断Java应用程序。它提供了许多功能,包括查看JVM信息、反编译Java类、动态加载Java类、监控方法调用、追踪方法执行等。Arthas是一个轻量级工具,适合在生产环境中使用,不需要重新启动Java应用程序即可附加和诊断。

  • JProfiler:是一个商业的Java性能剖析器,主要用于性能分析和优化。它提供了许多功能,包括CPU分析、内存分析、线程分析、数据库分析等。JProfiler通常用于开发环境,帮助开发者找出性能瓶颈、内存泄漏和线程问题。JProfiler有图形用户界面(GUI),方便可视化分析性能数据。

总结:

  • Arthas更适合在线诊断和排查问题,不需要重新启动应用程序即可使用。
  • JProfiler更适合开发和测试环境,主要用于性能分析和优化。
  • Arthas是开源工具,JProfiler是商业工具。

10 为什么 redefine命令要设置为修改完成不可恢复?

redefine 命令的行为是基于 Java 语言的 Instrumentation 接口中的 redefineClasses 方法实现的。这个方法是 JVM 提供的一个功能,允许在运行时替换类的字节码。它通常用于热修复、热部署等场景。

当我们使用 redefine 命令时,我们实际上是在运行时替换了类的字节码。这是一个不可逆的操作,因为 JVM 不会保存原始的字节码。一旦我们重新定义了类,JVM 中的类将使用新的字节码,并且无法恢复到原始状态。

这里的不可逆性不是 Arthas 的设计决策,而是基于 JVM 的限制。redefineClasses 方法只能替换字节码,但不提供恢复原始字节码的功能。

如果需要在 redefine 之后恢复到原始的字节码,可以手动保存类的原始字节码,然后在需要恢复时使用 redefine 命令重新加载原始字节码。

需要注意的是,在重新定义类时,有一些限制。例如,不能添加、删除或修改类的字段和方法签名。这是因为这样的更改可能会导致已加载的类和新字节码之间的不兼容性,并且可能会导致运行时错误。

11 redefine

11.1 redefine命令使用方式

比如我现在想要将刚刚上传至linux的/root/tmp/目录下新的XXX.class文件进行热部署。只需要使用下面这个命令:

redefine /root/tmp/ XXX.class

11.1.1 为什么使用redefine命令时只需要一个参数呢?难道不应该指定被替换的内存.class文件的全限定名嘛

在Java中,.class文件的内部包含了类的元数据,包括类的全限定名。当Arthas使用redefine命令读取.class文件时,它会从文件的元数据中解析出类的全限定名。然后,Arthas将使用这个全限定名在运行时内存中查找相应的类,并替换其字节码。

具体来说,Java的.class文件格式包含了一个常量池(Constant Pool),其中包含了类的全限定名、方法名、字段名等信息。Arthas可以从常量池中获取到类的全限定名。

11.2 一个使用redefine实现线上故障排除的demo

热部署:Arthas的redefine命令

12 redefine为什么会和reset冲突

redefinereset 命令冲突的原因主要在于它们的工作机制和目的不同。

  1. redefine 命令:

    redefine 命令用于加载外部的 .class 文件,重新定义到 JVM 里。它是一种热替换技术,允许你在不重新启动应用程序的情况下,直接替换已加载的类的字节码。redefine 命令使用的是 JVM 的 Instrumentation 接口中的 redefineClasses 方法来实现字节码替换。

  2. reset 命令:

    reset 命令用于重置所有增强的类,恢复到没有任何 Arthas 增强(例如监视、追踪等)的原始状态。reset 命令的目的是移除 Arthas 对类的修改,还原类的字节码到应用程序启动时的状态。

冲突的原因:

当你使用 redefine 命令替换类的字节码后,Arthas 的 reset 命令不能识别这个新的字节码(因为这个新的字节码不是应用程序启动时的字节码)。因此,reset 命令不能正确地恢复 redefine 后的类到其原始状态。

同样,当你执行 redefine 命令后,如果再执行如 jadwatchtracemonitortt 等命令,则可能会把 redefine 的字节码重置,因为这些命令可能会触发类的重新加载。

为了避免这种冲突,建议在使用 redefine 命令之前先执行 reset 命令,确保所有增强的类都已重置。然后再使用 redefine 命令替换类的字节码。在替换字节码后,如果需要使用其他 Arthas 命令,请谨慎操作,避免意外地重置 redefine 后的字节码。

13 因为这个字节码是被替换的,而不是在原来的基础上修改的是嘛

是的,当使用 redefine 命令时,你实际上是完全替换了目标类的字节码,而不是在原有字节码的基础上进行修改。这就是为什么 reset 命令不能识别重新定义后的字节码的原因。

14 那使用redefine,使用了reset命令,除了被redefine的文件,其他的文件应该都能恢复到应用程序启动时的状态吧,会对应用程序产生影响嘛

是的,reset 命令将其他被 Arthas 增强的类恢复到应用程序启动时的状态。只有使用 redefine 命令替换的类不能被恢复。这不会对应用程序产生影响,只是该特定类的字节码不能被恢复。

15 如果使用了redefine,reset失效,那有什么办法恢复到应用程序启动时的状态呢?

恢复到应用程序启动时的状态的方法:

  • 手动恢复:在使用 redefine 命令之前,手动保存目标类的原始字节码。当需要恢复时,再次使用 redefine 命令,将保存的原始字节码重新加载到 JVM 中。
  • 重启应用程序:如果无法手动恢复,重启应用程序是最简单且最可靠的方法。重启应用程序会使 JVM 重新加载所有的类,从而恢复到应用程序启动时的状态。

请注意,redefine 命令适用于紧急情况下的热修复或热部署。在开发和测试阶段,推荐使用常规的开发、测试和部署流程。在生产环境中使用 redefine 命令时,请确保充分测试,并做好相应的风险评估和备份。

16 classloader命令

16.1 classloader -t 显示类加载器的层次与继承关系

±BootstrapClassLoader
±sun.misc.Launcher E x t C l a s s L o a d e r @ 6 d 6 f 6 e 28 + − c o m . t a o b a o . a r t h a s . a g e n t . A r t h a s C l a s s l o a d e r @ 464 c d 0 d 8 + − s u n . m i s c . L a u n c h e r ExtClassLoader@6d6f6e28 +-com.taobao.arthas.agent.ArthasClassloader@464cd0d8 +-sun.misc.Launcher ExtClassLoader@6d6f6e28+com.taobao.arthas.agent.ArthasClassloader@464cd0d8+sun.misc.LauncherAppClassLoader@18b4aac2
±java.net.FactoryURLClassLoader@170c01c5
Affect(row-cnt:5) cost in 1 ms.
这是 Arthas 中的 classloader 命令的输出,它显示了 JVM 中的类加载器层次结构。

在 JVM 中,类加载器是按照层次结构组织的,每个类加载器(除了引导类加载器)都有一个父加载器。这种组织方式有助于实现类加载器的双亲委派模型。

在这个例子中:

  • BootstrapClassLoader 是 JVM 的引导类加载器。它负责加载 JVM 的核心类库(例如 java.lang.* 类)。
  • sun.misc.Launcher$ExtClassLoader@6d6f6e28 是扩展类加载器。它负责加载位于 <JAVA_HOME>/jre/lib/ext 目录或由系统属性 java.ext.dirs 指定的目录中的类。
    • com.taobao.arthas.agent.ArthasClassloader@464cd0d8 是 Arthas 的类加载器。它负责加载 Arthas 自身的类。
    • sun.misc.Launcher$AppClassLoader@18b4aac2 是应用程序类加载器。它负责加载应用程序类,通常位于类路径(classpath)上。
      • java.net.FactoryURLClassLoader@170c01c5 是一个自定义的类加载器。它可能用于加载额外的资源或插件。

每行的前缀 +- 表示该类加载器的层次结构。+ 表示它有子类加载器,- 表示它没有子类加载器。

这个输出非常有用,因为它可以帮助你了解类加载器的结构和层次,从而更好地理解类是如何被加载的。这对于诊断类加载相关的问题(例如类版本冲突、类未找到、类转换异常等)非常有用。

16.2 在双亲委派模型中,BootstrapClassLoader 是最顶层的加载器,ExtClassLoader是它的子类吧?为什么这里显示两个同级?

在双亲委派模型中,BootstrapClassLoader 是最顶层的加载器,但它是一个特殊的加载器,不是 Java 中的一个对象。因此,在类加载器的层次结构中,BootstrapClassLoader 通常不会显示为其他类加载器的父加载器。

具体来说,BootstrapClassLoader 是 JVM 的一部分,它是用 C++ 编写的,并且不暴露为 Java 对象。这是为什么你在 Java 程序中无法获取到 BootstrapClassLoader 的实例的原因。

在 Java 中,所有其他的类加载器都是 java.lang.ClassLoader 类的实例。ExtClassLoaderAppClassLoaderURLClassLoader 的实例,而 URLClassLoaderClassLoader 的子类。在 Java 的类加载器层次结构中,ExtClassLoaderAppClassLoader 的父加载器。

在 Arthas 的 classloader 命令的输出中,BootstrapClassLoader 显示在最顶层,并且与 ExtClassLoader 并列显示。这是因为 BootstrapClassLoader 不是 Java 对象,它不会作为其他加载器的父加载器显示。但实际上,BootstrapClassLoader 是最顶层的加载器,ExtClassLoader 是它的子类。

17 watch个monitor命令的区别

18 ttwatch

18.1 ttwatch 的区别

ttwatch 都是 Arthas 中的强大命令,它们在某些方面有相似之处,但主要的使用场景和功能有所不同。以下是它们之间的主要区别:

tt (TimeTunnel) 命令:

  1. 记录调用数据tt 命令是为了记录方法的调用数据,包括入参、出参、异常等,并将这些数据存储在一个“时间隧道”中。

  2. 查看历史记录:您可以随时使用 tt 命令来查看已经记录的调用数据,而不仅仅是最近的一次。

  3. 重放方法调用tt 命令的一个强大特点是能够重放方法调用,这有助于重现问题并进行调试。

  4. 不支持实时显示tt 命令不会实时显示方法调用的结果,除非您明确地查看已经记录的数据。

watch 命令:

  1. 实时观察watch 命令的主要用途是实时观察方法的调用数据。每次当目标方法被调用时,都会触发 watch 命令,并立即显示结果。

  2. 支持表达式watch 命令支持 OGNL 表达式,这使得它可以非常灵活地显示方法的调用数据,例如参数、返回值、异常等。

  3. 不记录历史数据:与 tt 命令不同,watch 命令不会记录历史的方法调用数据。它仅仅关注实时的数据。

总结:

  • 如果您需要记录方法的调用数据并可能稍后查看或重放它,那么 tt 命令更为合适。

  • 如果您想实时观察方法的调用数据,或者在特定条件下查看方法的调用数据(例如当方法抛出异常时),那么 watch 命令更为合适。

尽管它们有所不同,但在许多调试和诊断任务中,ttwatch 命令经常一起使用,以获得更完整的信息。

18.2 为什么有了watch命令还需要有tt命令?

在我们调试应用程序时,有时候我们可能不知道问题的根源在哪里。tt 命令可以帮助我们记录方法的调用情况,包括输入参数、返回值和抛出的异常等信息。这样,我们就可以随时查看这些信息,而不必再花费时间来重现问题或者猜测可能出现问题的地方。

具体而言,tt 命令允许我们对目标方法的调用情况进行“快照”,并将这些信息保存在一个“时间隧道”中。稍后,我们可以使用 tt 命令查看这些快照,并对它们进行分析,找出问题的根源。甚至还可以通过 tt 命令的重放功能,重新执行方法调用,从而更深入地了解问题的原因。

总之,tt 命令是一个非常强大的工具,能够帮助我们更轻松地定位和解决问题。当然,与任何其他工具一样,为了更好地使用它,我们需要对它的功能和限制有一定的了解。

19 profiler

19.1 为什么需要火焰图:

火焰图(Flame Graph)是一种可视化的工具,用于展示代码在程序运行过程中所占用的CPU资源。火焰图的优势在于它能够直观地展示程序中各个函数的调用栈以及它们在CPU上的占用情况。火焰图的可视化形式让开发者能够更容易地定位程序中的性能瓶颈,并迅速诊断性能问题。火焰图对于代码的性能优化和程序的调试都是非常有用的工具。

19.2 火焰图表示的是单cpu还是所有cpu的繁忙程度:

火焰图可以表示单个CPU或所有CPU的繁忙程度。这取决于如何收集火焰图的数据。

  • 单CPU火焰图:当你只关注单个CPU的性能瓶颈时,可以使用火焰图表示单个CPU的繁忙程度。这是常见的应用场景,尤其是在调试单线程程序时。

  • 所有CPU火焰图:当你需要对整个系统进行性能分析时,可以收集所有CPU的数据,生成展示所有CPU繁忙程度的火焰图。这种火焰图可以显示整个系统的性能分布,有助于了解整体的性能瓶颈。

在生成火焰图时,你可以选择需要收集的数据范围(单个CPU或所有CPU),根据自己的需求来决定。

19.3 为什么“火焰图就是看顶层的哪个函数占据的宽度最大。只要有"平顶"(plateaus),就表示该函数可能存在性能问题。”

y 轴表示调用栈,每一层都是一个函数。调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数。

x 轴表示抽样数,如果一个函数在 x 轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长。注意,x
轴不代表时间,而是所有的调用栈合并后,按字母顺序排列的。

**火焰图就是看顶层的哪个函数占据的宽度最大。只要有"平顶"(plateaus),就表示该函数可能存在性能问题。

答:
火焰图(Flame Graph)是一种可视化技术,用于展示软件执行中栈的性能瓶颈。火焰图由 Brendan Gregg 发明并广泛应用于性能调优和性能瓶颈分析。火焰图提供了一个更加直观、高效的方式来展示程序的调用栈及其性能。

火焰图的Y轴表示调用栈,每一层都是一个函数。调用栈越深,火焰就越高。顶部是当前正在执行的函数,下方都是它的父函数。X轴不是时间,而是按字母顺序排列的所有调用栈合并后的抽样数。一个函数在X轴上占据的宽度越宽,表示它被抽到的次数多,即执行的时间长。

火焰图的核心理念是将调用栈的数据进行可视化,使得开发者能够更快地发现性能瓶颈。火焰图通过将栈数据可视化成一个彩色的图形,使得性能瓶颈变得非常明显。

“平顶”(plateaus)是火焰图中比较宽的部分,表示该函数在整个执行过程中被多次抽样,意味着该函数的执行时间较长。因此,平顶通常表示性能瓶颈。解决平顶函数中的性能问题,通常可以显著提高整个程序的性能。

火焰图的优势在于它能迅速定位到性能瓶颈所在的代码位置。由于火焰图将函数调用栈可视化,开发者可以迅速找到导致性能问题的函数,并进行优化。

19.4 那底层的函数呢?如果底层是平顶呢?

如果底层的函数是平顶,它可能不一定是性能瓶颈。在火焰图中,底层通常是根调用,它可能包括main函数或其他系统调用。底层函数的宽度很可能是因为它被多次调用,但它本身不一定是性能瓶颈。

在性能分析中,我们通常更关注的是顶层的函数,因为它们的执行时间可能包括了其子函数的执行时间。顶层函数的宽度表示该函数及其所有子函数的总执行时间。如果顶层函数是平顶的,那么它可能是性能瓶颈。

而底层函数的宽度只表示该函数自身的执行时间。即使底层函数是平顶的,它可能只是被频繁调用,但每次调用的执行时间可能很短。因此,我们通常不会将底层函数视为性能瓶颈。

总之,火焰图中的平顶主要是用来找出顶层函数的性能瓶颈,底层函数的平顶通常不是我们关注的重点。在性能优化时,我们应该更关注那些在顶层且宽度较大的函数。

20 使用arthas诊断案例

20.1 使用arthas确定某一个耗时的请求来自哪一个controller,并且分析以及代码优化

20.1.1 为什么要做第一步的“确定请求来源的controller”?

分析:首先我们需要知道我们想要分析哪一个请求,比如用户登录请求,其次我们需要了解springMVC的特性,我们知道所有的请求都会走DispatcherServet这个类,然后会从一个getHandler的方法中返回给DispatcherServet处理该请求的controller。

20.1.2 第一阶段:找到具体的处理方法

  1. 所以我们的重点就是直接使用watch命令观测这个getHandler方法的输入参数和返回值;
watch org.springframework.web.servlet.DispatcherServlet getHandler 'returnObj'
  1. 然后在前端或者postman上触发一次这样的请求,比如我这里触发登录请求。

3. 查看得到的哪些controller:
通过下面的标红的部分,可知这个登录请求经过了UserController控制器的login方法,和StudentController控制器的findAll方法。
在这里插入图片描述

20.1.3 第二阶段:分析

  1. 入参和返回值分析:使用watch命令观察这个具体的controller中的处理方法的入参和返回值
watch com.itheima.controller.* * '{params,returnObj}' -x 2

得到的结果如下所示:

method=com.itheima.controller.UserController.login location=AtExit
ts=2023-08-21 20:55:52; [cost=5.2786ms] result=@ArrayList[
    @Object[][
        @User[User{
    
    id=null, name='newboy', password='123'}],
        @StandardSessionFacade[org.apache.catalina.session.StandardSessionFacade@2a57d000],
    ],
    @String[forward:/student/list],
]
method=com.itheima.controller.StudentController.findAll location=AtExit
ts=2023-08-21 20:55:52; [cost=6.0088ms] result=@ArrayList[
    @Object[][isEmpty=true;size=0],
    @ModelAndView[
        view=@String[list],
        model=@ModelMap[isEmpty=false;size=1],
        status=null,
        cleared=@Boolean[false],
    ],
]
  1. 调用链路和节点耗时分析:使用trace命令得到这个控制器处理方法的调用链路以及各个节点上耗费的时间

trace com.itheima.controller.* login

在这里插入图片描述
我们发现这个controller调用了"com.itheima.service.UserService:login()"这个最耗时的方法

  1. 于是我们可以继续分析这个业务层的login方法

trace com.itheima.service.UserService login

结果如下所示:我们发现最耗时的还是访问数据库的com.itheima.dao.UserDao:login()方法,由此可见,IO操作往往占用处理一个请求的绝大多数时间

Affect(class count: 3 , method count: 2) cost in 78 ms, listenerId: 12
`---ts=2023-08-21 21:10:42;thread_name=http-nio-8080-exec-3;id=1a;is_daemon=true;priority=5;TCCL=org.apache.catalina.loader.ParallelWebappClassLoader@64e2f243
    `---[7.7167ms] com.sun.proxy.$Proxy25:login()
        `---[28.93% 2.2322ms ] com.itheima.service.impl.UserServiceImpl:login()
            `---[98.83% 2.206ms ] com.itheima.dao.UserDao:login() #17
  1. 查看这个生成的代理方法,并且使用jad命令反编译

jad com.sun.proxy.$Proxy24 login

ClassLoader:

  +-java.net.URLClassLoader@1c4af82c
    +-sun.misc.Launcher$AppClassLoader@764c12b6
      +-sun.misc.Launcher$ExtClassLoader@3d82c5f3

Location:

public final User login(User user) {
    
    
    try {
    
    
        return (User)this.h.invoke(this, m3, new Object[]{
    
    user});
    }
    catch (Error | RuntimeException throwable) {
    
    
        throw throwable;
    }
    catch (Throwable throwable) {
    
    
        throw new UndeclaredThrowableException(throwable);
    }
}

20.1.4 第三阶段:代码优化

  1. sql优化:

注意:此项目中最耗费时间的方法是一个路径为com.sun.proxy.$Proxy25:login()的代理方法,这个方法是mybatis插件生成的,一般我们不能对其进行改动,如果访问数据库确实非常耗时,那也是去修改sql语句以及调整数据库的相关参数

  1. 如果是处理请求的业务层方法非常耗时,那一般我们的代码优化涉及到修改字节码文件,所以需要事先使用dump命令将这个业务方法所在的字节码文件保存到另一个目录中;假设我们第6步中的得到具体实现类-com.itheima.service.impl.UserServiceImpl:login()方法非常耗时,我们现在需要改进他

保存UserServiceImpl这个类到另一个目录以便恢复,

dump com.itheima.service.impl.UserServiceImpl > /root/UserServiceImpl.java

反编译UserServiceImpl实现类到指定目录,

jad --source-only com.itheima.service.impl.UserServiceImp > /root/UserServiceImpl.java

在本地idea或者在vim编辑器中修改 /root/UserServiceImpl.java文件,并且使用mc命令编译这个java文件到/root/bbb/目录下

mc -d /root/bbb/ /root/UserServiceImpl.java

使用redefine命令加载新的字节码

redefine /root/bbb/UserServiceImpl.class

20.2 根据一篇知乎的调优总结依次定位对应的命令

面试官:如何进行 JVM 调优(附真实案例)

20.2.1 基础知识提问

20.2.1.1 arthas的boot程序运行后,首先要选择一个java进程,然后使用arthas的dashboard命令,这里的dashboard展示的是这个进程的宏观运行情况嘛

是的,Arthas 的 dashboard 命令提供了一个宏观的实时视图,展示了选定 Java 进程的运行状况和关键指标。这个视图可以帮助开发者快速了解 Java 进程的整体状况和性能表现。

dashboard 命令展示的信息包括但不限于:

  1. 系统负载:展示系统的 CPU 使用率、内存使用情况、线程数等信息。

  2. JVM 信息:展示 JVM 的堆内存使用情况、非堆内存使用情况、垃圾回收情况等信息。

  3. 线程信息:展示 Java 进程中的线程状态分布,包括运行中的线程、阻塞的线程、等待的线程等。

  4. Tomcat 信息:如果 Java 进程是一个 Tomcat 服务器,还会展示 Tomcat 的运行状况和关键指标。

通过这些信息,开发者可以快速了解 Java 进程的运行状况,发现性能瓶颈或故障迹象,并根据需要采取进一步的诊断和优化措施。

20.2.1.2 下面是dashboard关于java进程的内存使用信息,这里的总内存是jvm给这个应用程序分配的内存嘛还是jvm的整个内存

Memory                    used    total    max     usage    GC
heap                      60M     243M     3593M   1.69%    gc.ps_scavenge.count          1
ps_eden_space             49M     63M      1326M   3.76%    gc.ps_scavenge.time(ms)       6
ps_survivor_space         10M     10M      10M     99.71%   gc.ps_marksweep.count         0
ps_old_gen                481K    173568K          0.02%    gc.ps_marksweep.time(ms)      0
nonheap                   28M     29M      -1      97.16%
code_cache                6M      6M       240M    2.74%
metaspace                 19M     20M      -1      96.72%
compressed_class_space    2M      2M       1024M   0.23%

这里的 “total” 是指 JVM 给这个应用程序分配的堆内存。JVM 在启动时会为应用程序分配一定量的堆内存,这个值是可以在启动参数中进行配置的。“total” 值会随着程序的运行逐渐增加(直到达到 “max” 值)。当 “total” 的值增加时,表示应用程序需要更多的内存来存储对象。

总结一下:

  • “used” 是应用程序已经使用的堆内存。
  • “total” 是JVM当前为应用程序分配的堆内存。
  • “max” 是JVM最多可以为应用程序分配的堆内存。
  • “usage” 是使用率,计算方式是 “used” / “max”。

20.2.1.3 jvm下面这些参数的设置,都只针对某一个java进程的嘛而不是整个jvm虚拟机中的所有进程嘛?

-Xms128m
-Xmx1024m
-XX:ReservedCodeCacheSize=512m
-XX:+IgnoreUnrecognizedVMOptions
-XX:+UseG1GC
-XX:SoftRefLRUPolicyMSPerMB=50
-XX:CICompilerCount=2
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow 

是的,这些 JVM 参数都只针对一个特定的 Java 进程,而不是整个 JVM 虚拟机。JVM 参数在启动 Java 应用程序时指定,并且仅对该进程有效。每个 Java 进程都有自己的 JVM 实例,这些参数对于每个 JVM 实例来说都是独立的。

简单解释一下这些参数:

  • -Xms128m: 设置 JVM 堆内存的初始大小为 128MB。
  • -Xmx1024m: 设置 JVM 堆内存的最大大小为 1024MB。
  • -XX:ReservedCodeCacheSize=512m: 设置代码缓存区的大小为 512MB。
  • -XX:+IgnoreUnrecognizedVMOptions: 允许 JVM 忽略无法识别的 JVM 选项。
  • -XX:+UseG1GC: 使用 G1 垃圾回收器。
  • -XX:SoftRefLRUPolicyMSPerMB=50: 设置软引用对象的最长空闲时间。
  • -XX:CICompilerCount=2: 设置即时编译器的线程数为 2。
  • -XX:+HeapDumpOnOutOfMemoryError: 在发生内存溢出错误时生成堆转储文件。
  • -XX:-OmitStackTraceInFastThrow: 在抛出某些常见异常时不省略堆栈信息。

这些参数都是 Java 虚拟机 (JVM) 启动参数,用于配置和调整 Java 应用程序的运行环境。不同的 Java 进程可以有不同的启动参数。

20.2.2 cpu指标

查看占用CPU最多的进程
查看占用CPU最多的线程
查看线程堆栈快照信息
分析代码执行热点
查看哪个代码占用CPU执行时间最长
查看每个方法占用CPU时间比例

在 Arthas 中,你可以使用以下命令来解决你所提到的问题:

  1. 查看占用CPU最多的进程:

    • 使用操作系统的命令如topps查看。Arthas 没有特定的命令来查看进程的 CPU 使用情况。
  2. 查看占用CPU最多的线程;查看被死锁的几个线程:

    • thread命令可以查看 Java 进程中的线程信息,包括线程的状态,并按照 CPU 使用情况进行排序。
      在这里插入图片描述
  3. 查看线程堆栈快照信息:

    • thread命令可以查看 Java 进程中的某一个具体线程信息,包括线程的堆栈信息。
      在这里插入图片描述
  4. 分析代码执行热点:

    • profiler命令可以分析 Java 应用程序的执行热点,并生成火焰图。
    • trace命令可以追踪某个类的某一个(些)方法调用,并查看内部调用链路信息,并给出每一个节点的耗时和占比

在这里插入图片描述

  1. 查看哪个代码占用CPU执行时间最长:

    • profiler命令可以分析 Java 应用程序的执行情况,并生成火焰图。
    • watch命令可以观察方法的调用和返回情况,并可以计算方法的执行时间。
  2. 查看每个方法占用CPU时间比例:

    • profiler命令可以分析 Java 应用程序的执行情况,并生成火焰图。
    • trace命令可以分析调用链路上各个节点所代表方法的耗时和占比
      在这里插入图片描述

需要注意的是,profiler命令的火焰图是基于统计信息生成的,它能够显示各个方法的执行时间比例。火焰图可以帮助你找到性能瓶颈和优化代码的热点。

20.2.2 JVM 内存指标

查看当前 JVM 堆内存参数配置是否合理
查看堆中对象的统计信息
查看堆存储快照,分析内存的占用情况
查看堆各区域的内存增长是否正常
查看是哪个区域导致的GC
查看GC后能否正常回收到内存

在 Arthas 中,以下命令可以帮助你解决提到的问题:

  1. 查看当前 JVM 堆内存参数配置是否合理:

    • jvm命令可以查看 JVM 的各项参数配置和运行时信息,包括堆内存、非堆内存、GC等信息。
    • vmoption命令可以
  2. 查看堆中对象的统计信息:

    • heapdump命令可以生成堆存储快照。然后,可以使用外部工具(例如MAT)分析快照文件来查看对象的统计信息。
    • scsm 命令可以查看系统加载的类和方法的信息,但它们并不能提供对象的详细统计信息。
  3. 查看堆存储快照,分析内存的占用情况:

    • heapdump命令可以生成堆存储快照。然后,可以使用外部工具(例如MAT)分析快照文件来查看内存占用情况。
  4. 查看堆各区域的内存增长是否正常:

    • dashboard命令提供了堆内存的简单概览,可以显示各个堆区域的内存使用情况。
    • jvm命令也可以查看堆内存的各个区域的使用情况。
  5. 查看是哪个区域导致的GC:

    • dashboard命令也显示了GC的统计信息。
Memory                    used    total    max     usage    GC
heap                      60M     243M     3593M   1.69%    gc.ps_scavenge.count          1
ps_eden_space             49M     63M      1326M   3.76%    gc.ps_scavenge.time(ms)       6
ps_survivor_space         10M     10M      10M     99.71%   gc.ps_marksweep.count         0
ps_old_gen                481K    173568K          0.02%    gc.ps_marksweep.time(ms)      0
nonheap                   28M     29M      -1      97.16%
code_cache                6M      6M       240M    2.74%
metaspace                 19M     20M      -1      96.72%
compressed_class_space    2M      2M       1024M   0.23%

  1. 查看GC后能否正常回收到内存:
    • dashboard命令在显示内存使用情况的时候,可以看到GC后是否有内存释放。
    • jvm命令也可以查看堆内存的各个区域的使用情况,以及GC的统计信息。

需要注意的是,Arthas 提供的是一种在运行时诊断和分析 Java 应用程序的方式。它可以帮助你查看和收集一些信息,但更深入的分析和诊断可能需要借助其他工具,例如内存分析工具(MAT)、代码分析工具、性能分析工具等。

  1. 生成 Java 堆存储快照dump文件并存储到指定目录中

heapdump /path/to/dumpFile.hprof

20.2.3 JVM GC指标

查看每分钟GC时间是否正常
查看每分钟YGC次数是否正常
查看FGC次数是否正常
查看单次FGC时间是否正常
查看单次GC各阶段详细耗时,找到耗时严重的阶段
查看对象的动态晋升年龄是否正常

以上参数都可以通过dashboard命令查看,比如下面这张内存使用情况表

Memory                    used    total    max     usage    GC
heap                      73M     243M     3593M   2.04%    gc.ps_scavenge.count          1
ps_eden_space             62M     63M      1326M   4.71%    gc.ps_scavenge.time(ms)       6
ps_survivor_space         10M     10M      10M     99.71%   gc.ps_marksweep.count         0
ps_old_gen                481K    173568K          0.02%    gc.ps_marksweep.time(ms)      0
nonheap                   29M     30M      -1      97.61%
code_cache                6M      7M       240M    2.91%
metaspace                 20M     20M      -1      97.65%
compressed_class_space    2M      2M       1024M   0.24%

在上述的 dashboard 命令的输出中,我们可以找到以下信息来回答你的问题:

  1. 查看每分钟GC时间是否正常

    • gc.ps_scavenge.time(ms) 反映了 Minor GC(YGC)的总耗时。
    • gc.ps_marksweep.time(ms) 反映了 Full GC(FGC)的总耗时。
  2. 查看每分钟YGC次数是否正常

    • gc.ps_scavenge.count 反映了 Minor GC(YGC)的次数。
  3. 查看FGC次数是否正常

    • gc.ps_marksweep.count 反映了 Full GC(FGC)的次数。
  4. 查看单次FGC时间是否正常

    • 该信息不包含在上述输出中。为了查看单次FGC时间,你需要查看日志文件或使用其他工具,比如 jstat
  5. 查看单次GC各阶段详细耗时,找到耗时严重的阶段

    • 该信息不包含在上述输出中。为了查看各个阶段的详细耗时,你需要使用其他工具,如 GC日志、jstat 或可视化工具(如 JVisualVM)。
  6. 查看对象的动态晋升年龄是否正常

    • 该信息不包含在上述输出中。要查看对象的动态晋升年龄,你需要使用其他工具,如 jstat 或可视化工具(如 JVisualVM)。

注意:在上述输出中,我们可以看到堆内存的使用情况,以及不同区域的内存占用情况,但这些信息并不直接反映 GC 的详细行为。为了更深入地分析 GC 的行为,您可能需要使用其他的工具或分析 GC 日志。

问:w什么ps_scavenge能反映ygc,ps_marksweep反映FGC?

这里的名字是由Java HotSpot虚拟机使用的两个垃圾回收器的名字派生而来的。

  1. PS Scavenge(Parallell Scavenge):这是一个用于年轻代的垃圾回收器,它是并行的,并且主要用于清理年轻代中的对象。因此,gc.ps_scavenge.time(ms) 反映了 Minor GC(YGC)的总耗时,即年轻代的垃圾回收时间。

  2. PS MarkSweep:这是一个用于老年代的垃圾回收器,它在标记和清理阶段中是串行的,在CMS中,有可能会触发一次YGC操作,而且YGC的时间相对来说比较小。该收集器通常用于在应用程序的全局暂停时进行垃圾回收。因此,gc.ps_marksweep.time(ms) 反映了 Full GC(FGC)的总耗时,即整个堆(包括年轻代和老年代)的垃圾回收时间。

这两个回收器通常一起使用,并且组成了Java HotSpot虚拟机中默认的垃圾回收策略。

20.2.4 JNI进行反射和字节码进行反射的区别?(涉及到方法区/元空间的OOM问题)

背景:

元空间主要适用于存放类的相关信息,而存在内存碎片化说明很可能创建了较多的类加载器,同时使用率较低。

因此,当元空间出现内存碎片化时,我们会着重关注是不是创建了大量的类加载器。

在 JVM 上,最初是通过 JNI 调用来实现方法的反射调用,当 JVM 注意到通过反射经常访问某个方法时,它将生成字节码来执行相同的操作,称为膨胀(inflation)机制。如果使用字节码的方式,则会为该方法生成一个 DelegatingClassLoader,如果存在大量方法经常反射调用,则会导致创建大量 DelegatingClassLoader。

反射调用频次达到多少才会从 JNI 转字节码?

默认是15次,可通过参数 -Dsun.reflect.inflationThreshold 进行控制,在小于该次数时会使用 JNI 的方式对方法进行调用,如果调用次数超过该次数就会使用字节码的方式生成方法调用。

分析结论:反射调用导致创建大量 DelegatingClassLoader,占用了较大的元空间内存,同时存在内存碎片化现象,导致元空间利用率不高,从而较快达到阈值,触发 FGC。

1 元空间主要适用于存放类的相关信息,而存在元空间碎片化或者OOM说明很可能创建了较多的类加载器,同时使用率较低。为什么?

元空间(Metaspace)主要用于存储类的元数据信息,包括类的字节码、方法信息、字段信息等。当创建了大量的类加载器,并且这些类加载器加载了许多类时,元空间中的使用率可能会变得较低。可能有以下几个原因:

  1. 短命的类加载器:如果有很多短命的类加载器,那么它们会在一段时间内加载类并在完成任务后即被回收。这可能会导致元空间中的内存碎片化。

  2. 动态代码生成:某些框架和库会动态生成和加载类,这可能会导致大量的类被加载到元空间,进一步导致内存碎片化。

  3. 不合适的类加载器设计:如果应用程序中存在不合适的类加载器设计,例如不合理的类加载器层次结构或过多的自定义类加载器,那么可能会导致元空间的内存碎片化。

  4. 频繁的类卸载:如果应用程序频繁地加载和卸载类,那么可能会导致元空间中的内存碎片化。

  5. 类加载器泄露:如果类加载器持有了对类的引用,并且没有被正确地回收,那么可能会导致元空间中的内存碎片化。

总之,元空间的内存碎片化通常是由于类加载器的不合理设计、动态代码生成、短命的类加载器、频繁的类加载和卸载等原因导致的。解决这种问题的方法包括合理设计类加载器、避免不必要的动态代码生成、合理管理类加载器等。

2 为什么反射调用某一个方法的频次过多就需要使用字节码加载机制呢,这不是消耗空间的创建新的类字节码加载器机制嘛?

当反射调用一个方法时,JVM 的初始方式是使用 JNI(Java Native Interface)来执行调用。然而,JNI调用对性能有一定的开销,因此当一个方法被反复反射调用时,JVM会采取一种优化措施,即通过生成一个新的 Java 类,使用字节码的方式替代JNI方式进行反射调用。这种优化机制被称为"膨胀"(inflation)。

这里的新生成的 Java 类是一个代理类,专门用于快速执行反射调用。为了加载这个新生成的 Java 类,JVM 需要创建一个新的类加载器(DelegatingClassLoader)。这种做法是为了确保类加载的隔离和安全性。不同的反射调用应该在各自独立的类加载器下进行,以避免类加载器之间的冲突和干扰。

需要注意的是,这个新的类加载器并不是为了每次反射调用都创建一个新的。相反,一旦创建并加载了新生成的 Java 类,它就可以被多次反射调用共享。但是,如果有很多不同的方法被频繁地反射调用,JVM 会为这些不同的方法分别生成新的 Java 类,并创建相应的DelegatingClassLoader来加载这些新生成的类。这种做法可能会导致大量的DelegatingClassLoader被创建,可能会对内存产生一定的压力

总的来说,这种做法的目的是为了提高反射调用的性能,但是它可能会带来额外的内存开销。在实际应用中,如果反射调用非常频繁,建议考虑使用其他技术来替代反射,以减少性能开销和内存消耗。

3 为什么每一次反射都需要创建一个DelegatingClassLoader,直接复用第一个不行吗(事实上当创建了一个DelegatingClassLoader后,后续的反射都会复用这个类加载器)

实际上,并不是每一次反射都需要创建一个新的DelegatingClassLoader 在 JVM 中,当反射调用一个方法时,初始阶段通常会使用 JNI (Java Native Interface) 方式进行调用。当某个方法通过反射被频繁调用时(通常是超过15次,可以通过-Dsun.reflect.inflationThreshold参数控制),JVM 会触发膨胀(inflation)机制,动态生成一个新的 Java 类,并用字节码的方式替代 JNI 方式进行反射调用。

这个新生成的 Java 类需要被加载到 JVM 中,因此会创建一个DelegatingClassLoader来加载这个新的 Java 类。这是为了隔离不同的反射调用,确保它们在各自独立的类加载器下进行,这样就可以避免类加载器之间的冲突和干扰。一旦这个类加载器被创建并加载了新生成的 Java 类,它就可以被多次反射调用共享,不会为每次反射调用都创建一个新的DelegatingClassLoader

但如果有大量的方法被频繁地反射调用,JVM 就会为这些不同的方法分别生成新的 Java 类,并创建相应的DelegatingClassLoader来加载这些新生成的类,这就可能导致大量的DelegatingClassLoader被创建。

4 JNI的方式可能涉及到OS的系统调用,甚至是磁盘IO,还有可能是与其他语言交互,这些都比较耗费时间,所以jvm采用了空间换取时间的方法对吗?

部分正确。JNI(Java Native Interface)是一个框架,允许Java代码与本地代码(如C或C++代码)互相调用。通过JNI,Java代码可以调用本地方法,反之亦然。使用JNI,开发者可以使用本地库来执行一些Java可能无法高效执行的任务,或者访问系统级资源。

JNI调用确实有一些开销,主要是因为需要在Java虚拟机和本地代码之间进行上下文切换。但是,这些开销通常与操作系统的系统调用或磁盘I/O无关。JNI的主要开销来自于必须进行的数据转换和上下文切换。例如,当Java代码调用一个本地方法时,可能需要将Java对象转换为本地数据结构,或者在虚拟机和本地代码之间切换执行环境。

当JVM检测到一个方法经常通过反射调用时,它会采用膨胀(inflation)机制,通过生成Java字节码来替代JNI调用。这样做的目的确实是为了提高性能,因为字节码的执行速度通常比JNI调用更快。然而,这种做法会增加内存开销,因为需要为每个经常反射调用的方法生成一个新的Java类,并创建一个新的类加载器来加载这个类。

总的来说,JVM采用膨胀机制是为了提高反射调用的性能,但这并不是为了避免系统调用或磁盘I/O。而是为了避免JNI调用带来的数据转换和上下文切换开销。

5 为什么jdk创建快但是执行时相对慢,cglib刚好相反
JDK动态代理和CGLIB动态代理的性能差异主要来自于它们的实现方式和适用场景。

  1. JDK动态代理:基于接口进行代理,这意味着只需要为接口创建一个代理类,代理类会实现被代理对象的接口,并将所有方法调用转发到一个InvocationHandler。所以代理类的创建速度相对较快。然而,在每次方法调用时,都需要通过InvocationHandler的invoke方法来进行反射调用,这个过程比较耗时。

  2. CGLIB动态代理:基于继承进行代理,对于每个被代理的类,都会生成一个继承自该类的子类,子类覆盖了父类的所有非final的方法,并在覆盖方法中添加增强代码。所以创建代理对象时需要生成新的类和字节码,速度相对较慢。但是在运行时,直接调用覆盖方法的代码,不需要经过反射,因此执行速度较快。

这就是为什么JDK代理创建快但执行慢,CGLIB代理创建慢但执行快的原因。但是,随着JVM的优化,这两种代理方式的性能差异逐渐缩小。在选择代理方式时,还应该考虑其他因素,例如是否有接口,是否需要对特定方法进行代理等。

猜你喜欢

转载自blog.csdn.net/yxg520s/article/details/132381305