高级篇

1、GraalVM

1.1 什么是GraalVM

GraalVM是Oracle官方推出的一款高性能JDK,使用它享受比OpenJDK或者OracleJDK更好的性能。 GraalVM的官方网址:https://www.graalvm.org/

官方标语:Build faster, smaller, leaner applications。 更低的CPU、内存使用率

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

img

img

需求: 搭建Linux下的GraalVM社区版本环境。(windows环境下部分功能不支持)

这与安装JDK的流程一致

1.tar -xvf graalvm-jdk-21_linux-x64_bin.tar.gz

2.vim /etc/profile

3.加入了如下内容:

1
2
3
4
# Set Java environment
export JAVA_HOME=/home/geqian/graalvm-jdk-21.0.0
export CLASSPATH=$JAVA_HOME/lib/
export PATH=$JAVA_HOME/bin:$PATH

4.source /etc/profile使配置生效

5.java-version验证,安装成功

img

1.2 GraalVM的两种模式

  • JIT( Just-In-Time )模式 ,即时编译模式
  • AOT(Ahead-Of-Time)模式 ,提前编译模式

1.2.1JIT

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

Write Once,Run Anywhere -> 一次编写,到处运行。

预热之后,通过内置的Graal即时编译器优化热点代码,生成比Hotspot JIT更高性能的机器码。

img

需求:

分别在JDK8 、 JDK21 、 GraalVM 21 Graal即时编译器、GraalVM 21 不开启Graal即时编译器运行Jmh性能测试用例,对比其性能。

步骤:

1、在代码文件夹中找到GraalVM的案例代码,将java-simple-stream-benchmark文件夹下的代码使用maven打包成jar包。

img

img

2、将jar包上传到服务器,使用不同的JDK进行测试,对比结果。

注意:

-XX:-UseJVMCICompiler参数可以关闭GraalVM中的Graal编译器。

img

GraalVM开启Graal编译器下的性能还是不错的:

img

1.2.2AOT

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

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

这种模式生成的文件称之为Native Image本地镜像。

img

需求: 使用GraalVM AOT模式制作本地镜像并运行。 步骤: 1、安装Linux环境本地镜像制作需要的依赖库: https://www.graalvm.org/latest/reference-manual/native-image/#prerequisites 2、使用 native-image 类名 制作本地镜像。

img

img

3、运行本地镜像可执行文件。

img

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

img

1.3 应用场景

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

1、跨平台问题,在不同平台下运行需要编译多次。编译平台的依赖库等环境要与运行平台保持一致。

2、使用框架之后,编译本地镜像的时间比较长,同时也需要消耗大量的CPU和内存

3、AOT 编译器在编译时,需要知道运行时所有可访问的所有类。但是Java中有一些技术可以在运行时创建类,例如反射、动态代理等。这些技术在很多框架比如Spring中大量使用,所以框架需要对AOT编译器进行适配解决类似的问题。

解决方案:

1、使用公有云的Docker等容器化平台进行在线编译,确保编译环境和运行环境是一致的,同时解决了编译资源问题。

2、使用SpringBoot3等整合了GraalVM AOT模式的框架版本。

1.3.1SpringBoot搭建GraalVM应用

需求: SpringBoot3对GraalVM进行了完整的适配,所以编写GraalVM服务推荐使用SpringBoot3。

步骤: 1、使用 https://start.spring.io/ spring提供的在线生成器构建项目。

img

2、编写业务代码,修改原代码将PostConstructor注解去掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class UserServiceImpl implements UserService, InitializingBean {

private List<User> users = new ArrayList<>();

@Autowired
private UserDao userDao;

@Override
public List<UserDetails> getUserDetails() {
return userDao.findUsers();
}

@Override
public List<User> getUsers() {
return users;
}

@Override
public void afterPropertiesSet() throws Exception {
//初始化时生成数据
for (int i = 1; i <= 10; i++) {
users.add(new User((long) i, RandomStringUtils.randomAlphabetic(10)));
}
}
}

3、执行 mvn -Pnative clean native:compile 命令生成本地镜像。

img

img

4、运行本地镜像。

img

什么场景下需要使用GraalVM呢?

1、对性能要求比较高的场景,可以选择使用收费的企业版提升性能。

2、公有云的部分服务是按照CPU和内存使用量进行计费的,使用GraalVM可以有效地降低费用。

img

1.3.2将GraalVM应用部署到函数计算

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

1、开销大,包括了人力的开销、机房建设的开销。

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

img

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

img

serverless架构-函数计算

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

img

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

  • 小程序、API服务中的接口,此类接口的调用频率不高,使用常规的服务器架构容易产生资源浪费,使用Serverless就可以实现按需付费降低成本,同时支持自动伸缩能应对流量的突发情况。
  • 大规模任务的处理,比如音视频文件转码、审核等,可以利用事件机制当文件上传之后,自动触发对应的任务。

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

将程序部署到阿里云函数计算

img

步骤:

1、在项目中编写Dockerfile文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Using Oracle GraalVM for JDK 17
FROM container-registry.oracle.com/graalvm/native-image:17-ol8 AS builder

# Set the working directory to /home/app
WORKDIR /build

# Copy the source code into the image for building
COPY . /build
RUN chmod 777 ./mvnw

# Build
RUN ./mvnw --no-transfer-progress native:compile -Pnative

# The deployment Image
FROM container-registry.oracle.com/os/oraclelinux:8-slim

EXPOSE 8080

# Copy the native executable into the containers
COPY --from=builder /build/target/spring-boot-3-native-demo app
ENTRYPOINT ["/app"]

2、使用服务器制作镜像,这一步会消耗大量的CPU和内存资源,同时GraalVM相关的镜像服务器在国外,建议使用阿里云的镜像服务器制作Docker镜像。

img

3、使用函数计算将Docker镜像转换成函数服务。

img

img

配置触发器:

img

4、绑定域名并进行测试。

img

需要准备一个自己的域名:

img

配置接口路径:

img

会出现一个错误:

img

把域名导向阿里云的域名:

img

测试成功:

img

1.3.3将GraalVM应用部署到Serverless

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

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

服务分类 交付模式 弹性效率 计费模式
函数计算 函数 毫秒级 调用次数CPU内存使用量
Serverless应用 镜像容器 秒级 CPU内存使用量

步骤:

1、在项目中编写Dockerfile文件。

