Java 实战虚拟机 进阶 (一 万字)-实战 Java 虚拟机-高级篇

时间:2024-10-18 19:36:40

什么是 GraalVM

GraalVM 是 Oracle 官方推出的一款 **高性能JDK,**使用它享受比 OpenJDK 或者 OracleJDK 更好的性能。

  • GraalVM 的官方网址:https://www.graalvm.org/
  • 官方标语:Build faster, smaller, leaner applications.
  • 更低的 CPU、内存使用率

image-20241012154553514

  • 官方标语:Build faster, smaller, leaner applications.
  • 更低的CPU、内存使用率
  • 更快的启动速度,无需预热即可获得最好的性能
  • 更好的安全性、更小的可执行文件
  • 支持多种框架Spring Boot、Micronaut、Helidon 和 Quarkus。多家云平台支持。
  • 通过 Truffle框架运行 JS、Python、Ruby等其他语言。

image-20241012154725737

GraalVM 的版本

GraalVM 分为社区版(Community Edition)和企业版(Enterprise Edition)。企业版相比较社区版,在性能上有更多的优化。

image-20241012154932073

GraalVM 社区版环境搭建

image-20241012155004888

GraalVM 的两种运行模式

JIT (Just-In-Time) 模式,即时编译模式

JIT 模式的处理方式与 Oracle JDK类似,满足两个特点:

  • Write Once,Run Anywhere ->一次编写,到处运行。
  • 预热之后,通过 内置的Graal即时编译器 优化热点代码,生成比 Hotspot JIT 更高性能的机器码。

image-20241012155747330

image-20241012155938711

AOT (Ahead-Of-Time)模式,提前编译模式

AOT 编译器通过源代码,为特定平台创建可执行文件。比如,在Windows下编译完成之后,会生成 exe文件。通过这种方式,达到启动之后获得最高性能的目的。但是不具备跨平台特性,不同平台使用需要单独编译。这种模式生成的文件称之为Native Image本地镜像。

image-20241012160426792

image-20241012160506620

官网:https://www.graalvm.org/latest/reference-manual/native-image/#prerequisites

GraalVM 模式和版本的性能对比

社区版的GraalVM 使用本地镜像模式性能不如 HotspotJVM 的 JIT 模式,但是企业版的性能相对会高很多

image-20241012162306816

GraalVM 存在的问题

GraaIVM 的 AOT模式虽 然在启动速度、内存和CPU开销上非常有优势,但是使用这种技术会带来几个问题:

1、跨平台问题,在不同平台下运行需要编译多次。编译平台的依赖库等环境要与运行平台保持一致。
2、使用框架之后,编译本地镜像的时间比较长,同时也需要消耗 大量的CPU 和 内存。
3、AOT编译器 在编译时,需要知道运行时所有可访问的所有类。但是 Java 中有一些技术可以在运行时创建类,例如反射、动态代理等。这些技术在很多框架比如Spring中大量使用,所以框架需要 对AOT编译器进行适配解决类似的问题。

image-20241012162500806

实战案例1:使用 SpringBoot3 搭建 GraalVM 环境

image-20241012162707457

应用场景

image-20241012163559016

GraalVM 企业级应用 - Serverless 架构

传统的系统架构中,服务器等基础设施的运维、安全、高可用等工作都需要企业自行完成,存在两个主要问题:

1、开销大,包括了人力的开销、机房建设的开销。
2、**资源浪费,面对一些突发的流量冲击,比如秒杀等活动,必须提前规划好容量准备好大量的服务器,**这些服务器在其他时候会处于闲置的状态,造成大量的浪费。

image-20241012163832045

随着虚拟化技术、云原生技术的愈发成熟,云服务商提供了一套称为 Serverless无服务器化的 架构。企业无需进行服务器的任何配置和部署,完全由云服务商提供。比较 典型的有亚马逊 AWS、阿里云等 。

image-20241012163946250

Serverless架构 - 函数计算

Serverless架构 中第一种常见的服务是函数计算(Function as a Service),将一个应用拆分成多个函数,每个函数会以事件驱动的方式触发。典型代表有 AWS 的 Lambda、阿里云的 FC。

image-20241012164209647

函数计算主要应用场景有如下几种:

① 小程序、API服务 中的接口,此类接口的调用频率不高,使用常规的服务器架构容易产生资源浪费,使用 Serverless 就可以实现按需付费降低成本,同时支持自动伸缩能应对流量的突发情况。
② 大规模任务的处理,比如 音视频文件转码、审核等,可以利用事件机制当文件上传之后,自动触发对应的任务。
函数计算的计费标准中 包含CPU和内存使用量,所以使用 GraaIVM AOT模式 编译出来的本地镜像可以节省更多的成本。

函数计算的计费标准中 包含CPU和内存使用量,所以使用 GraaIVM AOT模式编译 出来的本地镜像可以节省更多的成本。

image-20241012164714003

实战案例 2:将程序部署到阿里云函数计算

image-20241012164827347

Serverlesss 架构 -Serverless 应用

函数计算的服务资源比较受限,比如 AWS 的Lambda 服务一般 无法支持超过15分钟的函数执行,所以云服务商提供了另外一套方案:基于容器的Serverless 应用,无需手动配置 K8s中 的 Pod、Service 等内容,只需选择镜像就可自动生成应用服务。

同样,Serverless应用 的计费标准中 包含CPU和内存使用量,所以 使用GraaIVM AOT模式 编译出来的本地镜像 可以节省更多的成本。

image-20241012165900643

将程序部署到 阿里云 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:内存快照文件的获取

image-20241012171142190

实战案例5:运行时数据的获取

image-20241012172132450

总结

image-20241012172507863

