ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

async-profiler 采样模式全解析:从 CPU 到分配、锁与原生内存泄漏

async-profiler 采样模式全解析:从 CPU 到分配、锁与原生内存泄漏 async-profiler 采样模式全解析从 CPU 到分配、锁与原生内存泄漏【免费下载链接】async-profilerSampling CPU and HEAP profiler for Java featuring AsyncGetCallTrace perf_events项目地址: https://gitcode.com/GitHub_Trending/as/async-profilerasync-profiler 的核心能力远不止 CPU 采样。本文以仓库文档 docs/ProfilingModes.md 为骨架系统梳理其全部采样模式——CPU、Allocation、Wall Clock、Java Method、Lock、Native Lock、Native Memory以及 Multiple Events 多事件联合采样与 Continuous Profiling 持续剖析并结合仓库 C 源码src/allocTracer.cpp、src/mallocTracer.cpp、src/lockTracer.cpp、src/arguments.cpp等解释各模式底层实现。读完本文你将能针对不同性能问题选择正确的采样事件并熟练组合asprof/jfrconv参数完成从采集到报告生成的全流程。CPU 采样perf_events 与 AsyncGetCallTrace 的协作CPU 模式下剖析器采集的堆栈样本同时覆盖Java 方法、native 调用、JVM 代码与内核函数。其总体思路是接收perf_events产生的调用栈并与AsyncGetCallTrace生成的调用栈进行匹配从而同时得到 Java 与 native 代码的精确 profile。此外async-profiler 还针对AsyncGetCallTrace在某些边界场景下失效的问题OpenJDK 已知问题 JDK-8178287提供了恢复栈的 workaround。相比「直接使用perf_events Java agent 将地址翻译为方法名」的传统方案该方案有四点优势无需-XX:PreserveFramePointer——该参数会带来有时高达 10% 的性能开销无需在启动 JVM 时挂载 agent来翻译 Java 代码地址能显示解释器帧interpreter frames不产生大量中间文件如 perf.data无需在用户态脚本中二次处理。如需解析libjvm内部的栈帧还需要安装调试符号详见下文「Allocation 采样」一节的安装说明。更多 CPU 采样引擎的选型如 itimer、perf_events 等可参考 docs/CpuSamplingEngines.md。Allocation 采样基于 TLAB 驱动的堆分配剖析Allocation 模式用于采集堆内存分配量最大的调用点。async-profiler 不使用字节码插桩或 DTrace probe 这类侵入性强、性能影响大的技术也不影响 Escape Analysis更不会阻碍 JIT 的分配消除allocation elimination等优化——只有真正发生的堆分配才会被计量。该模式依赖 HotSpot 特有的回调接收两类通知对象在**新建 TLABThread-Local Allocation Buffer**中分配对象在TLAB 之外慢路径如大对象直接分配分配。采样间隔可用--alloc选项调整。例如--alloc 500k表示平均每分配 500 KB 取一个样本。JDK 11 之前小于 TLAB 大小的间隔不会生效因为小于 TLAB 的分配不会触发回调。源码层面src/allocTracer.cpp 通过 trap 机制捕获分配点AllocTracer::trapHandler判断 PC 是否落在_in_new_tlab或_outside_tlab两个断点区域内据此区分ALLOC_SAMPLETLAB 内与ALLOC_OUTSIDE_TLABTLAB 外事件并通过updateCounter(_allocated_bytes, total_size, _interval)实现按字节数的周期采样见 src/allocTracer.cpp。采样间隔_interval由args._alloc换算而来src/allocTracer.cpp。在 allocation 模式下每个调用栈的栈顶帧是所分配对象的类计数器值表示堆压力新建 TLAB 或 TLAB 外对象的总大小。配合--live选项可以只统计存活对象见 src/main/main.cpp 的用法说明相关测试可参考 test/test/alloc/AllocTests.java。安装调试符号JDK 11 之前分配剖析器需要 HotSpot 调试符号。部分 OpenJDK 发行版Amazon Corretto、Liberica JDK、Azul Zulu已将其内嵌于libjvm.so其他 OpenJDK 构建通常以独立包形式提供调试符号。安装示例Debian / Ubuntu把17换成所需 JDK 版本# apt install openjdk-17-dbgCentOS、RHEL 等 RPM 系发行版可用debuginfo-install工具# debuginfo-install java-1.8.0-openjdkGentoo 上可为icedteaOpenJDK 包设置 per-package 选项FEATURESnostrip以保留符号。安装是否成功可用gdb验证libjvm的符号$ gdb $JAVA_HOME/lib/server/libjvm.so -ex info address UseG1GC输出若包含Symbol UseG1GC is at 0xxxxx则说明符号已就绪若为No symbol UseG1GC in current context则尚未生效。原生内存泄漏剖析nativememnativemem模式记录带地址的malloc、realloc、calloc与free调用使分配与释放可以相互匹配从而让报告只聚焦于未释放的分配——这正是内存泄漏的根源。基本用法asprof start -e nativemem -f app.jfr YourApp # 或 asprof start --nativemem N -f app.jfr YourApp # 或只关心分配调用不采集 free 调用 asprof start --nativemem N --nofree -f app.jfr YourApp asprof stop YourApp随后用jfrconv处理 JFR 文件以定位泄漏# --total 按字节累计默认按调用次数统计 jfrconv --total --nativemem --leak app.jfr app-leak.html # 不做泄漏分析包含全部原生分配 jfrconv --total --nativemem app.jfr app-malloc.html使用--leak时生成的火焰图只显示没有对应free调用的分配路径为避免对「剖析结束前尚未释放的最年轻分配」产生偏置泄漏剖析器默认忽略剖析时段最后 10%的尾部分配。尾部长度可用--tail调整参数接受ratio或percent%。例如忽略 10 分钟剖析中最后 2 分钟的分配jfrconv --nativemem --leak --tail 20% app.jfr app-leak.html这些选项与jfrconv的用法对应关系可在 src/converter/one/convert/Main.java 的 usage 中确认--nativememmalloc profile、--leak仅保留泄漏、--tail RATIO默认 10%。JFR 转火焰图的完整工作流另见 docs/ConverterUsage.md。nativemem的开销取决于原生分配频率但通常足够小可承受生产环境使用。如需进一步降低开销可配置采样间隔例如添加 profiler 选项nativemem1m则分配样本被限制为平均每分配 1 MB 至多一个样本。源码实现方面src/mallocTracer.cpp 通过malloc_hook/calloc_hook/realloc_hook/free_hook四个钩子包装libc中对应的导入符号patchImport并对_nofree标志做判断——开启--nofree时realloc_hook与free_hook不再上报见 src/mallocTracer.cpp。该实现还专门处理了 musl 等 libc 上calloc内部调用malloc导致的重复计数问题detectNestedMalloc见 src/mallocTracer.cpp。用 LD_PRELOAD 剖析非 Java 进程的原生内存泄漏与 Java 应用类似nativemem也适用于非 Java 进程详见 docs/ProfilingNonJavaApplications.md。以下命令以LD_PRELOAD方式启动应用每 10 分钟输出一个 JFR 记录LD_PRELOAD/path/to/libasyncProfiler.so ASPROF_COMMANDstart,nativemem,total,loop10m,cstackdwarf,fileprofile-%t.jfr NativeApp [args]再用jfrconv生成泄漏火焰图jfrconv --total --nativemem --leak profile.jfr profile-leak.htmlWall-clock 采样等间隔采样所有线程-e wall让剖析器每隔固定周期对所有线程等概率采样无论线程处于 Running、Sleeping 还是 Blocked 状态。例如剖析应用启动耗时场景就非常合适。Wall-clock 剖析在按线程模式-t下最为有用asprof -e wall -t -i 50ms -f result.html 8983源码中 src/wallClock.cpp 展示了实现细节_interval默认取args._wall未指定时若为纯 wall 模式会放大为DEFAULT_INTERVAL * 5因为采样线程数更多并对线程遍历间隔设有 100 微秒的硬下限以避免过高的开销src/wallClock.cpp。它同时兼容两类事件WALL_CLOCK_SAMPLE与EXECUTION_SAMPLE后者在 CPU_ONLY 模式下采样。Java 锁剖析lock-e lock用于测量被剖析应用中的锁竞争帮助开发者理解锁获取模式、竞争程度线程等待获取锁的时长、等待锁的时间以及哪些代码路径因锁而阻塞。在 lock 模式下栈顶帧是锁/监视器monitor的类计数器值为进入该锁/监视器所耗费的纳秒数。asprof -e lock -t -i 5ms -f result.html 8983底层实现见 src/lockTracer.cpp_interval由args._lock按 TSC 频率换算为纳秒src/lockTracer.cpp通过_total_duration累加器溢出判定是否采样src/lockTracer.cpp。JFR 输出中对应jdk.JavaMonitorEnter与jdk.ThreadPark两类事件。原生锁剖析nativelock--nativelock用于测量被剖析应用中的pthread 锁竞争帮助理解 pthread 锁获取模式、竞争程度、等待 pthread mutex / 读写锁的时间以及被原生同步原语阻塞的代码路径。原生锁剖析通过拦截以下调用实现pthread_mutex_lockpthread_rwlock_rdlockpthread_rwlock_wrlock该模式下栈顶帧是经历竞争的原生函数例如pthread_mutex_lock_hook计数器表示线程等待获取锁的纳秒数。与 Java 锁剖析的关键区别剖析的是原生 pthread 锁而非 Java monitor适用于C/C 应用以及 Java 应用使用的原生库能捕获 Java 锁剖析看不到的原生代码路径竞争。asprof --nativelock 5ms -t -f result.html 8983实现上src/nativeLockTracer.cpp 与 Java 锁追踪器并列参数解析见 src/arguments.cpp未指定阈值时使用DEFAULT_LOCK_INTERVALsrc/arguments.cpp。相关测试见 test/native/nativeLockTest.cpp 与 test/test/nativelock/NativelockTests.java。Java 方法剖析与原生函数剖析-e ClassName.methodName会对指定 Java 方法做插桩记录该方法的所有调用点及其堆栈-e java.util.Properties.getProperty上面的例子会剖析所有调用getProperty的位置。注意仅支持非 native的 Java 方法要剖析 native 方法应改用硬件断点事件例如-e Java_java_lang_Throwable_fillInStackTrace运行时 attach时对某个非 native Java 方法的首次插桩可能触发全部已编译方法的 deoptimization与jvmtiRedefineClasses中的相关逻辑一致后续插桩只冲刷 dependent code。以 agent 方式 attach 则不会发生大规模 CodeCache 冲刷。从源码看-e ClassName.methodName与--trace带延迟阈值的插桩剖析在参数类别中被统一归类src/arguments.cpp 中事件类别包含trace实现位于 src/instrument.cpp测试见 test/test/instrument/InstrumentTests.java。除 Java 方法外以下原生函数也常被用于剖析特定行为G1CollectedHeap::humongous_obj_allocate——追踪 G1 GC 的humongous 分配大对象JVM_StartThread——追踪新 Java 线程的创建Java_java_lang_ClassLoader_defineClass1——追踪类加载。多事件联合采样Multiple Eventsasync-profiler 可以同时剖析 CPU、分配与锁。除 CPU 外也可替换为 wall-clock、perf event、tracepoint、Java method 等任何执行事件。唯一支持多事件并存输出的格式是JFR其中包含的事件类型jdk.ExecutionSampleCPUjdk.ObjectAllocationInNewTLABallocjdk.ObjectAllocationOutsideTLABallocjdk.JavaMonitorEnterlockjdk.ThreadParklock同时开启 cpu alloc lockasprof -e cpu,alloc,lock -f profile.jfr ...或用--alloc与--lock指定各自的阈值asprof -e cpu --alloc 2m --lock 10ms -f profile.jfr ...以 agent 方式启动时等价写法-agentpath:/path/to/libasyncProfiler.sostart,eventcpu,alloc2m,lock10ms,fileprofile.jfr用--all一键开启常用事件集--all标志可同时启用一组预定义的常用事件cpu、wall、alloc、live、lock、nativemem。重要提醒--all适合开发环境获取全局概览不建议在生产环境尤其持续剖析场景开启应谨慎选择要剖析的事件及其参数。asprof --all -f profile.jfr也可以叠加--alloc/--wall/--lock/--nativemem覆盖单个事件的设置asprof --all --alloc 2m --lock 10ms -f profile.jfragent 方式等价写法-agentpath:/path/to/libasyncProfiler.sostart,all,alloc2m,lock10ms,fileprofile.jfr此外可用任意事件类型替换--all中的cpu。例如下面的命令剖析cycles加上wall、alloc、live、lock、nativememasprof --all -e cycles -f profile.jfr源码层面src/main/main.cpp 的 usage 明确--all是同时启用 cpu、wall、alloc、live、nativemem 与 lock 的简写src/arguments.cpp 在解析--all时将其展开为对应事件的默认阈值_alloc、_lock、_nativemem等随后在--alloc/--lock/--nativemem显式覆盖时生效。持续剖析Continuous profiling持续剖析是指应用被持续剖析、并每隔指定周期转储一次剖析结果。它能主动、高效地发现性能退化帮助理解同一应用不同版本间的性能差异将最新输出与历史输出对比定位退化并优化引入的变更。async-profiler 通过loop选项实现持续剖析。务必在文件名中包含时间戳模式否则每次迭代都会覆盖输出asprof --loop 1h -f /var/log/profile-%t.jfr 8983--loop的时间参数与-d等时长参数共用一套单位解析parseUnits%t在每次循环时展开为时间戳。Linux 上支持的 perf 事件类型在 Linux 上-e选项支持丰富的硬件 / 软件事件。下表整理了文档 docs/ProfilingModes.md 列出的全部事件类型用法说明预定义事件-e cpu-clock高精度 per-CPU 定时器。与-e cpu类似但强制走 perf_events-e page-faults软件缺页page faults-e context-switches上下文切换-e cycles总 CPU 周期数-e ref-cyclesCPU 参考周期数不受频率缩放影响-e instructions退休retired的 CPU 指令数-e cache-references缓存访问次数通常为 Last Level Cache视架构而定-e cache-misses需要从更高层缓存或主存取数的缓存访问次数-e branch-instructions退休的分支指令数-e branch-misses预测错误的分支指令数-e bus-cycles总线周期数-e L1-dcache-load-missesL1 数据缓存缺失次数-e LLC-load-misses末级缓存LLC缺失次数-e dTLB-load-missesTLBTranslation Lookaside Buffer数据装载缺失次数断点-e mem:addr在十进制或十六进制0x地址上设断点-e mem:func在公开或私有符号上设断点-e mem:func[offset][/len][:rwx]在带偏移、长度与读/写/执行权限的符号或地址上设断点。地址、偏移与长度可为十六进制或十进制mem事件格式与perf-record相同-e symbol等价于在符号上设执行断点mem:symbol:x。示例-e strcmp追踪所有strcmp原生调用Tracepoint-e trace:id指定数值 id 的内核 tracepoint-e tracepoint指定名称的内核 tracepoint。示例-e syscalls:sys_enter_open追踪所有open系统调用探针-e kprobe:func[offset]内核探针。示例-e kprobe:do_sys_open-e kretprobe:func[offset]内核返回探针。示例-e kretprobe:do_sys_open-e uprobe:func[offset]用户态探针。示例-e uprobe:/usr/lib64/libc-2.17.so0x114790-e uretprobe:func[offset]用户态返回探针PMU-e rNNN指定编号的架构相关 PMU 事件。示例-e r4d2选择MEM_LOAD_L3_HIT_RETIRED.XSNP_HITMevent 0xd2, umask 0x4-e pmu descriptorPMU 事件描述符。示例-e cpu/cache-misses/、-e cpu/event0xd2,umask4/同样语法可用于 uncore 与厂商特定事件如amd_l3/event0x01,umask0x80/小结与选型建议综合上述各模式可按问题类型快速选型定位热点用-e cpu定位堆分配热点用-e alloc或--live看存活对象启动耗时与线程阻塞用-e wall -tJava 锁竞争用-e lock原生锁竞争用--nativelock原生内存泄漏用-e nativemem配合jfrconv --leak --total。需要全景概览时用--all注意生产环境慎用需要长期观察性能退化时用--loop 时间戳文件名。所有命令的完整参数语义可在 src/main/main.cpp 的 usage 字符串中核对事件类别编号cpu/alloc/lock/wall/nativemem/nativelock/trace/span定义于 src/arguments.cpp。更多选项细节还可查阅 docs/ProfilerOptions.md 与 docs/GettingStarted.md。【免费下载链接】async-profilerSampling CPU and HEAP profiler for Java featuring AsyncGetCallTrace perf_events项目地址: https://gitcode.com/GitHub_Trending/as/async-profiler创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表