jvm 高级

GraalVM

GraalVM简介

GraalVM 是 Oracle 官方推出的一款高性能 JDK,它拥有比 OpenJDK 或者 OracleJDK 更好的性能。

  • 应用程序占用更低的CPU和内存资源
  • 应用程序启动速度快点,无需预热即可获得最好的性能
  • 使得应用程序安全性更好,由于更小的可执行文件
  • 支持多种框架,包括Spring Boot,Quarkus等

GraalVM 分为 社区版(Community Editon) 和 企业版(Enterprise Edition)。企业版相比于社区版,性能得到了更多的优化:

image-20240115204817117

安装

  • linux

    • 使用 arch 查看架构

    • 下载 GraalVM linux x86 社区版

    • 安装 GraalVM 并配置环境变量

      1
      2
      3
      tar -xvf graalvm-jdk-21_linux-x64_bin.tar.gz
      vim /etc/profile # 修改配置 JAVA_HOME=/usr/lib/jvm/graalvm-jdk-21.0.1+12.1
      source /etc/profile # 配置生效
  • docker

    1
    FROM ghcr.io/graalvm/native-image-community:21-muslib

GraalVM运行模式

JIT模式

GraalVM JIT(just-in-time)模式即即时编译模式。JIT模式的处理方式和Oracle JDK类似,但是性能更好

  • GraalVM 即时编译器在运行时将字节码转换为本地机器码并将热点代码进行优化并保存在内存中,下次直接从内存读取直接运行,提高性能。
  • GraalVM 即时编译器 采用了更加先进的算法,比传统的JIT编译器有更好的性能

注意:使用JIT模式并不能大幅降低应用程序cpu和内存的开销

适用场景: 适用于需要动态优化、长时间运行的服务,以及对启动时间要求不是非常严格的应用

AOT模式

GraalVM AOT(Ahead-of-time)模式即提前编译模式。

GraalVM AOT 将编译的字节码文件通过AOT编译器为特定的平台生成可执行文件(Native Image本地镜像),这种方式虽然牺牲了跨平台的特性,但提高了应用程序的启动性能,减少了运行时cpu和内存的开销(间减少了编译的开销和即时编译的内存开销)。

适用场景:适用于对启动时间要求较高、短时间运行的应用

使用GraalVM AOT模式制作本地镜像

  • 安装制作本地镜像的依赖库

    1
    sudo yum install gcc glibc-devel zlib-devel
  • 使用 native-image 类权限定名 制作本地镜像(注意:执行前需要先编译出字节码文件 javac Simple.Class)

    1
    2
    javac Simple.Class
    native-image Simple
  • 直接运行生成的可执行文件

    1
    ./Simple

两种模式的性能对比

官网数据:GraalVM社区版AOT模式 < GraalVM企业版AOT模式 < Hotspot JVM JIT模式 < GraalVM企业版AOT模式(PGO + G1) < GraalVM企业版JIT模式

image-20240115223719558

AOT 性能较低的原因是可执行文件是静态的,它由于无法适应动态场景,在动态场景性能较差

因此,AOT模式适用于对启动时间敏感以及对运行时cpu和内存的开销敏感的场景下。

GraalVM应用场景

问题

GraalVM 的AOT模式在启动速度、cpu和内存开销上有一定优势,但是也带来很多的问题:

  • 跨平台问题:在多个不同的平台上运行时,需要编译多次,并且需要保证运行平台的依赖库和编译平台的保持一致。
  • 引用开发框架之后,AOT编译本地镜像的时间比较长,编译过程中需要消耗大量cpu和内存
  • AOT无法适应动态场景,在动态场景性能较差,例如大量反射和动态代理的场景,而这些场景大量的出现在开发框架中。因此框架需要对AOT编译器做相应的适配工作。

解决方案

  • 在容器化平台上进行在线编译,这保证了编译环境和运行环境时一致的,同时在线编译解决了编译资源占用的问题。
  • 使用springboot3整合GraalVM AOT框架版本(SpringBoot3对于GraalVM进行了适配)

步骤

  • 引入mvn插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <build>
    <plugins>
    <plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <configuration>
    <skipNativeTests>true</skipNativeTests>
    </configuration>
    </plugin>
    </plugins>
    </build>
  • 使用 mvn -Pnative clean native:compile 命令生成本地镜像

应用场景

  • 使用GraalVM的企业版,可以有效提升性能
  • 在公有云的部分服务器按照CPU和内存使用量进行计费,使用GraalVM可以有效地降低费用
云服务 Serverless 架构

云服务商提供了一种 Serverless 无服务化的架构,企业无需进行任何服务器的配置和部署,完全由云服务商提供。Serverless 架构支持 自动扩容、高可用等特性,因此它是按照CPU、流量和内存使用量进行计费。

参数优化和故障诊断

参数优化

