JVM调优

这个文章会依次表述本人经过的实践

首先是如何去发现问题,为什么要去调优?

我之前在做开源项目的时候,碰到了需要优化字节码大小问题的需求,所以,这里就用到了虚拟机优化的知识:
在VM option里面增加参数

1
2
3
-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintInlining

这样执行代码后,命令行会打印出运行时的内存大小

  1. -XX:+UnlockDiagnosticVMOptions
    • 用途
      • 这个选项主要是用于解锁Java虚拟机(JVM)中的一些诊断相关的功能选项。在JVM的默认配置中,很多用于深度诊断和性能分析的选项是被限制使用的,目的是防止因误操作或不恰当的设置对JVM的正常运行产生负面影响。而通过使用-XX:+UnlockDiagnosticVMOptions,就像是打开了一个“高级功能开关”,使得后续可以使用其他更具深度的诊断选项,例如-XX:+PrintCompilation-XX:+PrintInlining
    • 示例
      • 假设你在没有添加-XX:+UnlockDiagnosticVMOptions的情况下,尝试在JVM启动命令中添加-XX:+PrintInlining来查看方法内联的详细信息。JVM会将这个选项忽略,因为这些诊断选项默认是被锁住的。只有在添加了-XX:+UnlockDiagnosticVMOptions后,JVM才会识别并启用-XX:+PrintInlining这个用于打印内联信息的选项。
  2. -XX:+PrintCompilation
    • 用途
      • 编译活动监控:它用于输出JVM中即时编译器(JIT)的编译活动情况。当JVM运行时,会根据一定的策略(如方法的调用频率、循环执行次数等)对字节码进行编译成机器码,这个过程是由JIT编译器完成的。-XX:+PrintCompilation可以让你看到哪些方法被编译、编译的顺序、编译的时间等信息。例如,你可以看到类似“300 3 java.util.HashMap::put (152 bytes)”的输出,其中“300”可能是一个编译任务编号,“3”可能是编译的级别或者版本相关信息,“java.util.HashMap::put”是被编译的方法名称及签名,“(152 bytes)”表示这个方法字节码的大小。
      • 性能瓶颈发现:通过观察方法的编译情况,有助于定位性能瓶颈。如果某个方法被频繁编译,那么这个方法很可能是程序中的热点方法,也就是对性能影响较大的方法。例如,在一个Web应用程序中,如果发现某个业务逻辑处理方法被反复编译,就可以重点关注这个方法的代码实现,看是否可以通过优化算法、减少嵌套层次或者调整数据结构等方式来提高性能。
      • 理解JIT策略:可以帮助开发人员理解JVM的JIT编译策略。不同的JVM实现(如HotSpot)有自己的一套编译策略,比如根据方法的热度(调用频率和执行时间)来决定何时进行编译。通过-XX:+PrintCompilation输出的信息,开发人员可以了解JVM是如何根据实际运行情况来动态优化代码的。
    • 示例
      • 考虑一个大型的企业级Java应用,它包含了复杂的业务逻辑和数据处理模块。在运行该应用并添加了-XX:+PrintCompilation选项后,发现一个名为calculateCustomerDiscount的方法频繁被编译。这就提示开发人员这个方法可能是性能敏感点,进一步查看这个方法的代码后,发现其中包含了复杂的嵌套循环和大量的条件判断。于是开发人员可以对这个方法进行优化,比如简化循环结构或者提前计算一些可以复用的数据,从而提高整个应用的性能。
  3. -XX:+PrintInlining
    • 用途
      • 方法内联信息展示:这个选项用于打印JVM中方法内联的相关信息。方法内联是JIT编译器的一种重要优化技术。简单来说,当一个方法被频繁调用,并且满足一定的条件(如方法体较小、执行时间短等)时,JIT编译器会把被调用方法的代码直接嵌入到调用者的方法体中,从而减少方法调用的开销(如参数传递、栈帧创建等)。-XX:+PrintInlining输出的信息可以让你清楚地看到哪些方法被内联了,以及内联的具体情况,如内联的深度、哪些方法因为内联而被优化等。
      • 性能优化指导:通过查看内联信息,开发人员可以更好地理解JVM是如何对代码进行优化的,并且可以根据这些信息来调整代码结构,使代码更符合JVM的优化策略。例如,如果发现某个关键方法没有被内联,而你认为它应该被内联以提高性能,就可以通过调整方法的大小、复杂度或者调用方式等,来使它更有可能被JVM内联。
    • 示例
      • 假设你有一个简单的Java类,里面有一个main方法和一个add方法,main方法中多次调用add方法。当运行这个程序并添加了-XX:+PrintInlining选项后,可能会看到类似“@12 java.util.ArrayList::add inline (hot) made not entrant”的输出。其中“@12”可能是一个内部的编译编号,“java.util.ArrayList::add”是被内联的方法名称及签名,“inline (hot)”表示这个方法因为是热点(频繁调用)而被内联,“made not entrant”可能是关于这个方法在被内联后的状态说明,比如它可能不再以独立的方式被调用。通过这些信息,开发人员可以了解JVM对这个方法的优化过程,并且可以根据这个情况来优化代码,比如如果想让更多类似的方法被内联,可以考虑减少方法的复杂度或者大小。

