news 2026/2/9 14:20:16

Async-profiler:低开销Java性能分析利器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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+

构建步骤

  1. 确保gccg++javaPATH环境变量中。
  2. 导航到async-profiler源码根目录。
  3. 运行make命令。构建完成后,启动器asprof将位于build/bin/asprof目录下。
  4. (可选)运行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_tlabTrap对象,代表在JVM的AllocTracer::send_allocation_in_new_tlabsend_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智能小助手)
    对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/4 8:30:43

Java面试必看:Daemon线程的秘密你必须知道

文章目录Java面试必看&#xff1a;Daemon线程的秘密你必须知道&#xff1f;什么是Daemon线程&#xff1f;为什么需要Daemon线程&#xff1f;Daemon线程的核心特性1. Daemon线程是“后台”线程2. Daemon线程不会阻止JVM退出3. 不能将主线程设置为Daemon4. 设置Daemon属性必须在线…

作者头像 李华
网站建设 2026/2/7 14:35:01

SysDVR 终极指南:轻松实现 Switch 无线投屏到电脑

SysDVR 终极指南&#xff1a;轻松实现 Switch 无线投屏到电脑 【免费下载链接】SysDVR Stream switch games to your PC via USB or network 项目地址: https://gitcode.com/gh_mirrors/sy/SysDVR 想要将 Nintendo Switch 游戏画面实时传输到电脑屏幕吗&#xff1f;SysD…

作者头像 李华
网站建设 2026/2/5 3:20:48

大学生必备9款AI论文工具:图灵助手含真实文献AIGC率仅6%!

如果你是正在熬夜赶稿的学术人&#xff1a;9款AI论文工具测评&#xff0c;图灵助手6%AIGC率帮你稳过&#xff01; 凌晨三点的宿舍&#xff0c;电脑屏幕亮着未完成的论文初稿&#xff0c;导师催稿信息刷爆微信&#xff0c;知网查重一次花掉半个月饭钱&#xff0c;更怕AI检测率过…

作者头像 李华
网站建设 2026/2/8 4:17:27

预警延迟频发?深度剖析气象Agent阈值设定中的3个致命误区

第一章&#xff1a;气象灾害Agent预警阈值的核心挑战在构建基于智能Agent的气象灾害预警系统时&#xff0c;设定合理的预警阈值是决定系统响应准确性和及时性的关键。然而&#xff0c;实际应用中面临多重技术与环境层面的挑战。动态环境下的数据不确定性 气象数据具有高度时空变…

作者头像 李华