项目标题与描述
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 -d30-f flamegraph.html1234基础使用示例
CPU性能分析:
$ asprof -e cpu -d60-o cpu_profile.html<PID>内存分配分析(每分配512KB采样一次):
$ asprof -e alloc -d30-f alloc_flame.html<PID>锁竞争分析:
$ asprof -e lock -d30-f lock_profile.html<PID>输出为JFR格式:
$ asprof -d30-o profile.jfr<PID>Java API集成
Async-profiler提供了Java API,可以直接在Java代码中调用。
importone.profiler.AsyncProfiler;publicclassProfilerDemo{publicstaticvoidmain(String[]args)throwsException{AsyncProfilerprofiler=AsyncProfiler.getInstance();// 启动CPU分析profiler.start("cpu",10000000);// 10ms间隔Thread.sleep(30000);// 运行30秒// 停止并保存结果Stringoutput=profiler.stop();System.out.println(output);}}原生API (C API) 集成
对于非Java应用或需要更精细控制的场景,可以使用原生C API。
#include"asprof.h"intmain(){asprof_init();asprof_error_terr=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);return0;}核心代码
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"voidCpuEngine::signalHandler(intsigno,siginfo_t*siginfo,void*ucontext){if(!_enabled)return;ExecutionEventevent(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"// 当我们的断点陷阱被触发时调用voidAllocTracer::trapHandler(intsigno,siginfo_t*siginfo,void*ucontext){StackFrameframe(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();}elseif(_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"u64CallTraceStorage::calcHash(intnum_frames,ASGCT_CallFrame*frames){u64 h=0;for(inti=0;i<num_frames;i++){// 组合方法ID和行号(BCI)来生成哈希h=h*31+(uintptr_t)frames[i].method_id;h=h*31+frames[i].bci;}returnh;}CallTrace*CallTraceStorage::storeCallTrace(intnum_frames,ASGCT_CallFrame*frames){u64 hash=calcHash(num_frames,frames);CallTrace*trace=findCallTrace(_current_table,hash);if(trace!=NULL){returntrace;}// 在分配器中为新调用踪迹分配内存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;returntrace;}代码注释:
CallTraceStorage:管理所有唯一调用栈踪迹的存储。calcHash:根据调用栈中所有帧的方法ID和行号(BCI)计算一个哈希值,用于快速查找。storeCallTrace:存储一个新的调用栈踪迹。- 首先通过
findCallTrace在哈希表中查找是否已存在相同栈。 - 如果不存在,使用
LinearAllocator(_allocator) 分配内存。这是一个高性能的自定义分配器,用于快速分配小对象。 - 如果分配失败(或哈希表扩容失败),递增溢出计数器并返回一个预定义的“溢出”踪迹。
- 首先通过
LongHashTable:一个两级哈希表结构,支持并发插入和动态扩容。incSize():原子地增加哈希表大小并返回新条目的索引。setTrace(trace):使用原子存储将踪迹指针设置到哈希表的值槽中,确保多线程环境下的内存可见性。- 该设计实现了去重:相同的调用栈只在内存中存储一次,后续采样只增加该栈对应的计数器,极大节省了内存空间。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)