什么是 GraalVM
GraalVM 是 Oracle 官方推出的一款 **高性能JDK,**使用它享受比 OpenJDK 或者 OracleJDK 更好的性能。
- GraalVM 的官方网址:https://www.graalvm.org/
- 官方标语:Build faster, smaller, leaner applications.
- 更低的 CPU、内存使用率
- 官方标语:Build faster, smaller, leaner applications.
- 更低的CPU、内存使用率
- 更快的启动速度,无需预热即可获得最好的性能
- 更好的安全性、更小的可执行文件
- 支持多种框架Spring Boot、Micronaut、Helidon 和 Quarkus。多家云平台支持。
- 通过 Truffle框架运行 JS、Python、Ruby等其他语言。
GraalVM 的版本
GraalVM 分为社区版(Community Edition)和企业版(Enterprise Edition)。企业版相比较社区版,在性能上有更多的优化。
GraalVM 社区版环境搭建
GraalVM 的两种运行模式
JIT (Just-In-Time) 模式,即时编译模式
JIT 模式的处理方式与 Oracle JDK类似,满足两个特点:
- Write Once,Run Anywhere ->一次编写,到处运行。
- 预热之后,通过 内置的Graal即时编译器 优化热点代码,生成比 Hotspot JIT 更高性能的机器码。
AOT (Ahead-Of-Time)模式,提前编译模式
AOT 编译器通过源代码,为特定平台创建可执行文件。比如,在Windows下编译完成之后,会生成 exe文件。通过这种方式,达到启动之后获得最高性能的目的。但是不具备跨平台特性,不同平台使用需要单独编译。这种模式生成的文件称之为Native Image本地镜像。
官网:https://www.graalvm.org/latest/reference-manual/native-image/#prerequisites
GraalVM 模式和版本的性能对比
社区版的GraalVM 使用本地镜像模式性能不如 HotspotJVM 的 JIT 模式,但是企业版的性能相对会高很多
GraalVM 存在的问题
GraaIVM 的 AOT模式虽 然在启动速度、内存和CPU开销上非常有优势,但是使用这种技术会带来几个问题:
1、跨平台问题,在不同平台下运行需要编译多次。编译平台的依赖库等环境要与运行平台保持一致。
2、使用框架之后,编译本地镜像的时间比较长,同时也需要消耗 大量的CPU 和 内存。
3、AOT编译器 在编译时,需要知道运行时所有可访问的所有类。但是 Java 中有一些技术可以在运行时创建类,例如反射、动态代理等。这些技术在很多框架比如Spring中大量使用,所以框架需要 对AOT编译器进行适配解决类似的问题。
实战案例1:使用 SpringBoot3 搭建 GraalVM 环境
应用场景
GraalVM 企业级应用 - Serverless 架构
传统的系统架构中,服务器等基础设施的运维、安全、高可用等工作都需要企业自行完成,存在两个主要问题:
1、开销大,包括了人力的开销、机房建设的开销。
2、**资源浪费,面对一些突发的流量冲击,比如秒杀等活动,必须提前规划好容量准备好大量的服务器,**这些服务器在其他时候会处于闲置的状态,造成大量的浪费。
随着虚拟化技术、云原生技术的愈发成熟,云服务商提供了一套称为 Serverless无服务器化的 架构。企业无需进行服务器的任何配置和部署,完全由云服务商提供。比较 典型的有亚马逊 AWS、阿里云等 。
Serverless架构 - 函数计算
Serverless架构 中第一种常见的服务是函数计算(Function as a Service),将一个应用拆分成多个函数,每个函数会以事件驱动的方式触发。典型代表有 AWS 的 Lambda、阿里云的 FC。
函数计算主要应用场景有如下几种:
① 小程序、API服务 中的接口,此类接口的调用频率不高,使用常规的服务器架构容易产生资源浪费,使用 Serverless 就可以实现按需付费降低成本,同时支持自动伸缩能应对流量的突发情况。
② 大规模任务的处理,比如 音视频文件转码、审核等,可以利用事件机制当文件上传之后,自动触发对应的任务。
函数计算的计费标准中 包含CPU和内存使用量,所以使用 GraaIVM AOT模式 编译出来的本地镜像可以节省更多的成本。
函数计算的计费标准中 包含CPU和内存使用量,所以使用 GraaIVM AOT模式编译 出来的本地镜像可以节省更多的成本。
实战案例 2:将程序部署到阿里云函数计算
Serverlesss 架构 -Serverless 应用
函数计算的服务资源比较受限,比如 AWS 的Lambda 服务一般 无法支持超过15分钟的函数执行,所以云服务商提供了另外一套方案:基于容器的Serverless 应用,无需手动配置 K8s中 的 Pod、Service 等内容,只需选择镜像就可自动生成应用服务。
同样,Serverless应用 的计费标准中 包含CPU和内存使用量,所以 使用GraaIVM AOT模式 编译出来的本地镜像 可以节省更多的成本。
将程序部署到 阿里云 Serverless 应用
步骤:
1、在项目中编写 Dockerfile 文件。
2、使用服务器制作镜像,这一步会消耗大量的CPU和内存资源,同时 GraaIVM相关 的镜像服务器在国外,**建议使用阿里云的镜像服务器制作Docker镜像。**前两步同实战案例2
3、配置Serverless应用,选择容器镜像、CPU和内存。
4、绑定外网负载均衡并使用 Postman 进行测试。
参数优化和故障诊断
GraalVM 的内存参数
由于 GraalVM 是一款独立的 JDK,所以大部分 HotSpot 中的虚拟机参数都不适用。常用的参数参考:官方手册
- 社区版只能使用串行 垃圾回收器(Serial GC),使用串行垃圾回收器的默认 最大Java 堆大小 会设置为物理内存大小的 80%,调整方式为使用 -Xmx最大堆大小。如果希望在编译期就指定该大小,可以在编译时添加参数 -R:MaxHeapSize=最大堆大小。
- G1垃圾回收器只能在企业版中使用,开启方式为添加 --gC=G1参数,有效降低垃圾回收的延迟。
- 另外提供一个Epsilon GC,开启方式:–gc=epsilon,它不会产生任何的垃圾回收行为所以没有额外的内存、CPU开销。如果在公有云上运行的程序生命周期短暂不产生大量的对象,可以使用该垃圾回收器,以节省最大的资源。
- -XX:+PrintGC -XX:+VerboseGC 参数打印垃圾回收详细信息。
实战案例4:内存快照文件的获取
实战案例5:运行时数据的获取
总结
新一代的GC
垃圾回收器的技术演进
不同的垃圾回收器设计的目标是不同的,如下图所示:
Shenandoah GC
Shenandoah 是由 Red Hat 开发的一款低延迟的垃圾收集器,Shenandoah 并发执行大部分 GC 工作,包括并发的整理,堆大小对 STW的时间 基本没有影响。
Shenandoah 的使用方法
1、下载。Shenandoah 只包含在 Open JDK中,默认不包含在内需要单独构建,可以直接下载构建好的。
下载地址: https://builds.shipilev.net/openjdk-jdk-shenandoah/
选择方式如下:
ZGC
ZGC 是一种可扩展的低延迟垃圾回收器。ZGC 在垃圾回收过程中,STW 的时间不会超过一毫秒,适合需要低延迟的应用。支持 几百兆 到 16TB 的堆大小,堆大小对 STW 的时间基本没有影响。ZGC 降低了停顿时间,能降低接口的最大耗时,提升用户体验。但是吞吐量不佳,所以 如果Java服务 比较关注 QPS(每秒的查询次数)那么 G1是 比较不错的选择。
ZGC 的版本更选
OracleJDK 和 OpenJDK 中都支持 ZGC,阿里的 DragonWell 龙井JDK 也支持 ZGC 但属于其自行对 Open JDK 11 的 ZGC 进行优化的版本。
建议使用 JDK17 之后的版本,延迟较低同时无需手动配置并行线程数。
ZGC 在设计上做到了自适应,根据运行情况自动调整参数,让用户手动配置的参数最少化。
- 自动设置年轻代大小,无需设置 -Xmn参数。
- 自动晋升阈值(复制中存活多少次才搬运到老年代),无需设置 -XX:TenuringThreshold。
- JDK17 之后支持自动的并行线程数,无需设置 -XX:ConcGCThreads。
ZGC 的参数设置
ZGC 的调优
ZGC 中可以使用 Linux 的 Huge Page 大页技术优化性能,提升吞吐量、降低延迟。
注意:安装过程需要 root 权限,所以 ZGC 默认没有开启此功能。
操作步骤:
1、计算所需页数,Linuxx86 架构中大页大小为 2MB,根据所需堆内存的大小估算大页数量。比如堆空间需要 16G,预留 2G( JVM 需要额外的一些非堆空间),那么页数就是 18G / 2MB = 9216。
2、配置系统的大页池以具有所需的页数(需要root权限):
$ echo 9216 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
3、添加参数 -XX:+UseLargePages 启动程序进行测试
实战案例
内存不足时的垃圾回收测试
总结
揭秘 Java 工具
在 Java 的世界中,除了 Java 编写的业务系统之外,还有一类程序也需要 Java程序员 参与编写,这类程序就是 Java工具。
常见的 Java工具 有以下几类:
1、诊断类工具,如 Arthas、VisualVM 等。
2、开发类工具,如 ldea、Eclipse。
3、APM 应用性能监测工具,如 Skywalking、Zipkin 等。
4、热部署工具,如 Jrebel 等。
学习 Java 工具常用技术 Java Agent
Java Agent技术是 JDk 提供的用来编写 Java工具 的技术,使用这种技术生成一种特殊的 jar包,这种 jar包 可以让 Java程序 运行其中的代码。
Java Agent 技术的两种模式-静态加载模式
静态加载模式可以在程序启动的一开始就执行我们需要执行的代码,适合用 APM等性能监测系统 从一开始就监控程序的执行性能。静态加载模式需要在Java Agent的项目中编写一个 premain 的方法,并打包成jar包。
搭建 java agent静态加载模式的环境
maven 环境配置Maven重点学习笔记(包入门 2万字)
Java代码
import java.lang.instrument.Instrumentation;
public class AgentMain {
// premain 方法
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain执行了");
}
}
Java Agent技术的两种模式-动态加载模式
动态加载模式可以随时让 java agent代码执行,适用于 Arthas等诊断系统。动态加载模式需要在 Java Agent的项目中编写一个 agentmain 的方法,并打包成 jar包。
搭建 java agent 动态加载模式的环境
Java 代码
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class AttachMain {
public static void main(String[] args) throws IOException, AttachNotSupportedException {
// 获取进程虚拟对象
VirtualMachine vm = VirtualMachine.attach("37632");
// 执行Java agent里面的 agentmain 方法
vm.loadAgent("D:/jvm/javaagent/itheima-agent/target/itheima-agent-1.0-SNAPSHOT-jar-with-dependencide");
}
}
实战案例
实战案例1:简化版的 Arthas
需求:
编写一个简化版的 Arthas程序,具备以下几个功能:
1、查看内存使用情况
2、生成堆内存快照
3、打印栈信息
4、打印类加载器
5、打印类的源码
6、打印方法执行的参数和耗时
该程序是一个 独立的Jar包,可以应用于 任何Java编写 的系统中具备以下特点:代码无侵入性、操作简单、性能高。
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class AttachMain {
public static void main(String[] args) throws IOException, AttachNotSupportedException {
// 获取进程列表,让用户手动进行输入
// 1.执行 jps 命令,打印所有进程列表
Process jps = Runtime.getRuntime().exec("jps");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(jsp.getInputStream()));
try {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} finally {
if (bufferedReader != null) {
bufferedReader.close();
}
}
// 2. 输入进程 id
Scanner scanner = new Scanner(System.in);
String processId = scanner.next();
// 获取进程虚拟对象
VirtualMachine vm = VirtualMachine.attach("processId");
// 执行Java agent里面的 agentmain 方法
vm.loadAgent("D:/jvm/javaagent/itheima-agent/target/itheima-agent-1.0-SNAPSHOT-jar-with-dependencide");
}
}
获取运行时信息 - JMX 技术
JDK 从 1.5 开始提供了 Java Management Extensions(JMX) 技术,通过 Mbean 对象的写入和获取,实现:
- 运行时配置的获取和更改
- 应用程序运行信息的获取(线程栈、内存、类信息等)
获取类和类加载器的信息 - Instumentation 对象
Oracle官方手册: https://docs.oracle.com/javase/17/docs/api/java/lang/instrument/Instrumentation.html
打印类的源码
这里我们会使用 jd-core 依赖库 来完成,github地址:https://github.com/java-decompiler/id-core
import java.util.List;
public class MemoryCommand {
// 打印所有内存信息
public static void printMemory() {
List<MemoryPoolMXBean> memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans();
// 堆内存
memoryPoolMXBeans.stream().filter(x -> x.getType().equals(MemoryType.HEAP))
.forEach(x -> {
StringBuilder sb = new StringBuilder();
sb.append("name")
.append(x.getName())
.append(" used:")
.append(x.getUsage().getUsed() / 1024 / 1024)
.append("m")
.append(" committed:")
.append(x.getUsage().getUsed() / 1024 / 1024)
.append("m")
.append(" max:")
.append(x.getUsage().getUsed() / 1024 / 1024)
.append("m")
});
}
}
Spring AOP是不是也可以实现类似的功能呢?
打印方法执行的参数和耗时
打印方法执行的参数和耗时需要对原始类的方法进行增强,可以使用类似于 Spring AOP这类 面向切面编程 的方式,但是考虑到并非每个项目都使用了Spring 这些框架,所以我们选择的是最基础的字节码增强框架。字节码增强框架是在当前类的字节码信息中插入一部分字节码指令,从而起到增强的作用。
打印方法执行的参数和耗时 - ASM
ASM是一个通用的 Java字节码 操作和分析框架。它可用于直接以二进制形式修改现有类或动态生成类。ASM重点关注性能。让操作尽可能小且尽可能快,所以它非常适合在动态系统中使用。ASM的缺点是代码复杂。
官网:ASM的官方网址:https://asm.ow2.io/
操作步骤
示例
// ASM 入门案例,向每个方法添加一行字节码指令
public class ASMDemo {
public static void main(String[] args) throws IOException {
// 1.从本地读取一个字节码文件, byte[]
byte[] bytes = FileUtils.readFileToByteArray(new File("D\\jvm\\AttahMain.class"));
// 2.通过ASM修改字节码文件
// 将二进制数据转换成可以解析内容
ClassReader classReader = new ClassReader(bytes);
// 创建visitor对象,修改字节码信息
ClassWriter classWriter = new ClassWriter(0);
ClassVisitor classVisitor = new ClassVisitor(ASM7, new ClassWriter(0)) {
@Override
public MethodVisitor visitMethod(int access, String name, string descriptor, String signature, String[] exception) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exception);
// 返回自定义的MethodVisitor
MethodVisitor methodVisitor = new MethodVisitor(this.api.mv) {
// 修改字节码指令
@Override
public void visitCode() {
// 插入一行代码指令 ICOST_0
visitInsn(ICONST_)
}
}
return methodVisitor;
}
};
classReader.accept(classVisitor, 0);
// 将修改完的字节码信息写入文件中,进行替换
FileUtils.writeByteArrayToFile(new File("D\\jvm\\AttahMain.class"), classWriter.toByteArray());
}
}
打印方法执行的参数和耗时 - Byte Buddy
Byte Buddy 是一个代码生成和操作库,用于在 Java应用程序 运行时创建和修改 Java类,而无需编译器的帮助。Byte Buddy底层基于ASM,提供了非常方便的 APl。
Byte Buddy官网:https://bytehuddy.net/
// 使用 bytebyddy 增强类
new AgentBuilder.Default()
// 禁止byte buddy处理时修改类名
.disableClassFormatChanges() // AgentBuilder
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) // AgentBuilder.RedefinitionListenable.With
// 打印出错误日志
.with(new AgentBuilder.Listener.WithTransformationsOnly(AgentBuilder.Listener.StreamWriting
.toSystemOut())) // AgentBuilder
// 匹配哪些类
.type(ElementMatchers.named(className)) // AgentBuilder.Identified.Narrowable
// 增强,使用MyAdvice通知,对所有方法都进行增强
.transform(builder, typeDescription, classLoader, module, pretectionDomain) ->
builder.visit(Advice.to(MyAdvice.class).on(ELementMathcers.any())) // AgentBuilder.Identify
.installOn(inst);
class MyAdvice {
@Advice.OnMethodEnter
static long enter(@Advice.AllArguments Object[] ary) {
if (ary != null) {
for (int i = 0; i < ary.length; i++) {
System.out.println("Argument: " + i + " is " + ary[i]);
}
}
return System.nanoTime();
}
@Advice.OnMethodExit
static void exit(@Advice.Enter long value) {
System.out.println("耗时为:" + (System.nanoTime() - value) + "纳秒");
}
}
最后将整个 简化版的arthas进行打包,在服务器上进行测试。使用 maven-shade-plugin插件 可以将所有依赖打入同一个 jar包 中并指定 入口main方法。
实战案例 2:APM系统 的数据采集
Application performance monitor (APM) 系统
Application performance monitor(APM) 应用程序性能监控系统是采集运行程序的实时数据并使用可视化的方式展示,使用 APM 可以确保 系统可用性,优化服务性能 和 响应时间,持续改善用户体验。常用的 APM系统 有Apache Skywalking、Zipkin等。
Skywalking官方网站: https://skywalking.apache.org/
ByteBuddy 参数的传递
在 Java Agent 中如果需要传递参数到 ByteBuddy,可以采用如下的方式:
1、绑定 KeyValue,Key 是一个自定义注解,Value 是参数的值。