2、使用服务器制作镜像,这一步会消耗大量的CPU和内存资源,同时GraalVM相关的镜像服务器在国外,建议使用阿里云的镜像服务器制作Docker镜像。

前两步同实战案例2

3、配置Serverless应用,选择容器镜像、CPU和内存。

img

4、绑定外网负载均衡并使用Postman进行测试。

img

先别急着点确定,需要先创建弹性公网IP:

img

全选默认,然后创建:

img

创建SLB负载均衡:

img

这次就可以成功创建了:

img

绑定刚才创建的SLB负载均衡:

img

img

访问公网IP就能处理请求了:

img

1.4 参数优化和故障诊断

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

  • 社区版只能使用串行垃圾回收器(Serial GC),使用串行垃圾回收器的默认最大 Java 堆大小会设置为物理内存大小的 80%,调整方式为使用 -Xmx最大堆大小。如果希望在编译期就指定该大小,可以在编译时添加参数-R:MaxHeapSize=最大堆大小。
  • G1垃圾回收器只能在企业版中使用,开启方式为添加–gc=G1参数,有效降低垃圾回收的延迟。
  • 另外提供一个Epsilon GC,开启方式:–gc=epsilon ,它不会产生任何的垃圾回收行为所以没有额外的内存、CPU开销。如果在公有云上运行的程序生命周期短暂不产生大量的对象,可以使用该垃圾回收器,以节省最大的资源。

-XX:+PrintGC -XX:+VerboseGC 参数打印垃圾回收详细信息。

添加虚拟机参数:

img

打印出了垃圾回收的信息:

img

内存快照文件的获取

需求:

获得运行中的内存快照文件,使用MAT进行分析。

步骤:

1、编译程序时,添加 --enable-monitoring=heapdump,参数添加到pom文件的对应插件中。

1
2
3
4
5
6
7
8
9
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<arg>--enable-monitoring=heapdump,jfr</arg>
</buildArgs>
</configuration>
</plugin>

2、运行中使用 kill -SIGUSR1 进程ID 命令,创建内存快照文件。

img

3、使用MAT分析内存快照文件。

img

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

JDK Flight Recorder (JFR) 是一个内置于 JVM 中的工具,可以收集正在运行中的 Java 应用程序的诊断和分析数据,比如线程、异常等内容。GraalVM本地镜像也支持使用JFR生成运行时数据,导出的数据可以使用VisualVM分析。

步骤:

1、编译程序时,添加 --enable-monitoring=jfr,参数添加到pom文件的对应插件中。

1
2
3
4
5
6
7
8
9
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<arg>--enable-monitoring=heapdump,jfr</arg>
</buildArgs>
</configuration>
</plugin>

2、运行程序,添加 -XX:StartFlightRecording=filename=recording.jfr,duration=10s参数。

img

3、使用VisualVM分析JFR记录文件。

img

img

2、新一代的GC

2.1 垃圾回收器的技术演进

img

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

img

2.2 Shenandoah GC

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

img

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

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

选择方式如下:

{aarch64, arm32-hflt, mipsel, mips64el, ppc64le, s390x, x86_32, x86_64}:架构,使用arch命令选择对应的的架构。

{server,zero}:虚拟机类型,选择server,包含所有GC的功能。

{release, fastdebug, Slowdebug, optimization}:不同的优化级别,选择release,性能最高。

{gcc*-glibc*, msvc*}:编译器的版本,选择较高的版本性能好一些,如果兼容性有问题(无法启动),选择较低的版本。

img

2、配置。将OpenJDK配置到环境变量中,使用java –version进行测试。打印出如下内容代表成功。

img

3、添加参数,运行Java程序。

-XX:+UseShenandoahGC 开启Shenandoah GC

