Async-profiler:低开销Java性能分析利器
项目标题与描述
Async-profiler 是一个针对Java的低开销采样性能分析器,它克服了传统分析器的“安全点偏差”(Safepoint bias)问题。项目利用了HotSpot JVM特有的API来收集堆栈踪迹和跟踪内存分配,能够分析非Java线程(例如GC和JIT编译线程),并在堆栈跟踪中显示本地和内核帧。
它支持多种分析模式,包括CPU时间、Java堆内存分配、本地内存分配与泄漏、竞争锁、硬件和软件性能计数器(如缓存未命中、页面错误、上下文切换等)。
功能特性
- 无安全点偏差采样:采用异步采样机制,避免传统Java分析器的固有缺陷。
- 多维度性能分析:
- CPU时间分析
- Java堆内存分配分析
- 本地内存分配与泄漏跟踪
- 锁竞争分析
- 硬件/软件性能计数器分析(如缓存未命中、页面错误等)
- 线程与栈帧分析:
- 监控非Java线程(如GC、JIT线程)
- 在堆栈跟踪中展示本地(Native)和内核(Kernel)帧
- 丰富的输出格式:
- 交互式火焰图(Flame Graph)
- Java飞行记录器(JFR)格式
- OpenTelemetry格式
- 文本格式报告
- 灵活的集成方式:
- 命令行工具(
asprof) - Java API
- 原生API(C API)
- 可通过代理方式集成
- 命令行工具(
- 平台支持:
- 官方支持Linux(x64, arm64)和macOS(x64, arm64)平台
- 社区支持其他架构端口(如x86, arm32, ppc64le, riscv64, loongarch64)
- 多种采样引擎:
- Perf Events(Linux)
- 时钟定时器(CTimer, ITimer)
- Java方法追踪与延迟分析(Instrumentation)
安装指南
下载稳定版本
可直接从GitHub Releases页面下载最新稳定版本(如v4.2.1)的预编译二进制包:
- Linux x64:
async-profiler-4.2.1-linux-x64.tar.gz - Linux arm64:
async-profiler-4.2.1-linux-arm64.tar.gz - macOS arm64/x64:
async-profiler-4.2.1-macos.zip
从源码构建
最低要求:
- GNU Make
- GCC 7.5.0+ 或 Clang 7.0.0+
- 静态版libstdc++(例如在Amazon Linux 2023上:
yum install libstdc++-static) - JDK 11+
构建步骤:
- 确保
gcc、g++和java在PATH环境变量中。 - 导航到async-profiler源码根目录。
- 运行
make命令。构建完成后,启动器asprof将位于build/bin/asprof目录下。 - (可选)运行
make test进行单元和集成测试,或运行make release打包二进制文件。
使用说明
快速开始
分析一个正在运行的Java应用通常只需使用 asprof 命令并指定Java进程的PID。
# 分析PID为1234的进程,持续30秒,并将结果保存为交互式火焰图
$ asprof -d 30 -f flamegraph.html 1234
基础使用示例
CPU性能分析:
$ asprof -e cpu -d 60 -o cpu_profile.html <PID>
内存分配分析(每分配512KB采样一次):
$ asprof -e alloc -d 30 -f alloc_flame.html <PID>
锁竞争分析:
$ asprof -e lock -d 30 -f lock_profile.html <PID>
输出为JFR格式:
$ asprof -d 30 -o profile.jfr <PID>
Java API集成
Async-profiler提供了Java API,可以直接在Java代码中调用。
import one.profiler.AsyncProfiler;
public class ProfilerDemo {
public static void main(String[] args) throws Exception {
AsyncProfiler profiler = AsyncProfiler.getInstance();
// 启动CPU分析
profiler.start("cpu", 10000000); // 10ms间隔
Thread.sleep(30000); // 运行30秒
// 停止并保存结果
String output = profiler.stop();
System.out.println(output);
}
}
原生API (C API) 集成
对于非Java应用或需要更精细控制的场景,可以使用原生C API。
#include "asprof.h"
int main() {
asprof_init();
asprof_error_t err = asprof_execute("start,event=cpu,interval=10ms", NULL);
if (err != NULL) {
fprintf(stderr, "Profiler error: %s\n", asprof_error_str(err));
}
// ... 运行被分析的代码 ...
err = asprof_execute("stop,file=profile.jfr", NULL);
return 0;
}
核心代码
1. CPU采样引擎信号处理核心逻辑 (cpuEngine.cpp)
此代码段展示了CPU采样引擎如何处理采样信号,记录执行样本。
/*
* Copyright The async-profiler authors
* SPDX-License-Identifier: Apache-2.0
*/
#include "cpuEngine.h"
#include "profiler.h"
#include "tsc.h"
void CpuEngine::signalHandler(int signo, siginfo_t* siginfo, void* ucontext) {
if (!_enabled) return;
ExecutionEvent event(TSC::ticks());
// 当估算总CPU时间时,计算错过的样本数
u64 total_cpu_time = _count_overrun ? u64(_interval) * (1 + OS::overrun(siginfo)) : u64(_interval);
Profiler::instance()->recordSample(ucontext, total_cpu_time, EXECUTION_SAMPLE, &event);
}
代码注释:
signalHandler:当配置的采样信号(如SIGPROF)触发时被调用。_enabled:静态标志,指示分析器是否处于活动状态。ExecutionEvent:封装采样时间戳的简单事件对象。TSC::ticks():使用时间戳计数器(TSC)获取高精度纳秒级时间。OS::overrun(siginfo):在支持的情况下,估算因信号队列满而丢失的样本数量,用于更准确地计算总CPU时间。Profiler::instance()->recordSample:将采样事件(包含上下文、时间、事件类型)传递给核心分析器进行记录和处理。
2. 内存分配跟踪引擎 (allocTracer.cpp)
此代码展示了如何通过设置断点来拦截JVM内部的内存分配方法,实现堆内存分配的采样。
/*
* Copyright The async-profiler authors
* SPDX-License-Identifier: Apache-2.0
*/
#include "allocTracer.h"
#include "profiler.h"
#include "stackFrame.h"
#include "tsc.h"
#include "vmStructs.h"
// 当我们的断点陷阱被触发时调用
void AllocTracer::trapHandler(int signo, siginfo_t* siginfo, void* ucontext) {
StackFrame frame(ucontext);
EventType event_type;
uintptr_t total_size;
uintptr_t instance_size;
// PC指向BREAKPOINT指令或下一条指令
if (_in_new_tlab.covers(frame.pc())) {
// send_allocation_in_new_tlab(...)
event_type = ALLOC_SAMPLE;
total_size = _trap_kind == 1 ? frame.arg2() : frame.arg1();
instance_size = _trap_kind == 1 ? frame.arg3() : frame.arg2();
} else if (_outside_tlab.covers(frame.pc())) {
// send_allocation_outside_tlab(...)
event_type = ALLOC_OUTSIDE_TLAB;
total_size = _trap_kind == 1 ? frame.arg2() : frame.arg1();
instance_size = 0;
} else {
// 不是我们的陷阱,交给其他处理程序
Profiler::instance()->trapHandler(signo, siginfo, ucontext);
return;
}
// 通过模拟“ret”指令离开被跟踪的函数
uintptr_t klass = frame.arg0();
frame.ret();
if (_enabled && updateCounter(_allocated_bytes, total_size, _interval)) {
recordAllocation(ucontext, event_type, klass, total_size, instance_size);
}
}
代码注释:
trapHandler:处理由分配断点触发的信号。StackFrame frame(ucontext):从信号上下文(ucontext)中解析出栈帧信息。_in_new_tlab,_outside_tlab:Trap对象,代表在JVM的AllocTracer::send_allocation_in_new_tlab和send_allocation_outside_tlab方法中设置的断点。covers(frame.pc()):检查程序计数器(PC)是否位于特定断点的地址范围内。frame.arg0(),arg1(),arg2(),arg3():根据调用约定(因JDK版本_trap_kind而异)从栈帧或寄存器中提取函数参数(如类指针、分配大小)。frame.ret():修改上下文,模拟从被拦截函数返回,使执行流程继续。updateCounter:基于配置的采样间隔(_interval),原子地更新已分配字节计数器,并决定是否记录当前分配样本。recordAllocation:创建并记录分配事件,包含类信息、大小和时间戳。
3. 栈帧存储与哈希管理 (callTraceStorage.cpp)
这段代码是分析器的核心数据结构,负责高效地存储和检索调用栈踪迹。
/*
* Copyright The async-profiler authors
* SPDX-License-Identifier: Apache-2.0
*/
#include "callTraceStorage.h"
#include "os.h"
u64 CallTraceStorage::calcHash(int num_frames, ASGCT_CallFrame* frames) {
u64 h = 0;
for (int i = 0; i < num_frames; i++) {
// 组合方法ID和行号(BCI)来生成哈希
h = h * 31 + (uintptr_t)frames[i].method_id;
h = h * 31 + frames[i].bci;
}
return h;
}
CallTrace* CallTraceStorage::storeCallTrace(int num_frames, ASGCT_CallFrame* frames) {
u64 hash = calcHash(num_frames, frames);
CallTrace* trace = findCallTrace(_current_table, hash);
if (trace != NULL) {
return trace;
}
// 在分配器中为新调用踪迹分配内存
size_t size = sizeof(CallTrace) + (num_frames - 1) * sizeof(ASGCT_CallFrame);
trace = (CallTrace*)_allocator.alloc(size);
if (trace == NULL) {
// 内存不足,返回溢出标识
_overflow++;
return &_overflow_trace;
}
trace->num_frames = num_frames;
memcpy(trace->frames, frames, num_frames * sizeof(ASGCT_CallFrame));
// 将新踪迹插入哈希表
u32 call_trace_id = _current_table->incSize();
if (call_trace_id >= _current_table->capacity()) {
// 哈希表已满,分配新的更大容量的表
LongHashTable* new_table = LongHashTable::allocate(_current_table, _current_table->capacity() * 2);
if (new_table != NULL) {
_current_table = new_table;
call_trace_id = 0;
} else {
_overflow++;
return &_overflow_trace;
}
}
u64* keys = _current_table->keys();
CallTraceSample* values = _current_table->values();
keys[call_trace_id] = hash;
values[call_trace_id].setTrace(trace);
values[call_trace_id].samples = 0;
values[call_trace_id].counter = 0;
return trace;
}
代码注释:
CallTraceStorage:管理所有唯一调用栈踪迹的存储。calcHash:根据调用栈中所有帧的方法ID和行号(BCI)计算一个哈希值,用于快速查找。storeCallTrace:存储一个新的调用栈踪迹。- 首先通过
findCallTrace在哈希表中查找是否已存在相同栈。 - 如果不存在,使用
LinearAllocator(_allocator) 分配内存。这是一个高性能的自定义分配器,用于快速分配小对象。 - 如果分配失败(或哈希表扩容失败),递增溢出计数器并返回一个预定义的“溢出”踪迹。
- 首先通过
LongHashTable:一个两级哈希表结构,支持并发插入和动态扩容。incSize():原子地增加哈希表大小并返回新条目的索引。setTrace(trace):使用原子存储将踪迹指针设置到哈希表的值槽中,确保多线程环境下的内存可见性。- 该设计实现了去重:相同的调用栈只在内存中存储一次,后续采样只增加该栈对应的计数器,极大节省了内存空间。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)
公众号二维码

公众号二维码


浙公网安备 33010602011771号