GraalVM是一款独立的JDK,大部分HotSpot中的虚拟机参数都不适用。常用的参数配置:官方手册

  • 社区版默认采用的垃圾回收器为 Serial GC - 串行垃圾回收器,使用这款垃圾回收器默认的最大java堆大小为物理内存大小的80%,可以通过 -Xms 最大堆内存(如果在编译期编译成本地镜像时直接确认堆内存的大小,可以在编译时添加参数 —R:MaxHeapSize=)。此外,可以开启 G1 垃圾回收器进行垃圾回收,开启方式:-gc=G1(注意:只有社区版才支持G1垃圾回收器)
  • GraalVM提供了一个特殊的垃圾回收器 - Epsilon GC,开启方式为:-gc:epsilon,它是一种无操作的垃圾回收器,针对一些函数计算(这些函数计算的生命周期比较短暂,可以很快的释放资源)的场景。使用该款垃圾回收器,可以避免初始时,创建一些垃圾回收线程,同时避免垃圾回收带来的性能开销。
  • -XX:PrintGC --XX:+VerboseGC:打印垃圾回收的详细信息。

故障诊断

内存泄漏

由于使用GraalVM时,我们运行的是本地镜像而不是java程序,因此不能使用之前arthas或者jmap命令,生成内存快照文件进行分析。

生成内存快照的方式:

  • 添加参数 --enable-monitoring=heapdump 到 pom 文件的插件中,重新编译

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <build>
    <plugins>
    <plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <configuration>
    <skipNativeTests>true</skipNativeTests>
    <buildArgs>
    <arg>--enable-monitoring=heapdump</arg>
    </buildArgs>
    </configuration>
    </plugin>
    </plugins>
    </build>
  • 使用 kill -SIGUSR1 <PID> 命令,创建内存快照文件

  • 使用 MAT 分析内存快照文件

运行时数据获取

JFR - JDK Flight Recorder 是一个jvm内置的工具,可以实时收集正在运行的 java 应用程序的诊断和分析数据(例如:线程和异常等信息), 可以将这些数据导出后,使用 VisualVM 进行分析。

导出运行时数据获取的方式:

  • 添加参数 --enable-monitoring=jfr 到 pom 文件的插件中,重新编译

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <build>
    <plugins>
    <plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <configuration>
    <skipNativeTests>true</skipNativeTests>
    <buildArgs>
    <arg>--enable-monitoring=heapdump,jfr</arg>
    </buildArgs>
    </configuration>
    </plugin>
    </plugins>
    </build>
  • 在运行程序时,添加参数 -XX:StartFlightRecord="filename=recording.jft,duration=10s" 参数,开启 JFR 功能

    • filename-文件名 duration-时间长度
  • 使用 VisualVM 分析 JFR 的记录文件

总结

  • GraalVM 是 Oracle 官方推出的一款高性能 JDK
    • 两种模式
      • JIT:即时编译器
      • AOT:制作本地镜像 牺牲跨平台性 启动速度快、CPU和内存占用率低
    • 使用场景
      • 更好的性能 企业版
      • 节省云服务的内存和cpu资源使用 AOT

垃圾回收器的技术演进

image-20240117100551411

Parallel Scavenge 和 Parallel Old 是 JDK8 默认的垃圾回收组合,年轻代使用 标记复制,老年代使用 标记整理,在年轻代和老年代进行回收时,会有长时间的停顿,优点则是吞吐量比较高,标记整理不会产生内存碎片。

ParNew 和 CMS 年轻代使用 标记复制,老年代使用 标记清理,比较关注系统的停顿时间,但是容易产生内存碎片问题,当产生内存问题无法存放对象时会退化为 SerialOld 垃圾回收器或者发生Full GC整理,发生长时间的STW。

G1 是 JDK11 默认的垃圾回收器,引入 region 有利于控制停顿时间,年轻代使用复制,老年代使用 标记复制(整理)。

Shenandoah 和 ZGC 不区分年轻代和老年代,整堆的回收方式是一样的,使用 并行标记 和 并行复制-整理算法。

垃圾回收器的设计目标

垃圾回收的设计目标:吞吐量、停顿时间、内存占用,三者不可兼得

image-20240117104137220

Shenandoah GC 和 ZGC 追求极致低的停顿时间,但是吞吐量较低和内存占用高

Shenandoah GC

Shenandoah GC 是 Red Hat 开发的一款低延迟的垃圾收集器,因此并没有引入到 OracleJDK 中,只能在 OpenJDK 中使用,Shenandoah GC垃圾回收线程和用户线程可以并行的工作,有效的减少了停顿时间。

Shenandoah GC的使用

Shenandoah GC只包含在OpenJDK中,默认OpenJDK不包含,需要下载构建:https://builds.shipilev.net/openjdk-jdk-shenandoah/

下载完成后,配置到环境变量中,添加启动参数,运行java程序

1
2
-XX:+UseShenandoahGC # 开启Shenandoah GC
-Xlog:gc # 打印GC日志

ZGC