-Xlog:gc 打印GC日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/*
* Copyright (c) 2005, 2014, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

package org.sample;

import com.sun.management.OperatingSystemMXBean;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

//执行5轮预热,每次持续2秒
@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
//输出毫秒单位
@OutputTimeUnit(TimeUnit.MILLISECONDS)
//统计方法执行的平均耗时
@BenchmarkMode(Mode.AverageTime)
//java -jar benchmarks.jar -rf json
@State(Scope.Benchmark)
public class MyBenchmark {

//每次测试对象大小 4KB和4MB
@Param({"4","4096"})
int perSize;

private void test(Blackhole blackhole){

//每次循环创建堆内存60%对象 JMX获取到Java运行中的实时数据
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
//获取堆内存大小
MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
//获取到剩余的堆内存大小
long heapSize = (long) ((heapMemoryUsage.getMax() - heapMemoryUsage.getUsed()) * 0.6);
//计算循环次数
long size = heapSize / (1024 * perSize);

for (int i = 0; i < 4; i++) {
List<byte[]> objects = new ArrayList<>((int)size);
for (int j = 0; j < size; j++) {
objects.add(new byte[1024 * perSize]);
}
blackhole.consume(objects);
}
}

@Benchmark
@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseSerialGC"})
public void serialGC(Blackhole blackhole){
test(blackhole);
}

@Benchmark
@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseParallelGC"})
public void parallelGC(Blackhole blackhole){
test(blackhole);
}

@Benchmark
@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g"})
public void g1(Blackhole blackhole){
test(blackhole);
}

@Benchmark
@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseShenandoahGC"})
public void shenandoahGC(Blackhole blackhole){
test(blackhole);
}


public static void main(String[] args) throws RunnerException {

Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(1)
.build();

new Runner(opt).run();
}
}

测试结果:

img

Shenandoah GC对小对象的GC停顿很短,但是大对象效果不佳。

2.3 ZGC

ZGC 是一种可扩展的低延迟垃圾回收器。ZGC 在垃圾回收过程中,STW的时间不会超过一毫秒,适合需要低延迟的应用。支持几百兆到16TB 的堆大小,堆大小对STW的时间基本没有影响。

ZGC降低了停顿时间,能降低接口的最大耗时,提升用户体验。但是吞吐量不佳,所以如果Java服务比较关注QPS(每秒的查询次数)那么G1是比较不错的选择。

img

ZGC的使用

OracleJDK和OpenJDK中都支持ZGC,阿里的DragonWell龙井JDK也支持ZGC但属于其自行对OpenJDK 11的ZGC进行优化的版本。

建议使用JDK17之后的版本,延迟较低同时无需手动配置并行线程数。

分代 ZGC添加如下参数启用 -XX:+UseZGC -XX:+ZGenerational

非分代 ZGC通过命令行选项启用 -XX:+UseZGC

img

ZGC的环境搭建

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

  • 自动设置年轻代大小,无需设置-Xmn参数。

自动晋升阈值(复制中存活多少次才搬运到老年代),无需设置-XX:TenuringThreshold。

JDK17之后支持自动的并行线程数,无需设置-XX:ConcGCThreads。

  • 需要设置的参数:

    • -Xmx 值 最大堆内存大小
    • 这是ZGC最重要的一个参数,必须设置。ZGC在运行过程中会使用一部分内存用来处理垃圾回收,所以尽量保证堆中有足够的空间。设置多少值取决于对象分配的速度,根据测试情况来决定。
  • 可以设置的参数:

    • -XX:SoftMaxHeapSize=值
    • ZGC会尽量保证堆内存小于该值,这样在内存靠近这个值时会尽早地进行垃圾回收,但是依然有可能会超过该值。
    • 例如,-Xmx5g -XX:SoftMaxHeapSize=4g 这个参数设置,ZGC会尽量保证堆内存小于4GB,最多不会超过5GB。
1
2
3
4
5
6
7
8
9
10
11
@Benchmark
@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseZGC","-XX:+UseLargePages"})
public void zGC(Blackhole blackhole){
test(blackhole);
}

@Benchmark
@Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseZGC","-XX:+ZGenerational","-XX:+UseLargePages"})
public void zGCGenerational(Blackhole blackhole){
test(blackhole);
}

img

ZGC整体表现还是非常不错的,分代也让ZGC的停顿时间有更好的表现。

ZGC调优

ZGC 中可以使用Linux的Huge Page大页技术优化性能,提升吞吐量、降低延迟。

注意:安装过程需要 root 权限,所以ZGC默认没有开启此功能。

操作步骤:

1、计算所需页数,Linux x86架构中大页大小为2MB,根据所需堆内存的大小估算大页数量。比如堆空间需要16G,预留2G(JVM需要额外的一些非堆空间),那么页数就是18G / 2MB = 9216。

2、配置系统的大页池以具有所需的页数(需要root权限):

$ echo 9216 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

3、添加参数-XX:+UseLargePages 启动程序进行测试

2.4 实战案例(测试下g1、Shenandoah、ZGC这三种垃圾回收器)

需求:

Java服务中存在大量软引用的缓存导致内存不足,测试下g1、Shenandoah、ZGC这三种垃圾回收器在这种场景下的回收情况。

步骤:

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.itheima.jvmoptimize.fullgcdemo;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.SneakyThrows;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@RestController
@RequestMapping("/fullgc")
public class Demo2Controller {

private Cache cache = Caffeine.newBuilder().weakKeys().softValues().build();
private List<Object> objs = new ArrayList<>();

private static final int _1MB = 1024 * 1024;

//FULLGC测试
//-Xms8g -Xmx8g -Xss256k -XX:MaxMetaspaceSize=512m -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/test.hprof -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
//ps + po 50并发 260ms 100并发 474 200并发 930
//cms -XX:+UseParNewGC -XX:+UseConcMarkSweepGC 50并发 157ms 200并发 833
//g1 JDK11 并发200 248
@GetMapping("/1")
public void test() throws InterruptedException {
cache.put(RandomStringUtils.randomAlphabetic(8),new byte[10 * _1MB]);
}

}

1、启动程序,添加不同的虚拟机参数进行测试。

img

2、使用Apache Benchmark测试工具对本机进行压测。

img

3、生成GC日志,使用GcEasy进行分析。

4、对比压测之后的结果。

img

2.5对比一下G1、Shenandoah 和 ZGC 这三款垃圾回收器

G1、Shenandoah 和 ZGC 都是现代 Java 的低延迟 GC 回收器。G1 将堆划分为 Region,通过并发标记和 STW 压缩来减少 full GC,适合通用场景;Shenandoah 实现了并发压缩,通过转发表解决引用更新问题,可将 GC 停顿控制在毫秒级;ZGC 采用 colored pointer 技术,实现了全并发、几乎全程无停顿的 GC,停顿时间低于 1ms,适用于延迟极敏感的大型系统。

回收器 一句话记住它
G1 GC 区域化管理,目标是均衡停顿和吞吐
Shenandoah 并发压缩,目标是最小停顿(MS 级)
ZGC 染色指针,全并发回收,目标是亚毫秒级停顿

3、揭秘Java工具

在Java的世界中,除了Java编写的业务系统之外,还有一类程序也需要Java程序员参与编写,这类程序就是Java工具。

常见的Java工具有以下几类:

1、诊断类工具,如Arthas、VisualVM等。

2、开发类工具,如Idea、Eclipse。

3、APM应用性能监测工具,如Skywalking、Zipkin等。

4、热部署工具,如Jrebel等。

3.1 Java工具的核心:Java Agent技术

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

img

Java Agent技术实现了让Java程序执行独立的Java Agent程序中的代码,执行方式有两种:

  • 静态加载模式

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

img

接下来使用以下命令启动Java程序,此时Java虚拟机将会加载agent中的代码并执行。

img

premain方法会在主线程中执行:

img

  • 动态加载模式

动态加载模式可以随时让java agent代码执行,适用于Arthas等诊断系统。动态加载模式需要在Java Agent的项目中编写一个agentmain的方法,并打包成jar包。

img

接下来使用以下代码就可以让java agent代码在指定的java进程中执行了

img

agentmain方法会在独立线程中执行:

img

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

步骤:

1、创建maven项目,添加maven-assembly-plugin插件,此插件可以打包出java agent的jar包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<!-- 所有依赖打入同一个jar包中-->
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!-- 指定java agent相关配置文件-->
<manifestFile>src/main/resources/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
<plugins>
<build>

2、编写类和premain方法,premain方法中打印一行信息。

1
2
3
4
5
6
7
8
9
10
11
public class AgentDemo {

/**
* 参数添加模式 启动java主程序时添加 -javaangent:agent路径
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("java agent执行了...");
}
}

3、编写MANIFEST.MF文件,此文件主要用于描述java agent的配置属性,比如使用哪一个类的premain方法。

1
2
3
4
5
6
Manifest-Version: 1.0
Premain-Class: com.itheima.jvm.javaagent.AgentDemo
Agent-Class: com.itheima.jvm.javaagent.AgentDemo
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true

4、使用maven-assembly-plugin进行打包。

img

5、创建spring boot应用,并静态加载上一步打包完的java agent。

img

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

步骤:

1、创建maven项目,添加maven-assembly-plugin插件,此插件可以打包出java agent的jar包。

2、编写类和agentmain方法, agentmain方法中打印一行信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.itheima.jvm.javaagent.demo01;

import java.lang.instrument.Instrumentation;

public class AgentDemo {

/**
* 参数添加模式 启动java主程序时添加 -javaangent:agent路径
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("java agent执行了...");
}

/**
* attach 挂载模式 java主程序运行之后,随时可以将agent挂载上去
*/