举例子

  1. 场景:优化一个高性能计算应用

    • 应用背景

      • 假设你正在开发一个高性能计算应用,用于对大规模的金融数据进行复杂的数学模型计算。这个应用包含了许多复杂的数学运算方法,例如矩阵乘法、向量运算等,并且需要处理大量的数据集合。
    • 步骤一:开启诊断功能

      • 你在JVM启动参数中添加-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining,这样可以深入了解JVM是如何对应用中的代码进行编译和内联优化的。例如,你的启动命令可能是java -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining -jar financial_modeling_app.jar
    • 步骤二:观察编译情况(使用 -XX:+PrintCompilation)

      • 当应用运行时,通过-XX:+PrintCompilation输出的信息,你可以看到哪些数学运算方法被JIT编译器频繁编译。例如,你可能会看到类似450 3 com.example.FinancialModel::matrixMultiplication (1200 bytes)这样的输出,这表明matrixMultiplication方法被编译了,其中“450”可能是编译任务编号,“3”可能与编译级别有关,“(1200 bytes)”表示方法字节码大小。
      • 如果这个方法频繁出现编译信息,就说明它是一个热点方法。这提示你这个方法的性能对整个应用的性能可能有很大影响。进一步检查这个方法,你可能发现它包含了多层嵌套循环用于矩阵元素的乘法和累加操作,这可能是导致它频繁编译和性能消耗的原因。
    • 步骤三:查看内联信息(使用 -XX:+PrintInlining)

      • 同时,-XX:+PrintInlining会输出方法内联的相关信息。例如,你可能会看到@70 com.example.FinancialModel::vectorAddition inline (hot) made not entrant,这表示vectorAddition方法在某个编译阶段因为是热点方法而被内联。通过观察这些内联信息,你可以了解JVM是如何优化方法调用的。
      • 假设你发现一个频繁调用的scalarMultiply方法没有被内联,但是你认为它的代码结构简单且适合内联来提高性能。这个方法可能是在计算向量与标量的乘法时使用。通过查看-XX:+PrintInlining的输出,你可以确定它没有被内联的情况,然后通过调整方法的大小(例如,减少不必要的局部变量声明)或者复杂度(例如,简化方法中的条件判断),使它更符合JVM内联的条件。
    • 步骤四:优化应用

      • 根据-XX:+PrintCompilation发现的热点方法和性能瓶颈,你可以对代码进行优化。对于matrixMultiplication方法,你可以考虑采用更高效的矩阵乘法算法,如Strassen算法来减少计算量。
      • 对于内联优化,通过调整scalarMultiply方法的代码结构后,再次运行应用并观察-XX:+PrintInlining的输出,看它是否能够被内联,从而提高方法调用的效率。通过这种方式,利用这三个JVM参数来深入分析和优化高性能计算应用的性能。
  2. 场景:优化一个Java Web应用服务器的性能

    • 应用背景

      • 考虑一个基于Java的Web应用服务器,它运行着多个企业级Web应用,处理大量的HTTP请求,包括用户认证、数据查询、业务逻辑处理等各种复杂功能。
    • 步骤一:启用诊断选项

      • 在服务器的JVM启动配置中添加-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining。例如,在Tomcat服务器中,你可以在catalina.sh(对于Linux系统)或catalina.bat(对于Windows系统)文件中找到JVM启动参数设置的地方,添加这三个参数。
    • 步骤二:分析编译活动(使用 -XX:+PrintCompilation)

      • 当服务器开始处理大量请求后,通过-XX:+PrintCompilation输出可以看到哪些业务逻辑方法被频繁编译。比如,在一个电子商务Web应用的订单处理模块中,你可能会看到380 3 com.example.ecommerce.OrderProcessor::calculateTotalPrice (800 bytes)频繁出现。这表明calculateTotalPrice方法是一个热点方法,它在计算订单总价的过程中可能涉及复杂的价格计算规则、折扣应用、税费计算等操作,导致频繁编译。
      • 进一步分析这个方法,你可能发现它从数据库中多次查询商品价格和折扣信息,这可能是性能瓶颈。你可以考虑采用缓存机制来减少数据库查询次数,从而优化这个方法。
    • 步骤三:利用内联信息(使用 -XX:+PrintInlining)

      • 通过-XX:+PrintInlining输出,你可以看到JVM对方法的内联情况。例如,在用户认证模块中,你可能看到@45 com.example.auth.UserAuthenticator::checkPassword inline (hot) made not entrant,这表示checkPassword方法在认证过程中被内联。这有助于你理解JVM如何优化这些频繁调用的方法。
      • 假设在另一个用户权限检查方法checkPermissions没有被内联,但它的代码结构简单且频繁调用。你可以根据-XX:+PrintInlining的信息调整这个方法,使其更有可能被内联,例如减少方法中的参数传递或者简化方法内部的逻辑判断,从而提高性能。
    • 步骤四:持续优化和监控

      • 根据这些参数提供的信息,对Web应用服务器中的关键方法进行持续优化。并且在后续的运行过程中,继续观察-XX:+PrintCompilation-XX:+PrintInlining的输出,以确保优化措施有效,并及时发现新的性能瓶颈。