ZGC 是由 Oracle 开发的一种可扩展的低延迟垃圾回收器。ZGC 在垃圾回收过程中,STW的时间不会超过一毫秒,适合需要低延迟的应用。支持几百兆到16TB的堆大小,堆大小对STW的时间基本没有影响。 ZGC降低了停顿时间,降低接口的最大耗时,提升用户体验。但是吞吐量不佳。

ZGC的使用

建议使用JDK17之后的版,这个版本延迟较低,达到亚毫秒级的最大暂停时间,支持自动计算出垃圾回收的并行线程数,无需手动配置

开启ZGC
  • 非分代 ZGC 启动参数:-XX+UseZGC -XX:+ZGenerational
  • 分代 ZGC 启动参数:-XX:+UseZGC
无需手动配置的参数

ZGC 在设计上做了自适应,会根据运行情况自动调整参数,尽量避免用户手动配置

  • 自动设置年轻代的大小,无需设置-Xmn参数
  • 自动年轻代晋升阈值,无需设置-XX:TenuringThreshold
  • JDK17之后开始支持自动设置垃圾回收的并行线程数,无需设置-XX:ConcGCThreads
需要手动配置的参数

最大堆内存

  • -Xmx

    必须设置ZGC的最大堆内存,ZGC的垃圾回收会占用一定的内存空间用来处理垃圾回收,因此要尽量保证堆中有足够的空间用于垃圾回收。

  • -XX:SoftMaxHeapSize=

    ZGC尽量保证堆内存小于这个值(可以会超过这个值),当堆内存达到这个值的时候会进行垃圾回收。

ZGC调优

ZGC的大部分参数会自动调整,不需要用户手动配置,所以在ZGC中,可以调优的方向就是开启ZGC的Huge Page大页技术。

大页技术就是一种通过使用操作系统提供的Huge Page大页(大于标准页面大小的内存块)来减少内存访问开销的方法。具体而言,就是通过减少内存映射的页表数量,减少缓存命中的次数,提高内存访问效率,从而提高性能,提升吞吐量,降低延迟。

注意:ZGC默认没有开启 Huge Page 技术,因为开启大页技术,需要开启 root 权限

开启步骤
  • 计算页数

    linux x86架构中的大页大小为 2MB,根据jvm所需堆内存的大小估算出大页的数量。(比如堆空间需要16G,预留2G用于ZGC的垃圾回收。页数=18G/2M = 9216个页数

  • 配置系统的大页页数

    1
    echo 9216 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
  • 添加java启动参数启用大页支持

    1
    -XX:UseLargePages 

总结

ZGC和Shenandoah GC设计的目的都是追求较短的停顿时间,在两个垃圾回收器在并行回收时,都会使用垃圾回收线程占用CPU资源

  • 内存充足的情况下,ZGC的垃圾回收表现效果更好
  • 内存不足的情况下,Shenandoah GC表现更好

Java agent

Java Agent 简介

Java Agent 是 jdk1.5 之后 jvm 提供的一种扩展机制,允许在应用程序在运行时动态地添加代理字节码代码(agents)来修改、监视或增强应用程序的行为。它可以帮助开发者在不修改应用程序源代码的情况下,添加一些额外的功能,例如性能监控、内存分析、日志跟踪等。

Java Agent 两种模式

静态模式

静态模式是应用程序启动时添加虚拟机的启动参数 -javaagent 指定 Java Agent 的 JAR 文件路径。

静态模式在应用程序启动前就进行加载。

使用场景:适用于在应用程序启动时对字节码进行静态修改,例如实现一些预处理或性能监控,但不需要在运行时变更的场景。

静态模式的 Java Agent 工程需要去编写 premain 方法作为静态加载入口点,该方法会在应用程序的主线程的main方法执行前执行

1
2
// agentArgs
public static void premain(String agentArgs, Instrumentation inst);

Java 工程启动时,需要添加参数加载 Java Agent

1
java -javaagent:/xx/xx.jar -jar xxxx.jar
原理

premain 方法是 java agent 的一个约定方法,java 虚拟机会在应用程序启动前在主线程中的main方法执行前调用这个方法

image-20240117214208129

动态模式

动态模式是在应用程序运行时加载 Java Agent,而不需要在启动的时指定。

通常用于监控和管理

动态模式的 Java Agent 工程通过去 agentmain 方法提供功能

1
public static void agentmain(String agentAgrs, Instrumentation inst);

动态模式下,Java Agent 需要指定 Java 进程ID

1
2
VirtualMachine vm = VirtualMachine.attach("pid");
vm.loadAgent("xxx.jar")
原理

agentmain 方法在 独立线程 attach thread 中执行

image-20240117215025483

Java Agent 工程搭建

静态模式搭建

  • 添加maven插件maven-assembly-plugin,用于打包java agent的jar包

    1
    2
    3
    4
    5
    6
    7
    <build>
    <plugins>
    <plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
  • 编写premain方法,该方法会在应用程序的主线程的main方法执行前执行

    1
    2
    3
    public static void premain(String args, Instrumentation inst) {
    ...
    }
  • 编写 MANIFEST.MF 文件s