public static void agentmain(String agentArgs, Instrumentation inst) {
//打印线程名称
System.out.println(Thread.currentThread().getName());
System.out.println("attach模式执行了...");
}
}

3、编写MANIFEST.MF文件,此文件主要用于描述java agent的配置属性,比如使用哪一个类的agentmain方法。

4、使用maven-assembly-plugin进行打包。

5、编写main方法,动态连接到运行中的java程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itheima.jvm.javaagent.demo01;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
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, AgentLoadException, AgentInitializationException {
VirtualMachine vm = VirtualMachine.attach("24200");
vm.loadAgent("D:\\jvm-java-agent\\target\\itheima-jvm-java-agent-jar-with-dependencies.jar");
}
}

3.2 实战案例1:简化版的Arthas(动态加载模式)

需求:

编写一个简化版的Arthas程序,具备以下几个功能:

1、查看内存使用情况

2、生成堆内存快照

3、打印栈信息

4、打印类加载器

5、打印类的源码

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

需求:

该程序是一个独立的Jar包,可以应用于任何Java编写的系统中。

具备以下特点:代码无侵入性、操作简单、性能高。

1、查看内存使用情况

JDK从1.5开始提供了Java Management Extensions (JMX) 技术,通过Mbean对象的写入和获取,实现:

运行时配置的获取和更改

应用程序运行信息的获取(线程栈、内存、类信息等)

img

获取JVM默认提供的Mbean可以通过如下的方式,例如获取内存信息:

img

ManagementFactory提供了一系列的方法获取各种各样的信息:

img

功能目标:打印 JVM 的内存使用情况

包括:

  • 堆内存(如 Eden、Old、Survivor)
  • 非堆内存(Metaspace、Code Cache)
  • 直接内存(NIO DirectBuffer)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package com.itheima.jvm.javaagent.demo02;

import java.lang.instrument.Instrumentation;
import java.lang.management.*;
import java.util.List;

/**
* 1、查询所有进程
* 2、显示内存相关的信息
*/
public class AgentDemo {

/**
* 参数添加模式 启动java主程序时添加 -javaangent:agent路径
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("java agent执行了...");
}

/**
* attach 挂载模式 java主程序运行之后,随时可以将agent挂载上去
*/

//-XX:+UseSerialGC -Xmx1g -Xms512m
public static void agentmain(String agentArgs, Instrumentation inst) {
//打印内存的使用情况
memory();
}

//获取内存信息
private static void memory(){
List<MemoryPoolMXBean> memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans();

System.out.println("堆内存:");
//获取堆内存
getMemoryInfo(memoryPoolMXBeans, MemoryType.HEAP);

//获取非堆内存
System.out.println("非堆内存:");
getMemoryInfo(memoryPoolMXBeans, MemoryType.NON_HEAP);

//nio使用的直接内存
try{
@SuppressWarnings("rawtypes")
Class bufferPoolMXBeanClass = Class.forName("java.lang.management.BufferPoolMXBean");
@SuppressWarnings("unchecked")
List<BufferPoolMXBean> bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(bufferPoolMXBeanClass);
for (BufferPoolMXBean mbean : bufferPoolMXBeans) {
StringBuilder sb = new StringBuilder();
sb
.append("name:")
.append(mbean.getName())

.append(" used:")
.append(mbean.getMemoryUsed()/ 1024 / 1024)
.append("m")

.append(" max:")
.append(mbean.getTotalCapacity() / 1024 / 1024)
.append("m");

System.out.println(sb);
}
}catch (Exception e){
System.out.println(e);
}

}

private static void getMemoryInfo(List<MemoryPoolMXBean> memoryPoolMXBeans, MemoryType heap) {
memoryPoolMXBeans.stream().filter(x -> x.getType().equals(heap))
.forEach(x -> {
StringBuilder sb = new StringBuilder();
sb
.append("name:")
.append(x.getName())

.append(" used:")
.append(x.getUsage().getUsed() / 1024 / 1024)
.append("m")

.append(" max:")
.append(x.getUsage().getMax() / 1024 / 1024)
.append("m")

.append(" committed:")
.append(x.getUsage().getCommitted() / 1024 / 1024)
.append("m");

System.out.println(sb);
});
}

public static void main(String[] args) {
memory();
}
}

img

img

img

img

img

这段代码实现了通过 Java Agent 动态 attach 到目标进程后,使用 JMX(管理 API)打印出 堆、非堆和直接内存 的使用信息,等同于你用 Arthas 或 jcmd 查看内存状态,是 Java Agent 的典型实战案例之一。

2、生成堆内存快照

目标:生成 .hprof 格式的堆内存快照文件,用于分析内存使用情况(可以用 Eclipse MAT、VisualVM、jhat 等工具打开分析)。

img

更多的信息可以通过ManagementFactory.getPlatformMXBeans获取,比如:

img

通过这种方式,获取到了Java虚拟机中分配的直接内存和内存映射缓冲区的大小。

img

获取到虚拟机诊断用的MXBean,通过这个Bean对象可以生成内存快照。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void heapDump(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
String filename = simpleDateFormat.format(new Date()) + ".hprof";
System.out.println("生成内存dump文件,文件名为:" + filename);

HotSpotDiagnosticMXBean hotSpotDiagnosticMXBean =
ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);

try {
hotSpotDiagnosticMXBean.dumpHeap(filename, true);
} catch (IOException e) {
e.printStackTrace();
}
}

img

img

img

这一部分的代码通过 HotSpotDiagnosticMXBean 在运行时生成 .hprof 格式的堆快照文件,方便我们用可视化工具进行离线内存分析,是 Java Agent 实战中“主动触发内存快照”的核心技能。