新一代的GC

垃圾回收器的技术演进

image-20241012173037234

不同的垃圾回收器设计的目标是不同的,如下图所示:

image-20241012173146626

Shenandoah GC

Shenandoah 是由 Red Hat 开发的一款低延迟的垃圾收集器,Shenandoah 并发执行大部分 GC 工作,包括并发的整理,堆大小对 STW的时间 基本没有影响。

image-20241012173527082

Shenandoah 的使用方法

1、下载。Shenandoah 只包含在 Open JDK中,默认不包含在内需要单独构建,可以直接下载构建好的。

下载地址: https://builds.shipilev.net/openjdk-jdk-shenandoah/

选择方式如下:

image-20241012173735360

image-20241012173902676

ZGC

ZGC 是一种可扩展的低延迟垃圾回收器。ZGC 在垃圾回收过程中,STW 的时间不会超过一毫秒,适合需要低延迟的应用。支持 几百兆 到 16TB 的堆大小,堆大小对 STW 的时间基本没有影响。ZGC 降低了停顿时间,能降低接口的最大耗时,提升用户体验。但是吞吐量不佳,所以 如果Java服务 比较关注 QPS(每秒的查询次数)那么 G1是 比较不错的选择。

image-20241012174934173

ZGC 的版本更选

image-20241012175136688

OracleJDK 和 OpenJDK 中都支持 ZGC,阿里的 DragonWell 龙井JDK 也支持 ZGC 但属于其自行对 Open JDK 11 的 ZGC 进行优化的版本。
建议使用 JDK17 之后的版本,延迟较低同时无需手动配置并行线程数。

image-20241012175243422

ZGC 在设计上做到了自适应,根据运行情况自动调整参数,让用户手动配置的参数最少化。

  • 自动设置年轻代大小,无需设置 -Xmn参数。
  • 自动晋升阈值(复制中存活多少次才搬运到老年代),无需设置 -XX:TenuringThreshold。
  • JDK17 之后支持自动的并行线程数,无需设置 -XX:ConcGCThreads。
ZGC 的参数设置

image-20241012175603648

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 启动程序进行测试

实战案例

内存不足时的垃圾回收测试

image-20241012193452255

总结

image-20241012194656868

揭秘 Java 工具

在 Java 的世界中,除了 Java 编写的业务系统之外,还有一类程序也需要 Java程序员 参与编写,这类程序就是 Java工具。
常见的 Java工具 有以下几类:
1、诊断类工具,如 Arthas、VisualVM 等。
2、开发类工具,如 ldea、Eclipse。
3、APM 应用性能监测工具,如 Skywalking、Zipkin 等。
4、热部署工具,如 Jrebel 等。

image-20241012194931547

学习 Java 工具常用技术 Java Agent

Java Agent技术是 JDk 提供的用来编写 Java工具 的技术,使用这种技术生成一种特殊的 jar包,这种 jar包 可以让 Java程序 运行其中的代码。

image-20241012195349431

Java Agent 技术的两种模式-静态加载模式

静态加载模式可以在程序启动的一开始就执行我们需要执行的代码,适合用 APM等性能监测系统 从一开始就监控程序的执行性能。静态加载模式需要在Java Agent的项目中编写一个 premain 的方法,并打包成jar包。

image-20241012195559233

搭建 java agent静态加载模式的环境

111

maven 环境配置Maven重点学习笔记(包入门 2万字)

image-20241012200142572

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包。

image-20241012195756051

搭建 java agent 动态加载模式的环境

image-20241012201851700

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编写 的系统中具备以下特点:代码无侵入性、操作简单、性能高。

image-20241012202538085

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 对象的写入和获取,实现:

  • 运行时配置的获取和更改
  • 应用程序运行信息的获取(线程栈、内存、类信息等)

image-20241012203813856

image-20241012203854237

image-20241012205231141

获取类和类加载器的信息 - Instumentation 对象

Oracle官方手册: https://docs.oracle.com/javase/17/docs/api/java/lang/instrument/Instrumentation.html

image-20241013233358454

打印类的源码

image-20241013235026834

image-20241013235153242

这里我们会使用 jd-core 依赖库 来完成,github地址:https://github.com/java-decompiler/id-core

image-20241014002051571

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是不是也可以实现类似的功能呢?

image-20241014090150083

打印方法执行的参数和耗时

打印方法执行的参数和耗时需要对原始类的方法进行增强,可以使用类似于 Spring AOP这类 面向切面编程 的方式,但是考虑到并非每个项目都使用了Spring 这些框架,所以我们选择的是最基础的字节码增强框架。字节码增强框架是在当前类的字节码信息中插入一部分字节码指令,从而起到增强的作用。

image-20241014090352278

打印方法执行的参数和耗时 - ASM

ASM是一个通用的 Java字节码 操作和分析框架。它可用于直接以二进制形式修改现有类或动态生成类。ASM重点关注性能。让操作尽可能小且尽可能快,所以它非常适合在动态系统中使用。ASM的缺点是代码复杂。

官网:ASM的官方网址:https://asm.ow2.io/

image-20241014090609928

操作步骤

image-20241014091009882

image-20241014091056899

示例
// 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。

image-20241014093907551

Byte Buddy官网:https://bytehuddy.net/

image-20241014094038195

// 使用 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);

image-20241014094155076

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/

image-20241014100657506

image-20241014101258488

image-20241014102202700

ByteBuddy 参数的传递

在 Java Agent 中如果需要传递参数到 ByteBuddy,可以采用如下的方式:

1、绑定 KeyValue,Key 是一个自定义注解,Value 是参数的值。

image-20241014102351597

image-20241014102420446

image-20241014103621224

image-20241014103730183