3、打印栈信息

目标:模拟类似于 jstack 命令的作用,打印当前 JVM 中所有线程的状态、名称、ID、栈帧信息

这对排查线程死锁、卡顿、长时间阻塞等问题非常有用!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.itheima.jvm.javaagent.demo03;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class ThreadCommand {

public static void printStackInfo(){
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] infos = threadMXBean.dumpAllThreads(threadMXBean.isObjectMonitorUsageSupported(),
threadMXBean.isSynchronizerUsageSupported());
for (ThreadInfo info : infos) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("name:")
.append(info.getThreadName())
.append(" threadId:")
.append(info.getThreadId())
.append(" state:")
.append(info.getThreadState())
;
System.out.println(stringBuilder);

StackTraceElement[] stackTrace = info.getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
System.out.println(stackTraceElement.toString());
}

System.out.println();
}
}


public static void main(String[] args) {
printStackInfo();
}
}

img

img

img

这一部分通过 ThreadMXBeandumpAllThreads 方法模拟了 jstack 工具的效果,帮助我们程序内实时获取所有线程的栈信息,是 Java Agent 实战中非常实用的诊断能力。

4、打印类加载器

目标:打印当前 JVM 中所有使用过的类加载器,并以字符串形式输出(便于分析和可视化)。

Java Agent中可以获得Java虚拟机提供的Instumentation对象:

img

img

该对象有以下几个作用: 1、redefine,重新设置类的字节码信息。 2、retransform,根据现有类的字节码信息进行增强。 3、获取所有已加载的类信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.itheima.jvm.javaagent.demo04;

import org.jd.core.v1.ClassFileToJavaSourceDecompiler;
import org.jd.core.v1.api.loader.Loader;
import org.jd.core.v1.api.loader.LoaderException;
import org.jd.core.v1.api.printer.Printer;

import java.lang.instrument.*;
import java.security.ProtectionDomain;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.stream.Collectors;

public class ClassCommand {

//获取所有类加载器
private static Set<ClassLoader> getAllClassLoader(Instrumentation inst){
HashSet<ClassLoader> classLoaders = new HashSet<>();
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class clazz : allLoadedClasses) {
ClassLoader classLoader = clazz.getClassLoader();
classLoaders.add(classLoader);
}

return classLoaders;
}

public static void printAllClassLoader(Instrumentation inst){
Set<ClassLoader> allClassLoader = getAllClassLoader(inst);
String result = allClassLoader.stream().map(x -> {
if (x ==null) {
return "BootStrapClassLoader";
} else {
return x.toString();
}
}).distinct().sorted(String::compareTo).collect(Collectors.joining(","));
System.out.println(result);
}
}

img

img

img

这段代码通过 Java Agent 获取 JVM 中所有类的加载器,并打印出所有 曾被用过的类加载器类型与实例信息,便于我们分析类加载体系、调试类冲突或 classpath 问题。

5、打印类的源码

目标:通过字节码 → 源码的反编译过程,获取目标类的源码内容,哪怕没有源文件也能“还原”类的逻辑。

打印类的源码需要分为以下几个步骤

1、获得内存中的类的字节码信息。利用Instrumentation提供的转换器来获取字节码信息。

img

img

2、通过反编译工具将字节码信息还原成源代码信息。

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

Pom添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<dependency>
<groupId>org.jd</groupId>
<artifactId>jd-core</artifactId>
<version>1.1.3</version>
</dependency>
//获取类信息
public static void printClass(Instrumentation inst){
Scanner scanner = new Scanner(System.in);
System.out.println("请输入类名:");
String next = scanner.next();
Class[] allLoadedClasses = inst.getAllLoadedClasses();
System.out.println("要查找的类名是:" + next);
//匹配类名
for (Class clazz : allLoadedClasses) {
if(clazz.getName().equals(next)){
System.out.println("找到了类,类加载器为:" + clazz.getClassLoader());
ClassFileTransformer transformer = new ClassFileTransformer() {
@Override
public byte[] transform(Module module, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
ClassFileToJavaSourceDecompiler classFileToJavaSourceDecompiler = new ClassFileToJavaSourceDecompiler();

Printer printer = new Printer() {
protected static final String TAB = " ";
protected static final String NEWLINE = "\n";

protected int indentationCount = 0;
protected StringBuilder sb = new StringBuilder();

@Override public String toString() { return sb.toString(); }

@Override public void start(int maxLineNumber, int majorVersion, int minorVersion) {}
@Override public void end() {
System.out.println(sb.toString());
}

@Override public void printText(String text) { sb.append(text); }
@Override public void printNumericConstant(String constant) { sb.append(constant); }
@Override public void printStringConstant(String constant, String ownerInternalName) { sb.append(constant); }
@Override public void printKeyword(String keyword) { sb.append(keyword); }
@Override public void printDeclaration(int type, String internalTypeName, String name, String descriptor) { sb.append(name); }
@Override public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) { sb.append(name); }

@Override public void indent() { this.indentationCount++; }
@Override public void unindent() { this.indentationCount--; }

@Override public void startLine(int lineNumber) { for (int i=0; i<indentationCount; i++) sb.append(TAB); }
@Override public void endLine() { sb.append(NEWLINE); }
@Override public void extraLine(int count) { while (count-- > 0) sb.append(NEWLINE); }

@Override public void startMarker(int type) {}
@Override public void endMarker(int type) {}
};

try {
classFileToJavaSourceDecompiler.decompile(new Loader() {
@Override
public boolean canLoad(String s) {
return false;
}

@Override
public byte[] load(String s) throws LoaderException {
return classfileBuffer;
}
},printer,className);
} catch (Exception e) {
e.printStackTrace();
}
//System.out.println(new String(classfileBuffer));
return ClassFileTransformer.super.transform(module, loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
}
};

inst.addTransformer(transformer,true);
try {
inst.retransformClasses(clazz);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}finally {
inst.removeTransformer(transformer);
}

}
}
}

img

img

img

img

img

这段代码通过 Java Agent 动态抓取 JVM 中已加载类的字节码,并借助 JD-Core 库将其反编译为源码,即便没有源码文件,也能还原类的结构和方法内容,在调试、排查、逆向分析中非常有用。

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

Spring AOP是不是也可以实现类似的功能呢?

Spring AOP 确实也能做到类似功能,但存在 以下局限

问题 说明
框架耦合性强 项目需使用 Spring 才可用
无法动态启用/禁用 AOP 功能部署后较为静态
有入侵性 必须配置切面、注解、或包路径
对第三方类无能为力 无法增强 JDK/外部依赖类的方法

所以使用Java Agent技术 + 字节码增强技术,就可以解决上述三个问题。

img

框架 优点 缺点
ASM 精细控制指令级别、性能极高 编写复杂、学习曲线陡峭
Byte Buddy 接口友好、可读性好、社区活跃 性能略逊(但足够用了)
ASM字节码增强技术

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

img

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

ASM的官方网址:https://asm.ow2.io/ 操作步骤:

1、引入依赖

1
2
3
4
5
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.6</version>
</dependency>

2、搭建基础框架,此代码为固定代码。

img

3、编写一个类描述如何去增强类,类需要继承自MethodVisitor

ASM基础案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package com.itheima.jvm.javaagent.demo05;

import org.objectweb.asm.*;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;

import static org.objectweb.asm.Opcodes.*;

public class ASMDemo {

public static byte[] classASM(byte[] bytes){
ClassWriter cw = new ClassWriter(0);
// cv forwards all events to cw
ClassVisitor cv = new ClassVisitor(ASM7, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(this.api,mv);
}
};
ClassReader cr = new ClassReader(bytes);
cr.accept(cv, 0);

return cw.toByteArray();
}

public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
InputStream inputStream = ASMDemo.class.getResourceAsStream("/com/itheima/jvm/javaagent/demo05/ASMDemo.class");
byte[] b1 = inputStream.readAllBytes();

byte[] b2 = classASM(b1); // b2 represents the same class as b1

//创建类加载器
MyClassLoader myClassLoader = new MyClassLoader();
Class clazz = myClassLoader.defineClass("com.itheima.jvm.javaagent.demo05.ASMDemo", b2);
clazz.getDeclaredConstructor().newInstance();
}
}

class MyClassLoader extends ClassLoader {
public Class defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}

class MyMethodVisitor extends MethodVisitor {

public MyMethodVisitor(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}

@Override
public void visitCode() {
mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
mv.visitLdcInsn("开始执行");
mv.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);
super.visitCode();
}

@Override
public void visitInsn(int opcode) {
if(opcode == ARETURN || opcode == RETURN ) {
mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
mv.visitLdcInsn("结束执行");
mv.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);
}
super.visitInsn(opcode);
}

@Override
public void visitEnd() {
mv.visitMaxs(20,50);
super.visitEnd();
}

}

img

Byte Buddy字节码增强技术(推荐)

Byte Buddy 是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。 Byte Buddy底层基于ASM,提供了非常方便的 API。

img

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

操作步骤:

1、引入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.10</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.10</version>
</dependency>

2、搭建基础框架,此代码为固定代码

img

3、编写一个Advice通知描述如何去增强类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.itheima.jvm.javaagent.demo05;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.matcher.ElementMatchers;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;

public class ByteBuddyDemo {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {

Foo foo = new Foo();
MyClassLoader myClassLoader = new MyClassLoader();

Class<? extends Foo> newClazz = new ByteBuddy()
.subclass(Foo.class)
.method(ElementMatchers.any())
.intercept(Advice.to(MyAdvice.class))
.make()
.load(myClassLoader)
.getLoaded();

Foo foo1 = newClazz.getDeclaredConstructor().newInstance();
foo1.test();
}
}

class MyAdvice {
@Advice.OnMethodEnter
static void onEnter(){
System.out.println("方法进入");
}

@Advice.OnMethodExit
static void onExit(){
System.out.println("方法退出");
}

}

增强后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.itheima.jvm.javaagent.demo05;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
import org.jd.core.v1.ClassFileToJavaSourceDecompiler;
import org.jd.core.v1.api.loader.Loader;
import org.jd.core.v1.api.loader.LoaderException;
import org.jd.core.v1.api.printer.Printer;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import java.util.Scanner;

import static net.bytebuddy.matcher.ElementMatchers.isMethod;

public class ClassEnhancerCommand {


//获取类信息
public static void enhanceClass(Instrumentation inst){
Scanner scanner = new Scanner(System.in);
System.out.println("请输入类名:");
String next = scanner.next();
Class[] allLoadedClasses = inst.getAllLoadedClasses();
System.out.println("要查找的类名是:" + next);
//匹配类名
for (Class clazz : allLoadedClasses) {
if(clazz.getName().equals(next)){
System.out.println("找到了类,类加载器为:" + clazz.getClassLoader());

new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with( //new AgentBuilder.Listener.WithErrorsOnly(
new AgentBuilder.Listener.WithTransformationsOnly(
AgentBuilder.Listener.StreamWriting.toSystemOut()))
//.type(ElementMatchers.isAnnotatedWith(named("org.springframework.web.bind.annotation.RestController")))
.type(ElementMatchers.named(clazz.getName()))
.transform((builder, type, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(MyAdvice.class).on(ElementMatchers.any()))
// builder .method(ElementMatchers.any())
// .intercept(MethodDelegation.to(MyInterceptor.class))
)
.installOn(inst);
}
}
}
}
package com.itheima.jvm.javaagent.demo07;

import net.bytebuddy.asm.Advice;

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) + "纳秒");
}
}

img

最后将整个简化版的arthas进行打包,在服务器上进行测试。使用maven-shade-plugin插件可以将所有依赖打入同一个jar包中并指定入口main方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!--打包成jar包使用-->

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>itheima-attach-agent</finalName>
<transformers>
<!--java -jar 默认启动的主类-->
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.itheima.jvm.javaagent.AttachMain</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

img

以下是简化版 Arthas 实战项目(基于 Java Agent)的完整流程梳理总结,专为面试场景设计,既体现技术深度,又便于表达。


✅ 一句话总览

我做过一个基于 Java Agent 的实战项目,用于非侵入地监控和增强运行中 Java 程序的能力,具备查看线程栈、类加载器、方法参数与耗时等功能,类似简化版的 Arthas 工具。


✅ 项目目标:打造轻量级诊断工具

通过 Java Agent + 字节码增强,无需重启、无框架依赖,实现对运行中 Java 程序的动态观测与增强


✅ 整体流程分为六大功能模块:

1️⃣ Attach 技术引入 Agent

  • 使用 AttachMain 程序 + VirtualMachine 实现远程 attach;
  • 注入我们自定义的 Agent jar;
  • 在 jar 包中实现 agentmain(String agentArgs, Instrumentation inst) 方法作为入口;
  • 利用 Instrumentation 对运行中的 JVM 进行控制。

💡 亮点:无需程序预设,运行中可附加 agent。


2️⃣ 打印线程栈信息(类似 thread 命令)

  • 使用 ManagementFactory.getThreadMXBean() 获取线程信息;
  • 遍历 ThreadInfo[],打印线程名、状态、堆栈轨迹。

🔥 面试可说:

实现了类似 Arthas 的 thread 命令,定位死锁、阻塞线程。


3️⃣ 获取所有类加载器信息(类似 classloader 命令)

  • 通过 inst.getAllLoadedClasses() 拿到所有已加载类;
  • 提取并打印其 ClassLoader,区分 AppClassLoader、Bootstrap 等。

🔥 面试可说:

可用于排查类冲突问题、Spring类加载器多重父子关系问题。


4️⃣ 实时反编译字节码(类似 jad 命令)

  • 使用 jd-core 反编译框架;
  • 将目标类的 byte[] 读取后通过 ClassFileToJavaSourceDecompiler 转换为源码。

🔥 面试可说:

可快速分析三方 jar 或自己系统中的已加载类源码逻辑。


5️⃣ 打印方法参数和执行耗时(核心功能)

  • 使用 字节码增强(ASM 或 Byte Buddy);
  • System.nanoTime() 写入方法前后;
  • 通过 @Advice.OnMethodEnter@Advice.OnMethodExit 拦截并打印:
1
2
3
4
5
@Advice.OnMethodEnter
static long enter(@Advice.AllArguments Object[] args)

@Advice.OnMethodExit
static void exit(@Advice.Enter long startTime)

🔥 面试可说:

类似 Arthas 的 monitor / watch 命令,解决线上性能分析和异常追踪问题。


6️⃣ 打包部署:shade 插件构建 Fat-Jar

  • 使用 maven-shade-plugin 构建带入口的 fat-jar;
  • 所有依赖打包进 jar,方便在生产环境中部署;
  • 入口设置为 AttachMain,可直接运行。

✅ 技术要点总结(关键词速记)

关键词 面试描述
Java Agent 动态插桩、运行时增强、Instrumentation API
agentmain() 动态 attach 的入口函数
ASM / Byte Buddy 字节码增强框架,分别代表底层控制与高层封装
ThreadMXBean JVM 线程监控接口,打印线程栈
getAllLoadedClasses() 获取所有类及其 ClassLoader
jd-core Java 反编译工具
maven-shade-plugin 构建可执行的 fat jar
VirtualMachine.attach() 远程挂载 agent 的关键接口

✅ 面试回答模板(通用)

是的,我做过一个简化版的 Arthas 工具实战,基于 Java Agent + 字节码增强实现。它能在不修改业务代码、不重启服务的前提下,动态 attach 到目标 JVM,实现打印线程堆栈、类加载器结构、方法参数与执行耗时等功能。我们使用了 ASM 和 Byte Buddy 两种增强方式,并打包成 Fat Jar 方便部署。这个工具解决了我们线上定位问题时代码侵入大、监控不灵活的问题。


3.3 实战案例2:APM系统的数据采集(静态加载模式)

Application performance monitor (APM) 应用程序性能监控系统是采集运行程序的实时数据并使用可视化的方式展示,使用APM可以确保系统可用性,优化服务性能和响应时间,持续改善用户体验。常用的APM系统有Apache Skywalking、Zipkin等。

Skywalking官方网站: https://skywalking.apache.org/

img

img

需求:

编写一个简化版的APM数据采集程序,具备以下几个功能:

1、无侵入性获取spring boot应用中,controller层方法的调用时间。

2、将所有调用时间写入文件中。

问题:

Java agent 采用静态加载模式 还是 动态加载模式?

一般程序启动之后就需要持续地进行信息的采集,所以采用静态加载模式。

Java Agent参数的获取

在Java Agent中,可以通过如下的方式传递参数:

java -javaagent:./agent.jar=参数 -jar test.jar

接下来通过premain参数中的agentArgs字段获取:

img

如果有多个参数,可以使用如下方式:

java -javaagent:./agent.jar=param1=value1,param2=value2 -jar test.jar

在Java代码中使用字符串解析出对应的key value。

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

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

img

2、自定义注解

img

3、通过注解注入

img

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.itheima.javaagent;

import com.itheima.javaagent.command.ClassCommand;
import com.itheima.javaagent.command.MemoryCommand;
import com.itheima.javaagent.command.ThreadCommand;
import com.itheima.javaagent.enhancer.AgentParam;
import com.itheima.javaagent.enhancer.MyAdvice;
import com.itheima.javaagent.enhancer.TimingAdvice;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;

import java.lang.instrument.Instrumentation;
import java.util.Scanner;

public class AgentMain {
//premain方法
public static void premain(String agentArgs, Instrumentation inst){
//使用bytebuddy增强类
new AgentBuilder.Default()
//禁止byte buddy处理时修改类名
.disableClassFormatChanges()
//处理时使用retransform增强
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
//打印出错误日志
.with(new AgentBuilder.Listener.WithTransformationsOnly(AgentBuilder.Listener.StreamWriting
.toSystemOut()))
//匹配哪些类
.type(ElementMatchers.isAnnotatedWith(ElementMatchers.named("org.springframework.web.bind.annotation.RestController")
.or(ElementMatchers.named("org.springframework.web.bind.annotation.Controller")))
)
//增强,使用MyAdvice通知,对所有方法都进行增强
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice
.withCustomMapping()
.bind(AgentParam.class,agentArgs)
.to(TimingAdvice.class).on(ElementMatchers.any())))
.installOn(inst);
}



}
package com.itheima.javaagent.enhancer;

import net.bytebuddy.asm.Advice;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

//统计耗时,打印方法名、类名
public class TimingAdvice {

//方法进入时,返回开始时间
@Advice.OnMethodEnter
static long enter(){
return System.nanoTime();
}

//方法退出时候,统计方法执行耗时
@Advice.OnMethodExit
static void exit(@Advice.Enter long value,
@Advice.Origin("#t") String className,
@Advice.Origin("#m") String methodName,
@AgentParam("agent.log") String fileName){
String str = methodName + "@" + className + "耗时为: " + (System.nanoTime() - value) + "纳秒\n";
try {
FileUtils.writeStringToFile(new File(fileName),str, StandardCharsets.UTF_8,true);
} catch (IOException e) {
e.printStackTrace();
}
}
}

修改jar包名字,并重新打包:

img

启动spring boot服务时,添加javaagent的路径,并添加文件名参数:

img

打印结果:

img

下面是你这个 简化版 APM 系统数据采集工具(基于 Java Agent + Byte Buddy)完整案例讲解


✅ 一句话介绍该项目

我实现过一个轻量级 APM 数据采集工具,基于 Java Agent + Byte Buddy 技术,可以无侵入地采集 Spring Boot 项目中 @RestController@Controller 层方法的执行耗时,并将统计数据写入日志文件。


✅ 背景与目标(为什么做这个?)

现实中很多线上系统没有引入全量 APM 组件(如 Skywalking、Pinpoint、Arthas),但依然存在对“接口响应慢”的诊断需求。传统方式要么侵入业务代码、要么依赖框架。我们用 Java Agent 实现了一个低成本、非侵入、跨项目可用的 APM 数据采集工具,部署非常简单。


✅ 核心需求(你解决了什么问题?)

  1. 无侵入监控 Spring Boot Controller 方法耗时
  2. 将采集数据写入自定义日志文件
  3. 采集程序随主程序一同启动,无需额外运维操作
  4. Agent 可接收外部参数配置,如日志路径

✅ 技术选型与方案架构(你是怎么做的?)

🔧 技术组合:

  • Java Agent:用于程序启动时插桩,进行静态加载;
  • Byte Buddy:用于类增强(切面注入);
  • @Advice:注入耗时统计逻辑;
  • Agent 参数注入机制:支持配置采集日志路径;
  • FileUtils(commons-io):将日志输出到文件。

✅ 执行流程详解

1️⃣ 启动方式:静态加载 Agent

1
java -javaagent:./agent.jar=agent.log=apm.log -jar app.jar

解释:

  • -javaagent: 注入 agent jar;
  • agentArgs: 向 agent 传参,agent.log=apm.log 是我们配置的日志输出路径。

2️⃣ AgentMain.premain 实现增强注册

1
public static void premain(String agentArgs, Instrumentation inst)
  • 静态插桩方式:在主程序启动前运行;

  • 通过

    1
    AgentBuilder

    设置:

    • disableClassFormatChanges:避免类名变化;
    • RETRANSFORMATION:可对已加载类再次增强;
    • 匹配类:带有 @RestController@Controller 注解的类;
    • 匹配方法:所有方法(ElementMatchers.any());
    • 增强行为:使用 TimingAdvice 注入逻辑;
    • withCustomMapping():通过注解绑定参数。

3️⃣ 自定义注解与参数注入:@AgentParam

1
.bind(AgentParam.class, agentArgs)
  • 支持从 agentArgs 中提取配置项,自动绑定到增强逻辑;
  • @AgentParam("agent.log") 会自动注入 apm.log 的路径到 Advice 中;
  • 使增强逻辑具备 可配置性与扩展性

4️⃣ 增强逻辑实现:TimingAdvice

1
2
3
4
5
@Advice.OnMethodEnter
static long enter()

@Advice.OnMethodExit
static void exit(@Advice.Enter long value, @Advice.Origin("#m") String methodName, ...)

进入方法时记录时间戳 System.nanoTime(),退出时再次记录并计算差值,即为耗时。

日志格式如下:

1
testMethod@com.itheima.controller.UserController耗时为: 84578纳秒

最后通过 FileUtils.writeStringToFile() 写入到自定义日志文件中。


✅ 面试高频问题 Q&A

Q1:为什么用 Java Agent 而不是 Spring AOP?

Spring AOP 只能在 Spring 项目中使用,且需要侵入配置。而 Java Agent 是非侵入式的,甚至支持非 Spring 项目,并且可以在运行时通过 JVM attach 实现远程诊断。


Q2:为什么用 Byte Buddy 而不是 ASM?

ASM 性能更高,但开发复杂度大、易错,适合底层框架开发。Byte Buddy 封装好、语义更清晰,更适合快速实现功能型增强。


Q3:日志输出到文件的方案如何保证线程安全?

使用了 Apache FileUtils.writeStringToFile(..., true) 实现追加写入,该方法本身是线程安全的,适合轻量级日志收集。若对并发写入量大可加锁或使用队列 + 异步写文件。


Q4:如何扩展为更复杂的 APM 系统?

当前方案已具备基本能力,后续可扩展:

  • 加入 traceId 实现链路追踪;
  • 上报数据到 ELK、Prometheus;
  • 增加方法参数记录(结合 @Advice.AllArguments);
  • 实现告警模块,如接口超时告警等。

✅ 总结你可以怎么说(答题模板)

我做过一个简化版 APM 工具,核心目标是非侵入式地采集 Spring Boot Controller 接口耗时,并输出到指定日志文件。使用的是 Java Agent 静态加载 + Byte Buddy 增强方式,在主程序启动时注入,不影响业务逻辑。并通过 @AgentParam 注解支持配置日志路径,后续还可扩展为完整的链路追踪与异常告警体系。


3.4总结:

Arthas这款工具用到了什么Java技术,有没有了解过?

回答:

Arthas主要使用了Java Agent技术,这种技术可以让运行中的Java程序执行Agent中编写代码。

Arthas使用了Agent中的动态加载模式,可以选择让某个特定的Java进程加载Agent并执行其中的监控代码。监控方面主要使用的就是JMX提供的一些监控指标,同时使用字节码增强技术,对某些类和某些方法进行增强,从而监控方法的执行耗时、参数等内容。

img

APM系统是如何获取到Java程序运行中的性能数据的?

回答:

APM系统比如Skywalking主要使用了Java Agent技术,这种技术可以让运行中的Java程序执行Agent中编写代码。

Skywalking编写了Java Agent,使用了Agent中的静态加载模式,使用字节码增强技术,对某些类和某些方法进行增强,从而监控方法的执行耗时、参数等内容。比如对Controller层方法增强,获取接口调用的时长信息,对数据库连接增强,获取数据库查询的时长、SQL语句等信息。

img

img

高级篇完结