|
|
<!doctypehtml><html lang=zh-CN><meta charset=UTF-8><meta content=width=device-width name=viewport><meta media="(prefers-color-scheme: light)" content=#222 name=theme-color><meta media="(prefers-color-scheme: dark)" content=#222 name=theme-color><meta content="Hexo 7.3.0" name=generator><link href=https://cdnjs.cloudflare.com rel=preconnect><link href=/images/apple-touch-icon-next.png rel=apple-touch-icon sizes=180x180><link href=/images/favicon-32x32-next.png rel=icon sizes=32x32 type=image/png><link href=/images/favicon-16x16-next.png rel=icon sizes=16x16 type=image/png><link color=#222 href=/images/logo.svg rel=mask-icon><meta content=2X6S9P7CAjXjVvw8YyQR8pCu-B0oEu7O9quLgxXuWyA name=google-site-verification><meta content=dV8JGNzi0c name=baidu-site-verification><script>!function(){var t=!1;function e(){var e;t||(t=!0,(e=document.createElement("script")).setAttribute("data-ad-client","ca-pub-7480618470784058"),e.async=!0,e.src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js",document.head.appendChild(e))}function a(){"requestIdleCallback"in window?window.requestIdleCallback(e,{timeout:5e3}):window.setTimeout(e,5e3)}"complete"===document.readyState?a():window.addEventListener("load",a,{once:!0})}()</script><link href=/css/main.css rel=stylesheet><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.0/css/all.min.css integrity=sha256-VHqXKFhhMxcpubYf9xiWdCiojEbY9NexQ4jh8AxbvcM= rel=stylesheet><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/fancyapps-ui/6.1.14/fancybox/fancybox.css integrity=sha256-mdaMxrItEW6hMIVJJNPKuX0B7KCSJQVEv3mVY4N2we4= rel=stylesheet><script class=next-config data-name=main type=application/json>{"hostname":"nicksxs.me","root":"/","images":"/images","scheme":"Pisces","darkmode":true,"version":"8.29.0","exturl":false,"sidebar":{"position":"left","width_expanded":320,"width_dual_column":240,"display":"post","padding":18,"offset":12},"hljswrap":true,"codeblock":{"theme":{"light":"default","dark":"stackoverflow-dark"},"prism":{"light":"prism","dark":"prism-dark"},"copy_button":{"enable":true,"style":"default"},"fold":{"enable":false,"height":500},"language":false},"bookmark":{"enable":false,"color":"#222","save":"auto"},"mediumzoom":false,"pangu":false,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":true,"nav":null},"stickytabs":false,"motion":{"enable":false,"async":false,"duration":200,"transition":{"menu_item":"fadeInDown","post_block":"fadeIn","post_header":"fadeInDown","post_body":"fadeInDown","coll_header":"fadeInLeft","sidebar":"fadeInUp"}},"prism":false,"i18n":{"placeholder":"搜索...","empty":"没有找到任何搜索结果:${query}","hits_time":"找到 ${hits} 个搜索结果(用时 ${time} 毫秒)","hits":"找到 ${hits} 个搜索结果"},"algolia":{"appID":"663Q99OQQD","apiKey":"e461a1371d12cec1491c1153b288a9ed","indexName":"nicksxs","hits":{"per_page":10}}}</script><script defer src=/js/config.js></script><meta content="之前写过几篇 Java Agent 和 Javassist,也单独介绍过 Arthas 的 vmtool。这些内容放在一起看,会发现它们其实一直围绕着同一个问题:" name=description><meta content=article property=og:type><meta content=从ASM到ByteKit再到Arthas:一条Java字节码增强链路 property=og:title><meta content=https://nicksxs.me/2026/08/30/%E4%BB%8EASM%E5%88%B0ByteKit%E5%86%8D%E5%88%B0Arthas-%E4%B8%80%E6%9D%A1Java%E5%AD%97%E8%8A%82%E7%A0%81%E5%A2%9E%E5%BC%BA%E9%93%BE%E8%B7%AF/index.html property=og:url><meta content="Nicksxs's Blog" property=og:site_name><meta content="之前写过几篇 Java Agent 和 Javassist,也单独介绍过 Arthas 的 vmtool。这些内容放在一起看,会发现它们其实一直围绕着同一个问题:" property=og:description><meta content=zh_CN property=og:locale><meta content=https://nicksxs.me/uploads/asm-bytekit-arthas/tool-stack.svg property=og:image><meta content=2026-08-30T07:10:00.000Z property=article:published_time><meta content=2026-08-30T07:10:00.000Z property=article:modified_time><meta content=Nicksxs property=article:author><meta content=Java property=article:tag><meta content=ASM property=article:tag><meta content=ByteKit property=article:tag><meta content=Arthas property=article:tag><meta content=字节码 property=article:tag><meta content="Java Agent" property=article:tag><meta content=summary name=twitter:card><meta content=https://nicksxs.me/uploads/asm-bytekit-arthas/tool-stack.svg name=twitter:image><link href=https://nicksxs.me/2026/08/30/%E4%BB%8EASM%E5%88%B0ByteKit%E5%86%8D%E5%88%B0Arthas-%E4%B8%80%E6%9D%A1Java%E5%AD%97%E8%8A%82%E7%A0%81%E5%A2%9E%E5%BC%BA%E9%93%BE%E8%B7%AF/ rel=canonical><script class=next-config data-name=page type=application/json>{"sidebar":"","isHome":false,"isPost":true,"lang":"zh-CN","comments":true,"permalink":"https://nicksxs.me/2026/08/30/%E4%BB%8EASM%E5%88%B0ByteKit%E5%86%8D%E5%88%B0Arthas-%E4%B8%80%E6%9D%A1Java%E5%AD%97%E8%8A%82%E7%A0%81%E5%A2%9E%E5%BC%BA%E9%93%BE%E8%B7%AF/","path":"2026/08/30/从ASM到ByteKit再到Arthas-一条Java字节码增强链路/","title":"从ASM到ByteKit再到Arthas:一条Java字节码增强链路"}</script><script class=next-config data-name=calendar type=application/json>""</script><title>从ASM到ByteKit再到Arthas:一条Java字节码增强链路 | Nicksxs's Blog</title><script async src=https://www.googletagmanager.com/gtag/js?id=UA-61358619-1></script><script class=next-config data-name=google_analytics type=application/json>{"tracking_id":"UA-61358619-1","only_pageview":false,"measure_protocol_api_secret":null}</script><script defer src=/js/third-party/analytics/google-analytics.js></script><script defer src=/js/third-party/analytics/baidu-analytics.js></script><script async src=https://hm.baidu.com/hm.js?20f33b3c0c0eff9b1522999c0015646d></script><script crossorigin=anonymous defer integrity=sha256-QnJV4HT5r8i89ovZGRD86iynZqHerEbT2xt69vDM36I= src=https://cdnjs.cloudflare.com/ajax/libs/fancyapps-ui/6.1.14/fancybox/fancybox.umd.js></script><script defer src=/js/utils.js></script><script defer src=/js/sidebar.js></script><script defer src=/js/next-boot.js></script><script data-algolia-integrity=sha256-o49zoHLDPBm9WLg6AiliAVi3axCRTqe3ltTxLvhzAVg= data-algolia-src=https://cdnjs.cloudflare.com/ajax/libs/algoliasearch/5.36.0/lite/builds/browser.umd.js data-search-src=/js/third-party/search/algolia-search.js src=/js/third-party/search/algolia-search-loader.js></script><script defer src=/js/third-party/fancybox.js></script><noscript><link href=/css/noscript.css rel=stylesheet></noscript><link title="Nicksxs's Blog" href=/atom.xml rel=alternate type=application/atom+xml><body itemscope itemtype=http://schema.org/WebPage><div class=headband></div><main class=main><div class=column><header class=header itemscope itemtype=http://schema.org/WPHeader><div class=site-brand-container><div class=site-nav-toggle><div aria-label=切换导航栏 class=toggle role=button><span class=toggle-line></span><span class=toggle-line></span><span class=toggle-line></span></div></div><div class=site-meta><a class=brand href=/ rel=start><i class=logo-line></i><p class=site-title>Nicksxs's Blog</p><i class=logo-line></i></a><p class=site-subtitle itemprop=description>Java 运行时、生产诊断与 AI 工程实践</div><div class=site-nav-right><div class="toggle popup-trigger" aria-label=搜索 role=button><i class="fa fa-search fa-fw fa-lg"></i></div></div></div><nav class=site-nav><ul class="main-menu menu"><li class="menu-item menu-item-home"><a href=/ rel=section><i class="fa fa-home fa-fw"></i>首页</a><li class="menu-item menu-item-专题"><a href=/topics/ rel=section><i class="fa fa-layer-group fa-fw"></i>专题</a><li class="menu-item menu-item-工具"><a href=/tools/ rel=section><i class="fa fa-toolbox fa-fw"></i>工具</a><li class="menu-item menu-item-about"><a href=/about/ rel=section><i class="fa fa-user fa-fw"></i>关于我</a><li class="menu-item menu-item-tags"><a href=/tags/ rel=section><i class="fa fa-tags fa-fw"></i>标签</a><li class="menu-item menu-item-categories"><a href=/categories/ rel=section><i class="fa fa-th fa-fw"></i>分类</a><li class="menu-item menu-item-archives"><a href=/archives/ rel=section><i class="fa fa-archive fa-fw"></i>归档</a><li class="menu-item menu-item-search"><a class=popup-trigger role=button><i class="fa fa-search fa-fw"></i>搜索</a></ul></nav><div class=search-pop-overlay><div class="popup search-popup"><div class=search-header><span class=search-icon><i class="fa fa-search"></i></span><div class=search-input-container><input autocapitalize=off autocomplete=off class=search-input maxlength=80 placeholder=搜索... spellcheck=false type=search></div><span class=popup-btn-close role=button><i class="fa fa-times-circle"></i></span></div><div class=search-result-container><div class=search-result-icon><i class="fab fa-algolia fa-5x"></i></div></div></div></div></header><aside class=sidebar><div class="sidebar-inner sidebar-nav-active sidebar-toc-active"><ul class=sidebar-nav><li class=sidebar-nav-toc>文章目录<li class=sidebar-nav-overview>站点概览</ul><div class=sidebar-panel-container><div class="post-toc-wrap sidebar-panel"><div class="post-toc animated"><ol class=nav><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%85%88%E4%BB%8E-class-%E6%98%AF%E6%80%8E%E6%A0%B7%E8%A2%AB%E6%94%B9%E6%8E%89%E7%9A%84%E8%AF%B4%E8%B5%B7><span class=nav-number>1.</span> <span class=nav-text>先从 class 是怎样被改掉的说起</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#premain-%E5%92%8C-agentmain-%E5%8F%88%E6%98%AF%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB><span class=nav-number>2.</span> <span class=nav-text>premain 和 agentmain 又是什么关系</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E7%AC%AC%E4%B8%80%E5%B1%82%EF%BC%9A%E7%9B%B4%E6%8E%A5%E4%BD%BF%E7%94%A8-ASM-%E6%94%B9%E6%96%B9%E6%B3%95><span class=nav-number>3.</span> <span class=nav-text>第一层:直接使用 ASM 改方法</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%96%B9%E6%B3%95%E5%90%8D%E8%BF%98%E4%B8%8D%E5%A4%9F><span class=nav-number>3.1.</span> <span class=nav-text>为什么方法名还不够</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#ClassReader%E3%80%81Visitor-%E5%92%8C-ClassWriter><span class=nav-number>3.2.</span> <span class=nav-text>ClassReader、Visitor 和 ClassWriter</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%8F%92%E4%B8%80%E8%A1%8C%E6%97%A5%E5%BF%97%E4%B9%9F%E4%BC%9A%E7%89%B5%E6%B6%89%E6%93%8D%E4%BD%9C%E6%95%B0%E6%A0%88><span class=nav-number>3.3.</span> <span class=nav-text>为什么插一行日志也会牵涉操作数栈</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E5%9C%A8%E6%96%B9%E6%B3%95%E5%85%A5%E5%8F%A3%E8%AE%B0%E5%BD%95%E5%8F%82%E6%95%B0%E5%92%8C%E5%BC%80%E5%A7%8B%E6%97%B6%E9%97%B4><span class=nav-number>3.4.</span> <span class=nav-text>在方法入口记录参数和开始时间</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E4%B8%80%E4%B8%AA%E6%96%B9%E6%B3%95%E4%B8%8D%E4%B8%80%E5%AE%9A%E5%8F%AA%E6%9C%89%E4%B8%80%E4%B8%AA%E5%87%BA%E5%8F%A3><span class=nav-number>3.5.</span> <span class=nav-text>一个方法不一定只有一个出口</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#COMPUTE-MAXS-%E5%92%8C-COMPUTE-FRAMES-%E5%9C%A8%E7%AE%97%E4%BB%80%E4%B9%88><span class=nav-number>3.6.</span> <span class=nav-text>COMPUTE_MAXS 和 COMPUTE_FRAMES 在算什么</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E8%BF%90%E8%A1%8C-ASM-%E7%A4%BA%E4%BE%8B><span class=nav-number>4.</span> <span class=nav-text>运行 ASM 示例</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E7%AC%AC%E4%BA%8C%E5%B1%82%EF%BC%9AByteKit-%E6%8A%8A%E5%AD%97%E8%8A%82%E7%A0%81%E6%93%8D%E4%BD%9C%E5%8F%98%E6%88%90%E8%AF%8A%E6%96%AD%E8%AF%AD%E4%B9%89><span class=nav-number>5.</span> <span class=nav-text>第二层:ByteKit 把字节码操作变成诊断语义</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#%E6%B3%A8%E5%85%A5%E7%82%B9%E8%A7%A3%E5%86%B3%E2%80%9C%E5%9C%A8%E5%93%AA%E9%87%8C%E6%89%A7%E8%A1%8C%E2%80%9D><span class=nav-number>5.1.</span> <span class=nav-text>注入点解决“在哪里执行”</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#Binding-%E8%A7%A3%E5%86%B3%E2%80%9C%E5%9C%A8%E8%BF%99%E9%87%8C%E8%83%BD%E6%8B%BF%E5%88%B0%E4%BB%80%E4%B9%88%E2%80%9D><span class=nav-number>5.2.</span> <span class=nav-text>Binding 解决“在这里能拿到什么”</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#ByteKit-%E4%B8%8D%E6%98%AF%E9%80%9A%E8%BF%87%E5%8F%8D%E5%B0%84%E2%80%9C%E7%9B%91%E5%90%AC%E2%80%9D%E6%96%B9%E6%B3%95><span class=nav-number>5.3.</span> <span class=nav-text>ByteKit 不是通过反射“监听”方法</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#inline-%E5%88%B0%E5%BA%95%E5%86%85%E8%81%94%E4%BA%86%E4%BB%80%E4%B9%88><span class=nav-number>5.4.</span> <span class=nav-text>inline 到底内联了什么</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E4%B8%BA%E4%BB%80%E4%B9%88-ByteKit-%E6%9C%89%E9%98%B2%E9%87%8D%E5%A4%8D%E5%A2%9E%E5%BC%BA><span class=nav-number>5.5.</span> <span class=nav-text>为什么 ByteKit 有防重复增强</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E8%BF%90%E8%A1%8C-ByteKit-%E7%A4%BA%E4%BE%8B><span class=nav-number>6.</span> <span class=nav-text>运行 ByteKit 示例</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E7%AC%AC%E4%B8%89%E5%B1%82%EF%BC%9AArthas-%E6%8A%8A%E5%8A%A8%E6%80%81%E5%A2%9E%E5%BC%BA%E5%8F%98%E6%88%90%E6%8E%92%E9%9A%9C%E5%91%BD%E4%BB%A4><span class=nav-number>7.</span> <span class=nav-text>第三层:Arthas 把动态增强变成排障命令</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#%E7%AC%AC%E4%B8%80%E6%AD%A5%E5%85%88%E7%A1%AE%E8%AE%A4%E7%B1%BB%E5%92%8C-ClassLoader><span class=nav-number>7.1.</span> <span class=nav-text>第一步先确认类和 ClassLoader</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#watch-%E8%A7%82%E5%AF%9F%E4%B8%80%E6%AC%A1%E6%96%B9%E6%B3%95%E7%9A%84%E8%BE%93%E5%85%A5%E5%92%8C%E8%BE%93%E5%87%BA><span class=nav-number>7.2.</span> <span class=nav-text>watch 观察一次方法的输入和输出</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#trace-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%83%BD%E7%9C%8B%E5%88%B0%E5%86%85%E9%83%A8%E8%B0%83%E7%94%A8><span class=nav-number>7.3.</span> <span class=nav-text>trace 为什么能看到内部调用</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#jad-%E7%9C%8B%E5%88%B0%E7%9A%84%E6%98%AF-JVM-%E5%BD%93%E5%89%8D%E8%AE%A4%E8%AF%86%E7%9A%84%E7%B1%BB><span class=nav-number>7.4.</span> <span class=nav-text>jad 看到的是 JVM 当前认识的类</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E8%AF%8A%E6%96%AD%E7%BB%93%E6%9D%9F%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81-reset><span class=nav-number>7.5.</span> <span class=nav-text>诊断结束为什么要 reset</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E4%BB%8E%E6%BA%90%E7%A0%81%E7%9C%8B-Arthas-%E6%98%AF%E6%80%8E%E6%A0%B7%E4%BD%BF%E7%94%A8-ByteKit-%E7%9A%84><span class=nav-number>8.</span> <span class=nav-text>从源码看 Arthas 是怎样使用 ByteKit 的</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#SpyAPI-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%94%BE%E5%9C%A8%E7%89%B9%E6%AE%8A%E4%BD%8D%E7%BD%AE><span class=nav-number>9.</span> <span class=nav-text>SpyAPI 为什么要放在特殊位置</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#ASM%E3%80%81ByteKit%E3%80%81Arthas-%E5%92%8C-Javassist-%E6%80%8E%E4%B9%88%E9%80%89><span class=nav-number>10.</span> <span class=nav-text>ASM、ByteKit、Arthas 和 Javassist 怎么选</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#retransform-%E4%B9%9F%E4%B8%8D%E6%98%AF%E4%BB%BB%E6%84%8F%E7%83%AD%E6%9B%B4%E6%96%B0><span class=nav-number>11.</span> <span class=nav-text>retransform 也不是任意热更新</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%A4%9A%E4%B8%AA-Agent-%E5%90%8C%E6%97%B6%E5%B7%A5%E4%BD%9C%E6%97%B6%E5%8F%91%E7%94%9F%E4%BB%80%E4%B9%88><span class=nav-number>12.</span> <span class=nav-text>多个 Agent 同时工作时发生什么</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%AD%97%E8%8A%82%E7%A0%81%E5%A2%9E%E5%BC%BA%E7%9A%84%E6%80%A7%E8%83%BD%E5%BC%80%E9%94%80%E5%9C%A8%E5%93%AA%E9%87%8C><span class=nav-number>13.</span> <span class=nav-text>字节码增强的性能开销在哪里</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#%E9%87%8D%E8%BD%AC%E6%8D%A2%E6%9C%AC%E8%BA%AB%E7%9A%84%E5%BC%80%E9%94%80><span class=nav-number>13.1.</span> <span class=nav-text>重转换本身的开销</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E6%AF%8F%E6%AC%A1%E8%B0%83%E7%94%A8%E6%8E%A2%E9%92%88%E7%9A%84%E5%BC%80%E9%94%80><span class=nav-number>13.2.</span> <span class=nav-text>每次调用探针的开销</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E8%BE%93%E5%87%BA%E5%92%8C%E8%A1%A8%E8%BE%BE%E5%BC%8F%E8%AE%A1%E7%AE%97%E7%9A%84%E5%BC%80%E9%94%80><span class=nav-number>13.3.</span> <span class=nav-text>输出和表达式计算的开销</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E6%8A%8A%E6%95%B4%E6%9D%A1%E9%93%BE%E8%B7%AF%E5%86%8D%E8%B5%B0%E4%B8%80%E9%81%8D><span class=nav-number>14.</span> <span class=nav-text>把整条链路再走一遍</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#%E4%BD%BF%E7%94%A8-ASM><span class=nav-number>14.1.</span> <span class=nav-text>使用 ASM</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E4%BD%BF%E7%94%A8-ByteKit><span class=nav-number>14.2.</span> <span class=nav-text>使用 ByteKit</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E4%BD%BF%E7%94%A8-Arthas><span class=nav-number>14.3.</span> <span class=nav-text>使用 Arthas</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%AE%8C%E6%95%B4%E5%B7%A5%E7%A8%8B%E6%BA%90%E7%A0%81><span class=nav-number>15.</span> <span class=nav-text>完整工程源码</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#Maven-%E9%85%8D%E7%BD%AE><span class=nav-number>15.1.</span> <span class=nav-text>Maven 配置</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E7%A4%BA%E4%BE%8B%E5%BA%94%E7%94%A8><span class=nav-number>15.2.</span> <span class=nav-text>示例应用</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#ASM-Agent><span class=nav-number>15.3.</span> <span class=nav-text>ASM Agent</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#ByteKit-Agent><span class=nav-number>15.4.</span> <span class=nav-text>ByteKit Agent</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E8%BF%90%E8%A1%8C%E8%84%9A%E6%9C%AC%E5%92%8C-Arthas-%E5%91%BD%E4%BB%A4><span class=nav-number>15.5.</span> <span class=nav-text>运行脚本和 Arthas 命令</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%AE%8C%E6%95%B4%E9%AA%8C%E8%AF%81%E6%B8%85%E5%8D%95><span class=nav-number>16.</span> <span class=nav-text>完整验证清单</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99><span class=nav-number>17.</span> <span class=nav-text>参考资料</span></a></ol></div></div><div class="site-overview-wrap sidebar-panel"><div class="site-author animated" itemprop=author itemscope itemtype=http://schema.org/Person><img alt=Nicksxs class=site-author-image itemprop=image src=/uploads/avatar.jpg><p class=site-author-name itemprop=name>Nicksxs<div class=site-description itemprop=description>Nicksxs 的技术博客,深入 Java 运行时与生产诊断、低成本大模型推理和 Agent 工程实践。</div></div><div class="site-state-wrap animated"><nav class=site-state><div class="site-state-item site-state-posts"><a href=/archives/><span class=site-state-item-count>394</span> <span class=site-state-item-name>日志</span></a></div><div class="site-state-item site-state-categories"><a href=/categories/><span class=site-state-item-count>181</span> <span class=site-state-item-name>分类</span></a></div><div class="site-state-item site-state-tags"><a href=/tags/><span class=site-state-item-count>347</span> <span class=site-state-item-name>标签</span></a></div></nav></div><div class="links-of-author animated"><span class=links-of-author-item><a rel="noopener me" title="GitHub → https://github.com/nicksxs" href=https://github.com/nicksxs target=_blank><i class="fab fa-github fa-fw"></i>GitHub</a> </span><span class=links-of-author-item><a rel="noopener me" title="E-Mail → mailto:nicksxs1202@gmail.com" href=mailto:nicksxs1202@gmail.com target=_blank><i class="fa fa-envelope fa-fw"></i>E-Mail</a></span></div><div class="cc-license animated" itemprop=license><a class=cc-opacity href=https://creativecommons.org/licenses/by-nc-sa/4.0/ rel=noopener target=_blank><img alt="Creative Commons" src=https://cdnjs.cloudflare.com/ajax/libs/creativecommons-vocabulary/2020.11.3/assets/license_badges/small/by_nc_sa.svg></a></div><div class="widget-wrap sidebar-tag-cloud"><div class=tag-cloud-tags><a class=tag-cloud-10 href=/tags/Java/ style=font-size:16px>Java</a><a class=tag-cloud-9 href=/tags/leetcode/ style=font-size:15.62px>leetcode</a><a class=tag-cloud-8 href=/tags/LLM/ style=font-size:15.23px>LLM</a><a class=tag-cloud-8 href=/tags/%E7%94%9F%E6%B4%BB/ style=font-size:14.85px>生活</a><a class=tag-cloud-7 href=/tags/%E9%A2%98%E8%A7%A3/ style=font-size:14.46px>题解</a><a class=tag-cloud-6 href=/tags/SpringBoot/ style=font-size:14.08px>SpringBoot</a><a class=tag-cloud-6 href=/tags/MySQL/ style=font-size:14.08px>MySQL</a><a class=tag-cloud-5 href=/tags/C/ style=font-size:13.69px>C++</a><a class=tag-cloud-5 href=/tags/Redis/ style=font-size:13.31px>Redis</a><a class=tag-cloud-4 href=/tags/Tomcat/ style=font-size:12.92px>Tomcat</a><a class=tag-cloud-4 href=/tags/Mybatis/ style=font-size:12.92px>Mybatis</a><a class=tag-cloud-3 href=/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ style=font-size:12.54px>小技巧</a><a class=tag-cloud-2 href=/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ style=font-size:12.15px>数据结构</a><a class=tag-cloud-2 href=/tags/%E6%BA%90%E7%A0%81/ style=font-size:12.15px>源码</a><a class=tag-cloud-2 href=/tags/MQ/ style=font-size:11.77px>MQ</a><a class=tag-cloud-2 href=/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ style=font-size:11.77px>消息队列</a><a class=tag-cloud-2 href=/tags/RocketMQ/ style=font-size:11.77px>RocketMQ</a><a class=tag-cloud-2 href=/tags/%E8%BF%90%E5%8A%A8/ style=font-size:11.77px>运动</a><a class=tag-cloud-1 href=/tags/php/ style=font-size:11.38px>php</a><a class=tag-cloud-0 href=/tags/Spring/ style=font-size:11px>Spring</a><a class=tag-cloud-0 href=/tags/linux/ style=font-size:11px>linux</a><a class=tag-cloud-0 href=/tags/Docker/ style=font-size:11px>Docker</a><a class=tag-cloud-0 href=/tags/%E5%87%8F%E8%82%A5/ style=font-size:11px>减肥</a><a class=tag-cloud-0 href=/tags/%E8%B7%91%E6%AD%A5/ style=font-size:11px>跑步</a></div></div><div class="affiliate-creative affiliate-creative--sidebar" data-affiliate-id=tencent-cloud-general><a rel="sponsored noopener" aria-label=腾讯云产品优惠 href=https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=2e33a1eedb380997d03618b4a5ba6ca7&from=console target=_blank><img alt=腾讯云服务器、数据库和云产品优惠 decoding=async height=419 loading=lazy src=/uploads/affiliate/tencent-cloud-general.webp width=690> </a><span class=affiliate-disclosure>推广</span></div></div></div></div><div class="sidebar-inner sidebar-blogroll"><div class="links-of-blogroll animated"><div class=links-of-blogroll-title><i class="fa fa-globe fa-fw"></i> 链接</div><ul class=links-of-blogroll-list><li class=links-of-blogroll-item><a href=https://covermusic.cn/ rel=noopener target=_blank title=https://covermusic.cn>69伙伴</a></ul></div></div></aside></div><div class="main-inner post posts-expand"><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article lang=zh-CN><link href=https://nicksxs.me/2026/08/30/%E4%BB%8EASM%E5%88%B0ByteKit%E5%86%8D%E5%88%B0Arthas-%E4%B8%80%E6%9D%A1Java%E5%AD%97%E8%8A%82%E7%A0%81%E5%A2%9E%E5%BC%BA%E9%93%BE%E8%B7%AF/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="Nicksxs 的技术博客,深入 Java 运行时与生产诊断、低成本大模型推理和 Agent 工程实践。" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="从ASM到ByteKit再到Arthas:一条Java字节码增强链路 | Nicksxs's Blog" itemprop=name><meta content="之前写过几篇 Java Agent 和 Javassist,也单独介绍过 Arthas 的 vmtool。这些内容放在一起看,会发现它们其实一直围绕着同一个问题:" itemprop=description></span><header class=post-header><h1 itemprop="name headline" class=post-title>从ASM到ByteKit再到Arthas:一条Java字节码增强链路</h1><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-08-30 15:10:00" datetime=2026-08-30T15:10:00+08:00>2026-08-30</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/Java/ itemprop=url rel=index><span itemprop=name>Java</span></a> </span>, <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/Java/%E6%BA%90%E7%A0%81/ itemprop=url rel=index><span itemprop=name>源码</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-increment=true data-path=/2026/08/30/%E4%BB%8EASM%E5%88%B0ByteKit%E5%86%8D%E5%88%B0Arthas-%E4%B8%80%E6%9D%A1Java%E5%AD%97%E8%8A%82%E7%A0%81%E5%A2%9E%E5%BC%BA%E9%93%BE%E8%B7%AF/ data-title=从ASM到ByteKit再到Arthas:一条Java字节码增强链路 title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span></span></div><div class=post-description>之前写过几篇 Java Agent 和 Javassist,也单独介绍过 Arthas 的 vmtool。这些内容放在一起看,会发现它们其实一直围绕着同一个问题:</div></div></header><div class=post-body itemprop=articleBody><p>之前写过几篇 Java Agent 和 Javassist,也单独介绍过 Arthas 的 <code>vmtool</code>。这些内容放在一起看,会发现它们其实一直围绕着同一个问题:<blockquote><p>不修改业务源码,能不能在一个 Java 方法执行前后,临时加上一些观察逻辑?</blockquote><p>比如有这样一个方法:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> String <span class="title function_">placeOrder</span><span class=params>(String user, <span class=type>int</span> quantity)</span> {</span><br><span class=line> <span class=keyword>if</span> (<span class=string>"error"</span>.equals(user)) {</span><br><span class=line> <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">IllegalArgumentException</span>(<span class=string>"user cannot be error"</span>);</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> validateQuantity(quantity);</span><br><span class=line> <span class=type>String</span> <span class=variable>sku</span> <span class=operator>=</span> queryInventory(user);</span><br><span class=line> <span class=keyword>return</span> saveOrder(user, sku, quantity);</span><br><span class=line>}</span><br></pre></table></figure><p>我们希望知道:<ul><li>调用时传了什么参数;<li>正常执行返回了什么;<li>抛异常时是什么异常;<li>整个方法以及内部几个调用分别耗时多久。</ul><p>如果能改源码,加几行日志当然最简单。但在线上排查时,往往既不方便改代码,也不希望为了几行日志重新发布和重启应用。一般现在会使用arthas来查看参数,返回等,不过仔细研究下,ASM、ByteKit 和 Arthas,正好可以看成从底到顶的三种解决层次。<p><img alt="ASM、ByteKit 与 Arthas 的分层关系" loading=lazy src=/uploads/asm-bytekit-arthas/tool-stack.svg><p>简单概括就是:<table><thead><tr><th>工具<th>它解决的问题<th>使用者面对的东西<tbody><tr><td>ASM<td>怎样正确读写 class 字节码<td>指令、操作数栈、局部变量、栈帧<tr><td>ByteKit<td>怎样更方便地表达诊断插桩<td><code>@AtEnter</code>、<code>@AtExit</code>、<code>@Binding</code><tr><td>Arthas<td>怎样把插桩变成可以直接使用的排障工具<td><code>watch</code>、<code>trace</code>、<code>jad</code>、<code>reset</code></table><p>这里最容易产生的误解是把它们看成三套互相竞争的技术。实际上,ByteKit 构建在 ASM 之上,而 Arthas 的字节码增强又使用了 ByteKit。它们更像是同一条链路上的不同抽象层。<p>本文会沿着这条链路,从一个可以运行的例子开始,一层层看清楚它们分别做了什么。<p>完整示例的源码会在本文末尾直接渲染,本地工程放在:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>source/code/asm-bytekit-arthas-demo</span><br></pre></table></figure><p>下面的代码块来自这个目录中的真实文件,不依赖目录页面。示例要求 JDK 8 以上和 Maven 3.6 以上,本文实际使用 JDK 8、ASM 9.9.1、ByteKit 0.1.7 和 Arthas 4.3.2 验证。<h2 id=先从-class-是怎样被改掉的说起><a title="先从 class 是怎样被改掉的说起" class=headerlink href=#先从-class-是怎样被改掉的说起></a>先从 class 是怎样被改掉的说起</h2><p>在看 ASM 的 API 之前,先把 Java 程序从源码到运行的过程简化一下:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br></pre><td class=code><pre><span class=line>DemoService.java</span><br><span class=line> |</span><br><span class=line> | javac</span><br><span class=line> v</span><br><span class=line>DemoService.class,也就是一组 byte[]</span><br><span class=line> |</span><br><span class=line> | ClassLoader.defineClass</span><br><span class=line> v</span><br><span class=line>JVM 中的 Class<?> 和方法实现</span><br><span class=line> |</span><br><span class=line> | 解释执行 / JIT 编译</span><br><span class=line> v</span><br><span class=line>机器指令</span><br></pre></table></figure><p>平时我们关注的是 <code>.java</code> 和对象,字节码工具关注的则是中间那段 <code>byte[]</code>。<p>Java 在 <code>java.lang.instrument</code> 包里提供了 <code>Instrumentation</code> 接口。Agent 可以向它注册一个 <code>ClassFileTransformer</code>:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>instrumentation.addTransformer(transformer, <span class=literal>true</span>);</span><br></pre></table></figure><p>当 JVM 加载类,或者对已加载的类执行 <code>retransformClasses()</code> 时,会调用 Transformer:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line><span class=type>byte</span>[] transform(</span><br><span class=line> ClassLoader loader,</span><br><span class=line> String className,</span><br><span class=line> Class<?> classBeingRedefined,</span><br><span class=line> ProtectionDomain protectionDomain,</span><br><span class=line> <span class=type>byte</span>[] classfileBuffer</span><br><span class=line>)</span><br></pre></table></figure><p>这里最重要的就是最后一个参数和返回值:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>JVM 给 Transformer 原始 classfileBuffer</span><br><span class=line> |</span><br><span class=line> | 修改</span><br><span class=line> v</span><br><span class=line>Transformer 返回新的 byte[]</span><br><span class=line> |</span><br><span class=line> | 校验并安装</span><br><span class=line> v</span><br><span class=line>JVM 开始执行增强后的方法</span><br></pre></table></figure><p>如果 Transformer 返回 <code>null</code>,表示这个类不修改。返回原始数组也能达到类似效果,但按照接口语义,不处理时返回 <code>null</code> 更清楚。<p>这也是为什么 ASM、Javassist、Byte Buddy 和 ByteKit 虽然 API 完全不同,最后却都能挂到 Java Agent 上:它们最终都在完成同一件事,把一份 class 字节数组转换成另一份 class 字节数组。<h2 id=premain-和-agentmain-又是什么关系><a title="premain 和 agentmain 又是什么关系" class=headerlink href=#premain-和-agentmain-又是什么关系></a>premain 和 agentmain 又是什么关系</h2><p>之前 Agent 示例里使用过 <code>premain</code>:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">premain</span><span class=params>(String agentArgs, Instrumentation instrumentation)</span> {</span><br><span class=line> instrumentation.addTransformer(<span class=keyword>new</span> <span class="title class_">AsmTimingTransformer</span>(), <span class=literal>true</span>);</span><br><span class=line>}</span><br></pre></table></figure><p>配合 JVM 参数启动:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>java -javaagent:asm-agent.jar -jar app.jar</span><br></pre></table></figure><p><code>premain</code> 在应用 <code>main</code> 方法之前执行,所以特别适合 APM、链路追踪、覆盖率等需要从启动阶段持续工作的工具。<p>但 Arthas 面对的是一个已经运行的 JVM,它使用的是另一条入口:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">agentmain</span><span class=params>(String agentArgs, Instrumentation instrumentation)</span> {</span><br><span class=line> <span class=comment>// 动态挂载之后执行</span></span><br><span class=line>}</span><br></pre></table></figure><p>两者拿到的都是 <code>Instrumentation</code>,主要区别只是进入目标 JVM 的时机:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>启动时加载:-javaagent -> premain -> main</span><br><span class=line></span><br><span class=line>运行时加载:目标 JVM 已运行 -> Attach API -> agentmain</span><br></pre></table></figure><p>所以“Java Agent”和“Arthas 动态挂载”并不是两套原理。Arthas 只是把 Agent 从启动参数加载,变成了运行时 Attach。<h2 id=第一层:直接使用-ASM-改方法><a title="第一层:直接使用 ASM 改方法" class=headerlink href=#第一层:直接使用-ASM-改方法></a>第一层:直接使用 ASM 改方法</h2><p>ASM 是一个很底层的字节码操作库。这里的“底层”并不是说它直接操作二进制位,而是说它暴露出来的概念已经非常接近 JVM class 文件:<ul><li>类名使用 <code>com/example/DemoService</code> 这种 internal name;<li>方法由名字和 descriptor 一起确定;<li>方法内容是一条条 JVM 指令;<li>我们需要关心局部变量、操作数栈和 stack map frame。</ul><h3 id=为什么方法名还不够><a class=headerlink href=#为什么方法名还不够 title=为什么方法名还不够></a>为什么方法名还不够</h3><p>Java 支持方法重载,下面两个方法名字相同:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>placeOrder(String user, <span class=type>int</span> quantity)</span><br><span class=line>placeOrder(String user)</span><br></pre></table></figure><p>字节码里需要用 descriptor 区分。本文目标方法的 descriptor 是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>(Ljava/lang/String;I)Ljava/lang/String;</span><br></pre></table></figure><p>拆开看并不复杂:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>(</span><br><span class=line> Ljava/lang/String; 第一个参数 String</span><br><span class=line> I 第二个参数 int</span><br><span class=line>)</span><br><span class=line>Ljava/lang/String; 返回值 String</span><br></pre></table></figure><p>因此 ASM Transformer 里同时匹配类名、方法名和 descriptor:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line><span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>String</span> <span class=variable>TARGET_CLASS</span> <span class=operator>=</span></span><br><span class=line> <span class=string>"com/nicksxs/bytecode/demo/app/DemoService"</span>;</span><br><span class=line><span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>String</span> <span class=variable>TARGET_METHOD</span> <span class=operator>=</span> <span class=string>"placeOrder"</span>;</span><br><span class=line><span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>String</span> <span class=variable>TARGET_DESCRIPTOR</span> <span class=operator>=</span></span><br><span class=line> <span class=string>"(Ljava/lang/String;I)Ljava/lang/String;"</span>;</span><br></pre></table></figure><p>这比只匹配方法名稳妥,否则一个类里所有重载方法都可能被增强。<h3 id=ClassReader、Visitor-和-ClassWriter><a title="ClassReader、Visitor 和 ClassWriter" class=headerlink href=#ClassReader、Visitor-和-ClassWriter></a>ClassReader、Visitor 和 ClassWriter</h3><p>ASM 最典型的处理流程是:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br></pre><td class=code><pre><span class=line><span class=type>ClassReader</span> <span class=variable>reader</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassReader</span>(classfileBuffer);</span><br><span class=line><span class=type>ClassWriter</span> <span class=variable>writer</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassWriter</span>(</span><br><span class=line> reader,</span><br><span class=line> ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS</span><br><span class=line>);</span><br><span class=line></span><br><span class=line><span class=type>ClassVisitor</span> <span class=variable>visitor</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassVisitor</span>(Opcodes.ASM9, writer) {</span><br><span class=line> <span class=meta>@Override</span></span><br><span class=line> <span class=keyword>public</span> MethodVisitor <span class="title function_">visitMethod</span><span class=params>(</span></span><br><span class=line><span class=params> <span class=type>int</span> access,</span></span><br><span class=line><span class=params> String name,</span></span><br><span class=line><span class=params> String descriptor,</span></span><br><span class=line><span class=params> String signature,</span></span><br><span class=line><span class=params> String[] exceptions)</span> {</span><br><span class=line> <span class=type>MethodVisitor</span> <span class=variable>mv</span> <span class=operator>=</span> <span class=built_in>super</span>.visitMethod(</span><br><span class=line> access, name, descriptor, signature, exceptions);</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (TARGET_METHOD.equals(name)</span><br><span class=line> && TARGET_DESCRIPTOR.equals(descriptor)) {</span><br><span class=line> <span class=keyword>return</span> <span class=keyword>new</span> <span class="title class_">TimingAdvice</span>(mv, access, name, descriptor);</span><br><span class=line> }</span><br><span class=line> <span class=keyword>return</span> mv;</span><br><span class=line> }</span><br><span class=line>};</span><br><span class=line></span><br><span class=line>reader.accept(visitor, ClassReader.EXPAND_FRAMES);</span><br><span class=line><span class=keyword>return</span> writer.toByteArray();</span><br></pre></table></figure><p>可以把它理解成一条流水线:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>原始 byte[]</span><br><span class=line> |</span><br><span class=line>ClassReader 解析 class 结构</span><br><span class=line> |</span><br><span class=line>ClassVisitor 访问类、字段和方法</span><br><span class=line> |</span><br><span class=line>MethodVisitor 在目标方法中插入指令</span><br><span class=line> |</span><br><span class=line>ClassWriter 重新生成 byte[]</span><br></pre></table></figure><p>ASM 使用 Visitor 模式的一个好处是可以边读边写,不一定要把整个 class 转成另一套庞大的对象模型。需要更方便地增删和遍历指令时,也可以使用 ASM Tree API,把类读成 <code>ClassNode</code> 和 <code>MethodNode</code>。<h3 id=为什么插一行日志也会牵涉操作数栈><a class=headerlink href=#为什么插一行日志也会牵涉操作数栈 title=为什么插一行日志也会牵涉操作数栈></a>为什么插一行日志也会牵涉操作数栈</h3><p>源码中的:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>System.out.println(<span class=string>"hello"</span>);</span><br></pre></table></figure><p>落实到字节码,大致是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>GETSTATIC java/lang/System.out : Ljava/io/PrintStream;</span><br><span class=line>LDC "hello"</span><br><span class=line>INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V</span><br></pre></table></figure><p>前两条指令把 <code>PrintStream</code> 对象和字符串压入操作数栈,第三条再把它们消费掉。顺序错了、类型错了或者某条分支上的栈高度不一致,JVM 验证类时就可能抛出 <code>VerifyError</code>。<p>本文示例使用 <code>AdviceAdapter</code>,它在 <code>MethodVisitor</code> 上又封装了一层,提供:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>onMethodEnter()</span><br><span class=line>onMethodExit(<span class=type>int</span> opcode)</span><br></pre></table></figure><p>我们仍然是在生成 JVM 指令,只是少写了一些容易出错的样板代码。<h3 id=在方法入口记录参数和开始时间><a class=headerlink href=#在方法入口记录参数和开始时间 title=在方法入口记录参数和开始时间></a>在方法入口记录参数和开始时间</h3><p>示例在入口先调用 <code>System.nanoTime()</code>,把结果放进一个新建的局部变量:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br></pre><td class=code><pre><span class=line><span class=meta>@Override</span></span><br><span class=line><span class=keyword>protected</span> <span class=keyword>void</span> <span class="title function_">onMethodEnter</span><span class=params>()</span> {</span><br><span class=line> invokeStatic(SYSTEM_TYPE, NANO_TIME);</span><br><span class=line> startNanosLocal = newLocal(Type.LONG_TYPE);</span><br><span class=line> storeLocal(startNanosLocal);</span><br><span class=line></span><br><span class=line> getStatic(SYSTEM_TYPE, <span class=string>"out"</span>, PRINT_STREAM_TYPE);</span><br><span class=line> newInstance(STRING_BUILDER_TYPE);</span><br><span class=line> dup();</span><br><span class=line> invokeConstructor(STRING_BUILDER_TYPE, STRING_BUILDER_INIT);</span><br><span class=line> push(<span class=string>"[ASM] enter placeOrder user="</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);</span><br><span class=line> loadArg(<span class=number>0</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);</span><br><span class=line> push(<span class=string>", quantity="</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);</span><br><span class=line> loadArg(<span class=number>1</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_INT);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, TO_STRING);</span><br><span class=line> invokeVirtual(PRINT_STREAM_TYPE, PRINTLN);</span><br><span class=line>}</span><br></pre></table></figure><p>这里的 <code>loadArg(0)</code> 和 <code>loadArg(1)</code>,最后会变成相应的局部变量加载指令。ASM 知道方法是不是静态方法,因此会帮我们处理 <code>this</code> 占用的第 0 个局部变量槽位。<h3 id=一个方法不一定只有一个出口><a class=headerlink href=#一个方法不一定只有一个出口 title=一个方法不一定只有一个出口></a>一个方法不一定只有一个出口</h3><p>正常返回可能使用:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>IRETURN</span><br><span class=line>LRETURN</span><br><span class=line>FRETURN</span><br><span class=line>DRETURN</span><br><span class=line>ARETURN</span><br><span class=line>RETURN</span><br></pre></table></figure><p>异常还会经过 <code>ATHROW</code>。如果直接找到最后一条指令插日志,很容易漏掉前面的提前返回和异常分支。<p><code>AdviceAdapter.onMethodExit()</code> 会在这些退出指令前回调:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br></pre><td class=code><pre><span class=line><span class=meta>@Override</span></span><br><span class=line><span class=keyword>protected</span> <span class=keyword>void</span> <span class="title function_">onMethodExit</span><span class=params>(<span class=type>int</span> opcode)</span> {</span><br><span class=line> invokeStatic(SYSTEM_TYPE, NANO_TIME);</span><br><span class=line> loadLocal(startNanosLocal);</span><br><span class=line> math(SUB, Type.LONG_TYPE);</span><br><span class=line> <span class=type>int</span> <span class=variable>elapsedLocal</span> <span class=operator>=</span> newLocal(Type.LONG_TYPE);</span><br><span class=line> storeLocal(elapsedLocal);</span><br><span class=line></span><br><span class=line> <span class=comment>// 后面生成 println("cost=" + elapsed + " ns") 的指令</span></span><br><span class=line>}</span><br></pre></table></figure><p>示例还根据 <code>opcode == ATHROW</code> 区分正常返回和显式抛出异常。<p>这里有个值得注意的细节:只在 <code>ATHROW</code> 前插代码,能捕获方法字节码里显式执行的 <code>ATHROW</code>,却不等同于给整个方法套了一层 <code>try/finally</code>。如果某个子调用抛出异常并直接向外传播,目标方法本身不一定存在一条可见的 <code>ATHROW</code> 指令。真正通用的异常退出增强,需要改写异常表,ByteKit 的 <code>@AtExceptionExit</code> 就封装了这部分处理。<h3 id=COMPUTE-MAXS-和-COMPUTE-FRAMES-在算什么><a title="COMPUTE_MAXS 和 COMPUTE_FRAMES 在算什么" class=headerlink href=#COMPUTE-MAXS-和-COMPUTE-FRAMES-在算什么></a>COMPUTE_MAXS 和 COMPUTE_FRAMES 在算什么</h3><p>每个方法在 class 文件里都需要声明:<ul><li>最大操作数栈深度;<li>最大局部变量槽位数量;<li>分支合流位置的 stack map frame。</ul><p>插入新指令和新局部变量以后,这些数据可能发生变化。示例让 <code>ClassWriter</code> 自动计算:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS</span><br></pre></table></figure><p>这非常适合入门示例,但不是说以后就完全不用理解栈帧。复杂项目里,计算 frame 时可能需要解析类型的共同父类,从而牵涉目标 ClassLoader;不恰当地加载业务类,还可能带来类初始化或类加载隔离问题。Arthas 和 ByteKit 里有不少代码,就是在处理这些工程细节。<h2 id=运行-ASM-示例><a title="运行 ASM 示例" class=headerlink href=#运行-ASM-示例></a>运行 ASM 示例</h2><p>进入示例目录并构建:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line><span class=built_in>cd</span> <span class=built_in>source</span>/code/asm-bytekit-arthas-demo</span><br><span class=line>mvn clean package</span><br></pre></table></figure><p>先运行没有增强的版本:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>./run.sh baseline 4 10</span><br></pre></table></figure><p>输出只有应用自己的日志:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>demo pid=21257</span><br><span class=line>iterations=4</span><br><span class=line>[APP] order created: user=user-1, sku=SKU-USER-1, quantity=1</span><br><span class=line>[APP] order created: user=user-2, sku=SKU-USER-2, quantity=2</span><br><span class=line>[APP] order created: user=user-3, sku=SKU-USER-3, quantity=3</span><br><span class=line>[APP] failed: user cannot be error</span><br></pre></table></figure><p>再带上 ASM Agent:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>./run.sh asm 4 10</span><br></pre></table></figure><p>可以看到业务源码没有改变,但 <code>placeOrder()</code> 前后已经出现了 Agent 插入的输出:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>[ASM agent] installed</span><br><span class=line>[ASM agent] transforming com/nicksxs/bytecode/demo/app/DemoService</span><br><span class=line>[ASM] enter placeOrder user=user-1, quantity=1</span><br><span class=line>[ASM] return placeOrder cost=114967458 ns</span><br><span class=line>[APP] order created: user=user-1, sku=SKU-USER-1, quantity=1</span><br><span class=line>...</span><br><span class=line>[ASM] enter placeOrder user=error, quantity=4</span><br><span class=line>[ASM] throw placeOrder cost=478125 ns</span><br><span class=line>[APP] failed: user cannot be error</span><br></pre></table></figure><p>这一层已经把字节码增强的基本原理跑通了。不过问题也很明显:为了获取两个参数和一个耗时,我们写了不少类型、descriptor、局部变量和指令生成代码。如果要支持任意方法的参数、返回值、异常和内部调用,代码量会迅速增加。<p>ByteKit 就是为了解决这一层的重复劳动。<h2 id=第二层:ByteKit-把字节码操作变成诊断语义><a title="第二层:ByteKit 把字节码操作变成诊断语义" class=headerlink href=#第二层:ByteKit-把字节码操作变成诊断语义></a>第二层:ByteKit 把字节码操作变成诊断语义</h2><p>ByteKit 官方对自己的定位很明确:<blockquote><p>基于 ASM 提供更高层的字节码处理能力,面向诊断和 APM 领域,而不是一套通用字节码库。</blockquote><p>所以 ByteKit 并没有取代 ASM。它做的是把诊断工具经常需要的动作整理成更容易描述的概念。<h3 id=注入点解决“在哪里执行”><a class=headerlink href=#注入点解决“在哪里执行” title=注入点解决“在哪里执行”></a>注入点解决“在哪里执行”</h3><p>常见注入点包括:<table><thead><tr><th>注解<th>对应位置<tbody><tr><td><code>@AtEnter</code><td>方法刚进入<tr><td><code>@AtExit</code><td>方法正常返回<tr><td><code>@AtExceptionExit</code><td>方法异常退出<tr><td><code>@AtInvoke</code><td>方法内部调用另一个方法<tr><td><code>@AtInvokeException</code><td>内部调用抛出异常<tr><td><code>@AtLine</code><td>指定源码行<tr><td><code>@AtFieldAccess</code><td>访问字段<tr><td><code>@AtSyncEnter</code> / <code>@AtSyncExit</code><td>进入或退出同步块</table><p>ASM 层要自己寻找 <code>RETURN</code>、<code>ATHROW</code>、<code>MethodInsnNode</code> 或行号节点;ByteKit 层可以先说清楚“我要方法退出点”或者“我要内部调用结束点”。<h3 id=Binding-解决“在这里能拿到什么”><a title="Binding 解决“在这里能拿到什么”" class=headerlink href=#Binding-解决“在这里能拿到什么”></a>Binding 解决“在这里能拿到什么”</h3><p>注入点确定以后,还需要把运行时数据交给增强逻辑:<table><thead><tr><th>Binding<th>得到的数据<tbody><tr><td><code>@Binding.This</code><td>当前对象 <code>this</code><tr><td><code>@Binding.Args</code><td>方法参数数组<tr><td><code>@Binding.Return</code><td>返回值<tr><td><code>@Binding.Throwable</code><td>抛出的异常<tr><td><code>@Binding.MethodName</code><td>方法名<tr><td><code>@Binding.MethodDesc</code><td>方法 descriptor<tr><td><code>@Binding.Field</code><td>当前对象的指定字段<tr><td><code>@Binding.InvokeArgs</code><td>被调用方法的参数<tr><td><code>@Binding.InvokeReturn</code><td>被调用方法的返回值</table><p>本文的 ByteKit 拦截器就变得非常直观:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> <span class=keyword>class</span> <span class="title class_">DemoInterceptor</span> {</span><br><span class=line></span><br><span class=line> <span class=meta>@AtEnter(inline = true)</span></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">atEnter</span><span class=params>(</span></span><br><span class=line><span class=params> <span class=meta>@Binding</span>.MethodName String methodName,</span></span><br><span class=line><span class=params> <span class=meta>@Binding</span>.Args Object[] args)</span> {</span><br><span class=line> System.out.println(<span class=string>"[ByteKit] enter "</span></span><br><span class=line> + methodName + <span class=string>" args="</span> + Arrays.toString(args));</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=meta>@AtExit(inline = true)</span></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">atExit</span><span class=params>(<span class=meta>@Binding</span>.Return Object returnObject)</span> {</span><br><span class=line> System.out.println(<span class=string>"[ByteKit] return "</span> + returnObject);</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=meta>@AtExceptionExit(inline = true, onException = Throwable.class)</span></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">atExceptionExit</span><span class=params>(</span></span><br><span class=line><span class=params> <span class=meta>@Binding</span>.Throwable Throwable throwable)</span> {</span><br><span class=line> System.out.println(<span class=string>"[ByteKit] throw "</span></span><br><span class=line> + throwable.getClass().getSimpleName()</span><br><span class=line> + <span class=string>": "</span> + throwable.getMessage());</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><p>同样是获取参数、返回值和异常,这段代码比直接写 ASM 更接近我们的真实意图。<h3 id=ByteKit-不是通过反射“监听”方法><a title="ByteKit 不是通过反射“监听”方法" class=headerlink href=#ByteKit-不是通过反射“监听”方法></a>ByteKit 不是通过反射“监听”方法</h3><p>看到这些注解,很容易把它想成 Spring AOP 那样的代理调用。实际上 ByteKit 处理的仍然是目标方法本身的字节码。<p>示例的 Transformer 核心逻辑是:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br></pre><td class=code><pre><span class=line><span class=type>DefaultInterceptorClassParser</span> <span class=variable>parser</span> <span class=operator>=</span></span><br><span class=line> <span class=keyword>new</span> <span class="title class_">DefaultInterceptorClassParser</span>();</span><br><span class=line>List<InterceptorProcessor> interceptors =</span><br><span class=line> parser.parse(DemoInterceptor.class);</span><br><span class=line></span><br><span class=line><span class=type>ClassNode</span> <span class=variable>classNode</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassNode</span>();</span><br><span class=line><span class=type>ClassReader</span> <span class=variable>reader</span> <span class=operator>=</span> AsmUtils.toClassNode(</span><br><span class=line> classfileBuffer, classNode);</span><br><span class=line></span><br><span class=line><span class=keyword>for</span> (MethodNode methodNode : classNode.methods) {</span><br><span class=line> <span class=keyword>if</span> (!<span class=string>"placeOrder"</span>.equals(methodNode.name)) {</span><br><span class=line> <span class=keyword>continue</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=type>MethodProcessor</span> <span class=variable>methodProcessor</span> <span class=operator>=</span></span><br><span class=line> <span class=keyword>new</span> <span class="title class_">MethodProcessor</span>(classNode, methodNode);</span><br><span class=line> <span class=keyword>for</span> (InterceptorProcessor interceptor : interceptors) {</span><br><span class=line> interceptor.process(methodProcessor);</span><br><span class=line> }</span><br><span class=line>}</span><br><span class=line></span><br><span class=line><span class=keyword>return</span> AsmUtils.toBytes(classNode, loader, reader);</span><br></pre></table></figure><p>它大致经历四步:<ol><li>解析拦截器上的 <code>@AtEnter</code>、<code>@AtExit</code> 和 Binding;<li>用 ASM Tree API 把原始 class 读成 <code>ClassNode</code>;<li>找到目标 <code>MethodNode</code>,由 <code>MethodProcessor</code> 插入指令;<li>再把修改后的节点树写回 <code>byte[]</code>。</ol><p>ByteKit 的价值不是绕过字节码,而是把一批成熟的字节码生成规则封装起来。<h3 id=inline-到底内联了什么><a title="inline 到底内联了什么" class=headerlink href=#inline-到底内联了什么></a>inline 到底内联了什么</h3><p>示例的注解都设置了:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>inline = <span class=literal>true</span></span><br></pre></table></figure><p>可以把它理解成:增强后并不是简单地在业务方法里调用:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>DemoInterceptor.atEnter(...);</span><br></pre></table></figure><p>而是尽量把拦截器方法里的指令复制到目标方法中。这样做可以减少一次回调调用,也降低目标 ClassLoader 必须能加载拦截器类的要求。<p>不过内联也不是免费午餐。增强逻辑太大,会让目标方法字节码膨胀;增强代码引用的其他类型,仍然需要目标类加载器能够解析。因此诊断插桩通常应该保持短小,把复杂状态管理放在稳定的桥接 API 后面。<h3 id=为什么-ByteKit-有防重复增强><a title="为什么 ByteKit 有防重复增强" class=headerlink href=#为什么-ByteKit-有防重复增强></a>为什么 ByteKit 有防重复增强</h3><p>诊断工具可能在同一个类上连续执行 <code>watch</code>、<code>trace</code> 和 <code>monitor</code>。如果每次都无条件再插一遍入口和出口代码,目标方法会越来越大,输出也会重复。<p>ByteKit 提供了位置过滤机制,可以检查某个注入点是否已经存在。Arthas 的 <code>Enhancer</code> 还会专门检查目标方法中是否已经调用 <code>SpyAPI.atEnter</code>、<code>atExit</code> 和 <code>atExceptionExit</code>,避免重复插入同类探针。<p>这是 ASM 示例和真正工程化诊断工具之间很典型的差距:基础 API 能完成一次修改,框架还要负责多次修改之间的组合、去重和恢复。<h2 id=运行-ByteKit-示例><a title="运行 ByteKit 示例" class=headerlink href=#运行-ByteKit-示例></a>运行 ByteKit 示例</h2><p>执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>./run.sh bytekit 4 10</span><br></pre></table></figure><p>本文实际验证得到:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>[ByteKit agent] installed</span><br><span class=line>[ByteKit agent] transforming com/nicksxs/bytecode/demo/app/DemoService</span><br><span class=line>[ByteKit] enter placeOrder args=[user-1, 1]</span><br><span class=line>[ByteKit] return order created: user=user-1, sku=SKU-USER-1, quantity=1</span><br><span class=line>[APP] order created: user=user-1, sku=SKU-USER-1, quantity=1</span><br><span class=line>...</span><br><span class=line>[ByteKit] enter placeOrder args=[error, 4]</span><br><span class=line>[ByteKit] throw IllegalArgumentException: user cannot be error</span><br><span class=line>[APP] failed: user cannot be error</span><br></pre></table></figure><p>到这里,我们已经用两种方式修改了同一个方法:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>ASM:自己生成每一段参数加载、时间计算和输出指令</span><br><span class=line></span><br><span class=line>ByteKit:声明注入点和需要绑定的数据,由 ByteKit 生成指令</span><br></pre></table></figure><p>但这两种方式仍然需要我们自己写 Agent、打包、指定目标类并启动应用。线上排障真正想要的是:输入一条命令,就临时观察某个方法。<p>这就是 Arthas 所处的第三层。<h2 id=第三层:Arthas-把动态增强变成排障命令><a title="第三层:Arthas 把动态增强变成排障命令" class=headerlink href=#第三层:Arthas-把动态增强变成排障命令></a>第三层:Arthas 把动态增强变成排障命令</h2><p>先启动不带任何 Agent 的 Demo,并让它持续执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>./run.sh app</span><br></pre></table></figure><p>另开一个终端,启动 Arthas:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>curl -O https://arthas.aliyun.com/arthas-boot.jar</span><br><span class=line>java -jar arthas-boot.jar --use-version 4.3.2</span><br></pre></table></figure><p>选择:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>com.nicksxs.bytecode.demo.app.DemoApplication</span><br></pre></table></figure><p>这一步之后,Arthas Agent 已经通过 Attach API 进入目标 JVM,并拿到了 <code>Instrumentation</code>。<h3 id=第一步先确认类和-ClassLoader><a title="第一步先确认类和 ClassLoader" class=headerlink href=#第一步先确认类和-ClassLoader></a>第一步先确认类和 ClassLoader</h3><figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>sc -d com.nicksxs.bytecode.demo.app.DemoService</span><br></pre></table></figure><p>本文测试时的关键输出是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>class-info com.nicksxs.bytecode.demo.app.DemoService</span><br><span class=line>code-source .../asm-bytekit-arthas-demo/target/classes/</span><br><span class=line>class-loader +-sun.misc.Launcher$AppClassLoader@73d16e93</span><br><span class=line>classLoaderHash 73d16e93</span><br></pre></table></figure><p>这一步并不多余。同一个类名可能被多个 ClassLoader 各加载一份,特别是在 Tomcat、插件系统和热部署环境里。如果不先确认 ClassLoader,后面的命令可能命中错误的类,或者提示匹配到了多个类。<p>也可以继续查看方法:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>sm com.nicksxs.bytecode.demo.app.DemoService</span><br></pre></table></figure><h3 id=watch-观察一次方法的输入和输出><a title="watch 观察一次方法的输入和输出" class=headerlink href=#watch-观察一次方法的输入和输出></a>watch 观察一次方法的输入和输出</h3><p>执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>watch com.nicksxs.bytecode.demo.app.DemoService placeOrder \</span><br><span class=line> <span class=string>'{params,returnObj,throwExp}'</span> -x 2 -n 4</span><br></pre></table></figure><p>几个参数分别表示:<table><thead><tr><th>参数<th>含义<tbody><tr><td><code>params</code><td>当前调用的参数数组<tr><td><code>returnObj</code><td>正常返回值<tr><td><code>throwExp</code><td>异常,没有异常时为 <code>null</code><tr><td><code>-x 2</code><td>对象展开深度为 2<tr><td><code>-n 4</code><td>命中 4 次后自动退出</table><p>实际输出之一:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>method=com.nicksxs.bytecode.demo.app.DemoService.placeOrder location=AtExit</span><br><span class=line>[cost=123.336292ms] result=@ArrayList[</span><br><span class=line> @Object[][</span><br><span class=line> @String[user-25],</span><br><span class=line> @Integer[25],</span><br><span class=line> ],</span><br><span class=line> @String[order created: user=user-25, sku=SKU-USER-25, quantity=25],</span><br><span class=line> null,</span><br><span class=line>]</span><br></pre></table></figure><p>可以看到,<code>watch</code> 输出里的 <code>location=AtExit</code>,和 ByteKit 的 <code>@AtExit</code> 已经能对应起来。这里出现的 <code>params</code>、<code>returnObj</code> 和 <code>throwExp</code>,本质上也来自方法入口、正常出口和异常出口处插入的探针。<h3 id=trace-为什么能看到内部调用><a title="trace 为什么能看到内部调用" class=headerlink href=#trace-为什么能看到内部调用></a>trace 为什么能看到内部调用</h3><p>执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>trace com.nicksxs.bytecode.demo.app.DemoService placeOrder \</span><br><span class=line> <span class=string>'#cost > 0'</span> -n 2</span><br></pre></table></figure><p>本文实测输出:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>`---[116.058125ms] DemoService:placeOrder()</span><br><span class=line> +---[25.93% 30.089459ms ] DemoService:validateQuantity() #10</span><br><span class=line> +---[38.65% 44.852583ms ] DemoService:queryInventory() #11</span><br><span class=line> `---[35.08% 40.714208ms ] DemoService:saveOrder() #12</span><br></pre></table></figure><p>异常分支则是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>`---[0.902ms] DemoService:placeOrder() [throws Exception]</span><br><span class=line> `---throw:java.lang.IllegalArgumentException #7</span><br><span class=line> [user cannot be error]</span><br></pre></table></figure><p><code>watch</code> 主要关心目标方法的入口和出口,<code>trace</code> 还需要在方法内部的调用指令前后插入探针。对应到 ByteKit,就是 <code>@AtInvoke</code>、调用完成和调用异常等位置。<p>Arthas 源码里的 <code>Enhancer</code> 在 <code>isTracing</code> 为真时,会加入 <code>SpyTraceInterceptor</code>,遍历目标方法里的调用指令,并为每个调用注册 trace listener。最终看到的调用树,是这些 before/after/exception 事件按线程和调用层级组合出来的。<h3 id=jad-看到的是-JVM-当前认识的类><a title="jad 看到的是 JVM 当前认识的类" class=headerlink href=#jad-看到的是-JVM-当前认识的类></a>jad 看到的是 JVM 当前认识的类</h3><figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>jad com.nicksxs.bytecode.demo.app.DemoService placeOrder</span><br></pre></table></figure><p>它会把目标 JVM 中的 class 反编译出来,而不是去源码目录读取 <code>DemoService.java</code>。这在排查“代码明明改了,线上为什么还是旧逻辑”“同名 jar 到底加载了哪一份”时很有用。<p>本文 Demo 得到:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> String <span class="title function_">placeOrder</span><span class=params>(String user, <span class=type>int</span> quantity)</span> {</span><br><span class=line> <span class=keyword>if</span> (<span class=string>"error"</span>.equals(user)) {</span><br><span class=line> <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">IllegalArgumentException</span>(<span class=string>"user cannot be error"</span>);</span><br><span class=line> }</span><br><span class=line> <span class=built_in>this</span>.validateQuantity(quantity);</span><br><span class=line> <span class=type>String</span> <span class=variable>sku</span> <span class=operator>=</span> <span class=built_in>this</span>.queryInventory(user);</span><br><span class=line> <span class=keyword>return</span> <span class=built_in>this</span>.saveOrder(user, sku, quantity);</span><br><span class=line>}</span><br></pre></table></figure><p>需要注意,<code>watch -n</code> 或 <code>trace -n</code> 达到次数退出后,对应 listener 会结束,Arthas 也可能重新组织该类上的增强。因此 <code>jad</code> 展示的是执行命令当时 JVM 中有效的字节码状态,不应该把它简单理解成磁盘源码查看器。<h3 id=诊断结束为什么要-reset><a title="诊断结束为什么要 reset" class=headerlink href=#诊断结束为什么要-reset></a>诊断结束为什么要 reset</h3><figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>reset com.nicksxs.bytecode.demo.app.DemoService</span><br></pre></table></figure><p><code>watch</code>、<code>trace</code>、<code>monitor</code> 不是在 JVM 外面被动偷看,它们会修改目标类。诊断结束后执行 <code>reset</code>,可以移除 Arthas 对这个类的增强,让方法回到未观测状态。<p>官方文档也特别提醒,线上使用增强命令时要缩小类和方法匹配范围,并在结束后执行 <code>reset</code> 或 <code>stop</code>。<h2 id=从源码看-Arthas-是怎样使用-ByteKit-的><a title="从源码看 Arthas 是怎样使用 ByteKit 的" class=headerlink href=#从源码看-Arthas-是怎样使用-ByteKit-的></a>从源码看 Arthas 是怎样使用 ByteKit 的</h2><p>如果只看命令,很容易觉得 Arthas 内部可能有一套完全不同的魔法。直接看 <code>com.taobao.arthas.core.advisor.Enhancer</code>,链路就很清楚了。<p>首先,<code>Enhancer</code> 自己就是:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> <span class=keyword>class</span> <span class="title class_">Enhancer</span> <span class=keyword>implements</span> <span class="title class_">ClassFileTransformer</span></span><br></pre></table></figure><p>它收到 <code>classfileBuffer</code> 后,用 ByteKit 提供的 <code>AsmUtils</code> 读取:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=type>ClassNode</span> <span class=variable>classNode</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassNode</span>(Opcodes.ASM9);</span><br><span class=line><span class=type>ClassReader</span> <span class=variable>classReader</span> <span class=operator>=</span> AsmUtils.toClassNode(</span><br><span class=line> classfileBuffer, classNode);</span><br></pre></table></figure><p>然后解析 Arthas 自己定义的拦截器:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line><span class=type>DefaultInterceptorClassParser</span> <span class=variable>parser</span> <span class=operator>=</span></span><br><span class=line> <span class=keyword>new</span> <span class="title class_">DefaultInterceptorClassParser</span>();</span><br><span class=line></span><br><span class=line>interceptorProcessors.addAll(</span><br><span class=line> parser.parse(SpyInterceptor1.class));</span><br><span class=line>interceptorProcessors.addAll(</span><br><span class=line> parser.parse(SpyInterceptor2.class));</span><br><span class=line>interceptorProcessors.addAll(</span><br><span class=line> parser.parse(SpyInterceptor3.class));</span><br></pre></table></figure><p>如果是 <code>trace</code>,还会加入 <code>SpyTraceInterceptor</code>。找到匹配方法后,再执行:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line><span class=type>MethodProcessor</span> <span class=variable>methodProcessor</span> <span class=operator>=</span></span><br><span class=line> <span class=keyword>new</span> <span class="title class_">MethodProcessor</span>(classNode, methodNode, locationFilter);</span><br><span class=line></span><br><span class=line><span class=keyword>for</span> (InterceptorProcessor interceptor : interceptorProcessors) {</span><br><span class=line> interceptor.process(methodProcessor);</span><br><span class=line>}</span><br></pre></table></figure><p>最后重新生成 class:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=type>byte</span>[] enhanced = AsmUtils.toBytes(</span><br><span class=line> classNode, inClassLoader, classReader);</span><br><span class=line><span class=keyword>return</span> enhanced;</span><br></pre></table></figure><p>这和我们写的 ByteKit Demo 几乎是同一条主干,只是 Arthas 多了大量工程能力:<ul><li>类名、方法名和 ClassLoader 匹配;<li>多命令 listener 管理;<li>防止重复插桩;<li>trace 内部调用注册;<li>CGLIB 构造函数等特殊 class 的处理;<li>原始类和增强状态管理;<li>命令退出后的恢复;<li>把事件送回 Arthas 自己的 ClassLoader。</ul><p>因此可以把它们的关系写成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br></pre><td class=code><pre><span class=line>用户输入 watch / trace</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Arthas 创建 Matcher 和 AdviceListener</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Enhancer 作为 ClassFileTransformer</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>ByteKit 把 AtEnter / AtExit / AtInvoke 转换为增强动作</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>ASM 读取、修改并写回 class byte[]</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Instrumentation.retransformClasses()</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>JVM 执行插入了 SpyAPI 调用的新方法</span><br></pre></table></figure><h2 id=SpyAPI-为什么要放在特殊位置><a title="SpyAPI 为什么要放在特殊位置" class=headerlink href=#SpyAPI-为什么要放在特殊位置></a>SpyAPI 为什么要放在特殊位置</h2><p>Arthas 自己的核心类由独立 ClassLoader 加载,而业务类可能来自 AppClassLoader、WebAppClassLoader 或自定义 ClassLoader。业务方法增强后如果直接引用 Arthas Core 里的某个类,业务 ClassLoader 不一定能找到它。<p>Arthas 使用 <code>java.arthas.SpyAPI</code> 作为一座稳定的桥:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>业务 ClassLoader 中被增强的方法</span><br><span class=line> |</span><br><span class=line> | SpyAPI.atEnter / atExit</span><br><span class=line> v</span><br><span class=line>可被目标类访问的 SpyAPI</span><br><span class=line> |</span><br><span class=line> | 转发事件</span><br><span class=line> v</span><br><span class=line>Arthas AdviceListener</span><br></pre></table></figure><p><code>Enhancer.transform()</code> 的开头还会检查目标 ClassLoader 能否加载 <code>SpyAPI</code>,不能加载就放弃增强。这正是字节码工具里一个很现实的问题:生成一条方法调用指令并不难,难的是确保这条指令在目标类的加载环境里永远能解析成功。<p>我们的小 Demo 把 Agent 和应用都放在同一个 classpath 中,所以感觉不到这种复杂度。真正的诊断工具必须认真处理 ClassLoader 边界。<h2 id=ASM、ByteKit、Arthas-和-Javassist-怎么选><a title="ASM、ByteKit、Arthas 和 Javassist 怎么选" class=headerlink href=#ASM、ByteKit、Arthas-和-Javassist-怎么选></a>ASM、ByteKit、Arthas 和 Javassist 怎么选</h2><p>之前的文章使用 Javassist,通过字符串形式插入代码:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>method.insertBefore(<span class=string>"System.out.println($args);"</span>);</span><br><span class=line>method.insertAfter(<span class=string>"System.out.println($_);"</span>);</span><br></pre></table></figure><p>它的优势是接近 Java 源码,入门很直观。把它和本文三个层次放在一起,可以这样理解:<table><thead><tr><th>工具<th>最适合的场景<th>优点<th>需要承担的复杂度<tbody><tr><td>Javassist<td>快速生成或修改类、简单 Agent<td>接近源码表达,容易上手<td>字符串代码、类型和复杂控制流处理<tr><td>ASM<td>框架底层、需要精确控制和低开销<td>能控制每条指令,生态基础扎实<td>栈、frame、descriptor、异常表<tr><td>ByteKit<td>APM、诊断类插桩框架<td>注入点和 Binding 丰富,适合复用<td>仍要负责 Agent、匹配、生命周期<tr><td>Arthas<td>直接排查运行中的 Java 应用<td>开箱即用、动态挂载、命令完整<td>需要控制线上影响和匹配范围</table><p>如果只是想确认线上某个参数或耗时,优先用 Arthas,而不是现场写 ASM。<p>如果在开发自己的诊断或 APM 能力,ByteKit 能减少大量通用插桩工作。<p>如果 ByteKit 没有提供需要的注入位置,或者正在实现更底层的字节码框架,再下沉到 ASM。<p>理解 ASM 的意义,不是以后所有事情都手写指令,而是知道上层工具究竟替我们处理了哪些问题。<h2 id=retransform-也不是任意热更新><a title="retransform 也不是任意热更新" class=headerlink href=#retransform-也不是任意热更新></a>retransform 也不是任意热更新</h2><p><code>Instrumentation.retransformClasses()</code> 可以替换方法实现,但它不是完整的类结构热更新。通常不能在重转换时随意:<ul><li>新增或删除字段;<li>新增或删除方法;<li>修改方法签名;<li>改变继承关系;<li>改变 nest host、record component 等类结构属性。</ul><p><code>watch</code> 和 <code>trace</code> 只在已有方法内部增加指令,没有改变类的 schema,所以适合使用 retransform。<p>这也解释了为什么 Arthas 的 <code>mc</code> 加 <code>retransform</code> 能修改已有方法逻辑,却不能把一个全新的字段热塞进已经加载的类。限制来自 JVM 类重定义规则,不是 Arthas 少实现了一个参数。<h2 id=多个-Agent-同时工作时发生什么><a title="多个 Agent 同时工作时发生什么" class=headerlink href=#多个-Agent-同时工作时发生什么></a>多个 Agent 同时工作时发生什么</h2><p>一个 JVM 可以注册多个 <code>ClassFileTransformer</code>。类加载或重转换时,它们会按照 JVM 规定的顺序依次收到字节码,后面的 Transformer 看到的可能已经是前面修改过的结果。<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>原始 class</span><br><span class=line> |</span><br><span class=line>APM Transformer</span><br><span class=line> |</span><br><span class=line>覆盖率 Transformer</span><br><span class=line> |</span><br><span class=line>Arthas Transformer</span><br><span class=line> |</span><br><span class=line>最终 class</span><br></pre></table></figure><p>如果某个工具:<ul><li>生成了不合法的 frame;<li>没有保留其他工具插入的指令;<li>重复增强同一位置;<li>对异常表和构造函数处理不兼容;</ul><p>就可能出现 <code>VerifyError</code>、<code>ClassFormatError</code>、方法过大,或者某个 Agent 的增强被另一个覆盖。<p>因此线上已经运行 SkyWalking、Pinpoint、JaCoCo 或安全 Agent 时,使用 Arthas 增强命令要更加谨慎。先精确匹配一个方法、限制命中次数,再观察是否存在兼容性问题。<h2 id=字节码增强的性能开销在哪里><a class=headerlink href=#字节码增强的性能开销在哪里 title=字节码增强的性能开销在哪里></a>字节码增强的性能开销在哪里</h2><p>“没有重启应用”不等于“完全没有运行影响”。主要开销可以分成三部分。<h3 id=重转换本身的开销><a class=headerlink href=#重转换本身的开销 title=重转换本身的开销></a>重转换本身的开销</h3><p>类被 retransform 后,JVM 之前为它生成的 JIT 编译代码可能失效,相关方法需要重新解释执行并再次编译。因此刚增强或刚 reset 后,可能看到 JIT 编译线程活跃,方法性能也可能短暂波动。<h3 id=每次调用探针的开销><a class=headerlink href=#每次调用探针的开销 title=每次调用探针的开销></a>每次调用探针的开销</h3><p>入口、出口和内部调用都会多执行一些指令。<code>trace</code> 的粒度比只观察一次出口的 <code>watch</code> 更细,通常也会有更多事件。<h3 id=输出和表达式计算的开销><a class=headerlink href=#输出和表达式计算的开销 title=输出和表达式计算的开销></a>输出和表达式计算的开销</h3><p>把一个很大的对象用 <code>-x 5</code> 展开、执行复杂 OGNL 表达式、对高频方法持续输出,开销往往比那几条入口探针本身更大。<p>所以线上使用时建议:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>精确类名 > 大范围通配符</span><br><span class=line>精确方法名 > 整个类所有方法</span><br><span class=line>带 -n 次数限制 > 一直运行</span><br><span class=line>较小的 -x 展开深度 > 深度遍历对象图</span><br><span class=line>带条件表达式 > 输出所有请求</span><br></pre></table></figure><p>例如只观察慢于 100 毫秒的调用:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>watch com.nicksxs.bytecode.demo.app.DemoService placeOrder \</span><br><span class=line> <span class=string>'{params,returnObj,throwExp}'</span> <span class=string>'#cost > 100'</span> -x 2 -n 5</span><br></pre></table></figure><h2 id=把整条链路再走一遍><a class=headerlink href=#把整条链路再走一遍 title=把整条链路再走一遍></a>把整条链路再走一遍</h2><p>现在回到最开始的问题:不修改源码,怎样观察 <code>placeOrder()</code>?<h3 id=使用-ASM><a title="使用 ASM" class=headerlink href=#使用-ASM></a>使用 ASM</h3><p>我们自己完成:<ol><li>Agent 注册 Transformer;<li>匹配类名、方法名和 descriptor;<li>找到方法入口和每一个退出指令;<li>管理局部变量和操作数栈;<li>生成新的 class 字节数组。</ol><h3 id=使用-ByteKit><a title="使用 ByteKit" class=headerlink href=#使用-ByteKit></a>使用 ByteKit</h3><p>仍然需要 Agent 和 Transformer,但注入逻辑变成:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=meta>@AtEnter</span> + <span class=meta>@Binding</span>.Args</span><br><span class=line><span class=meta>@AtExit</span> + <span class=meta>@Binding</span>.Return</span><br><span class=line><span class=meta>@AtExceptionExit</span> + <span class=meta>@Binding</span>.Throwable</span><br></pre></table></figure><p>ByteKit 再把这些声明转换为 ASM 指令。<h3 id=使用-Arthas><a title="使用 Arthas" class=headerlink href=#使用-Arthas></a>使用 Arthas</h3><p>只需要输入:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>watch ...</span><br><span class=line>trace ...</span><br></pre></table></figure><p>Arthas 负责动态 Attach、类和方法匹配、ByteKit 插桩、listener 管理、结果展示以及恢复。<p>所以三者最准确的关系并不是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ASM vs ByteKit vs Arthas</span><br></pre></table></figure><p>而是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>Arthas</span><br><span class=line> 使用 ByteKit 表达诊断插桩</span><br><span class=line> 使用 ASM 操作字节码</span><br><span class=line> 通过 Instrumentation 交给 JVM</span><br></pre></table></figure><p>理解到这一层以后,再看 <code>watch</code> 输出里的 <code>AtExit</code>、<code>trace</code> 的内部调用树,以及 Agent 里的 <code>ClassFileTransformer</code>,它们就不再是几个零散的名词,而是同一条执行链路上的不同环节。<h2 id=完整工程源码><a class=headerlink href=#完整工程源码 title=完整工程源码></a>完整工程源码</h2><p>前面为了讲清原理,只截取了每个环节最关键的代码。下面通过 Hexo 的 <code>include_code</code> 标签直接加载本地工程文件,博客页面会把它们渲染成完整的代码块。<p>工程结构如下:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br></pre><td class=code><pre><span class=line>asm-bytekit-arthas-demo/</span><br><span class=line>├── pom.xml</span><br><span class=line>├── run.sh</span><br><span class=line>├── arthas-commands.txt</span><br><span class=line>└── src/main/java/com/nicksxs/bytecode/demo/</span><br><span class=line> ├── app/</span><br><span class=line> │ ├── DemoApplication.java</span><br><span class=line> │ └── DemoService.java</span><br><span class=line> ├── asm/</span><br><span class=line> │ ├── AsmAgent.java</span><br><span class=line> │ └── AsmTimingTransformer.java</span><br><span class=line> └── bytekit/</span><br><span class=line> ├── ByteKitAgent.java</span><br><span class=line> ├── ByteKitTransformer.java</span><br><span class=line> └── DemoInterceptor.java</span><br></pre></table></figure><h3 id=Maven-配置><a title="Maven 配置" class=headerlink href=#Maven-配置></a>Maven 配置</h3><p><code>pom.xml</code> 中除了 ASM 和 ByteKit 依赖,还通过 <code>maven-jar-plugin</code> 分别生成两个带不同 <code>Premain-Class</code> 的 Agent JAR。<h3 id=示例应用><a class=headerlink href=#示例应用 title=示例应用></a>示例应用</h3><p><code>DemoApplication</code> 会循环调用 <code>DemoService.placeOrder()</code>。每四次调用构造一次异常,方便同时验证正常返回和异常退出。<h3 id=ASM-Agent><a title="ASM Agent" class=headerlink href=#ASM-Agent></a>ASM Agent</h3><p>Agent 入口只负责注册 Transformer,具体的类匹配和指令生成都在 <code>AsmTimingTransformer</code> 中。<h3 id=ByteKit-Agent><a title="ByteKit Agent" class=headerlink href=#ByteKit-Agent></a>ByteKit Agent</h3><p>ByteKit 版本同样由 Agent 注册 Transformer,但插入点和运行时数据改用注解与 Binding 描述。<h3 id=运行脚本和-Arthas-命令><a title="运行脚本和 Arthas 命令" class=headerlink href=#运行脚本和-Arthas-命令></a>运行脚本和 Arthas 命令</h3><p><code>run.sh</code> 把原始应用、ASM Agent、ByteKit Agent 和 Arthas 目标应用四种运行方式放在了一起。<h2 id=完整验证清单><a class=headerlink href=#完整验证清单 title=完整验证清单></a>完整验证清单</h2><p>示例目录下依次执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br></pre><td class=code><pre><span class=line><span class=built_in>cd</span> <span class=built_in>source</span>/code/asm-bytekit-arthas-demo</span><br><span class=line></span><br><span class=line><span class=comment># 编译应用和两个 Agent</span></span><br><span class=line>mvn clean package</span><br><span class=line></span><br><span class=line><span class=comment># 1. 原始应用</span></span><br><span class=line>./run.sh baseline 4 10</span><br><span class=line></span><br><span class=line><span class=comment># 2. ASM 增强</span></span><br><span class=line>./run.sh asm 4 10</span><br><span class=line></span><br><span class=line><span class=comment># 3. ByteKit 增强</span></span><br><span class=line>./run.sh bytekit 4 10</span><br><span class=line></span><br><span class=line><span class=comment># 4. 为 Arthas 保持应用运行</span></span><br><span class=line>./run.sh app</span><br></pre></table></figure><p>另一个终端中:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>curl -O https://arthas.aliyun.com/arthas-boot.jar</span><br><span class=line>java -jar arthas-boot.jar --use-version 4.3.2</span><br></pre></table></figure><p>挂载后依次执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br></pre><td class=code><pre><span class=line>sc -d com.nicksxs.bytecode.demo.app.DemoService</span><br><span class=line></span><br><span class=line>watch com.nicksxs.bytecode.demo.app.DemoService placeOrder \</span><br><span class=line> <span class=string>'{params,returnObj,throwExp}'</span> -x 2 -n 4</span><br><span class=line></span><br><span class=line>trace com.nicksxs.bytecode.demo.app.DemoService placeOrder \</span><br><span class=line> <span class=string>'#cost > 0'</span> -n 3</span><br><span class=line></span><br><span class=line>jad com.nicksxs.bytecode.demo.app.DemoService placeOrder</span><br><span class=line></span><br><span class=line>reset com.nicksxs.bytecode.demo.app.DemoService</span><br></pre></table></figure><p>到这里,ASM、ByteKit 和 Arthas 的三个层次就都不只是概念,而是可以在同一个业务方法上亲手验证的结果了。<h2 id=参考资料><a class=headerlink href=#参考资料 title=参考资料></a>参考资料</h2><ul><li><a href=https://asm.ow2.io/ rel=noopener target=_blank>ASM 官方文档</a><li><a href=https://asm.ow2.io/versions.html rel=noopener target=_blank>ASM 版本记录</a><li><a href=https://github.com/alibaba/bytekit rel=noopener target=_blank>ByteKit 源码与说明</a><li><a href=https://github.com/alibaba/bytekit/blob/master/bytekit-core/src/test/java/com/example/ByteKitDemo.java rel=noopener target=_blank>ByteKit 官方示例 ByteKitDemo</a><li><a href=https://github.com/alibaba/arthas rel=noopener target=_blank>Arthas 源码</a><li><a href=https://github.com/alibaba/arthas/blob/master/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java rel=noopener target=_blank>Arthas Enhancer 源码</a><li><a href=https://arthas.aliyun.com/doc/commands.html rel=noopener target=_blank>Arthas 命令列表</a><li><a href=https://arthas.aliyun.com/doc/retransform.html rel=noopener target=_blank>Arthas retransform 文档</a><li><a href=https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html rel=noopener target=_blank>Java Instrumentation API</a></ul></div><footer class=post-footer><aside aria-label=文章所属专题 class=post-series><span>所属专题</span><a href=/topics/#java-runtime>Java 运行时与生产诊断</a><p>从类加载、Java Agent 和 Javassist 进入字节码增强,最后串联到 ByteKit 与 Arthas。</aside><div class="affiliate-creative affiliate-creative--post-end" data-affiliate-id=tencent-cloud-general><a rel="sponsored noopener" aria-label=腾讯云产品优惠 href=https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=2e33a1eedb380997d03618b4a5ba6ca7&from=console target=_blank><img alt=腾讯云服务器、数据库和云产品优惠 decoding=async height=419 loading=lazy src=/uploads/affiliate/tencent-cloud-general.webp width=690> </a><span class=affiliate-disclosure>推广</span></div><div class=reward-container><div>请我一杯咖啡吧!</div><button>赞赏</button><div class=post-reward><div><img alt="Nicksxs 微信" src=https://i.loli.net/2020/01/12/NUlWanT31E8LQGx.png><span>微信</span></div><div><img alt="Nicksxs 支付宝" src=https://ooo.0o0.ooo/2016/08/17/57b421e773057.jpg><span>支付宝</span></div></div></div><div class=post-copyright><ul><li class=post-copyright-author><strong>本文作者: </strong>Nicksxs<li class=post-copyright-link><strong>本文链接:</strong> <a href=https://nicksxs.me/2026/08/30/%E4%BB%8EASM%E5%88%B0ByteKit%E5%86%8D%E5%88%B0Arthas-%E4%B8%80%E6%9D%A1Java%E5%AD%97%E8%8A%82%E7%A0%81%E5%A2%9E%E5%BC%BA%E9%93%BE%E8%B7%AF/ title=从ASM到ByteKit再到Arthas:一条Java字节码增强链路>https://nicksxs.me/2026/08/30/从ASM到ByteKit再到Arthas-一条Java字节码增强链路/</a><li class=post-copyright-license><strong>版权声明: </strong>本博客所有文章除特别声明外,均采用 <a href=https://creativecommons.org/licenses/by-nc-sa/4.0/ rel=noopener target=_blank><i class="fab fa-fw fa-creative-commons"></i>BY-NC-SA</a> 许可协议。转载请注明出处!</ul></div><div class=post-tags><a href=/tags/Java/ rel=tag># Java</a><a href=/tags/ASM/ rel=tag># ASM</a><a href=/tags/ByteKit/ rel=tag># ByteKit</a><a href=/tags/Arthas/ rel=tag># Arthas</a><a href=/tags/%E5%AD%97%E8%8A%82%E7%A0%81/ rel=tag># 字节码</a><a href=/tags/Java-Agent/ rel=tag># Java Agent</a></div><div class=post-nav><div class=post-nav-item><a href=/2026/08/23/8GB%E6%98%BE%E5%AD%98%E5%A6%82%E4%BD%95%E8%B7%9135B-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3FreeToken%E7%9A%84%E4%B8%93%E5%AE%B6%E7%BC%93%E5%AD%98%E4%B8%8ECPU-GPU%E5%8D%8F%E5%90%8C/ rel=prev title=8GB显存如何跑35B:深入理解FreeToken的专家缓存与CPU-GPU协同><i class="fa fa-angle-left"></i> 8GB显存如何跑35B:深入理解FreeToken的专家缓存与CPU-GPU协同</a></div><div class=post-nav-item><a title="6GB显存能跑35B MoE吗:FreeToken极限配置、性能压榨与实操教程" href=/2026/09/05/6GB%E6%98%BE%E5%AD%98%E8%B7%9135B-MoE-FreeToken%E6%9E%81%E9%99%90%E9%85%8D%E7%BD%AE%E4%B8%8E%E5%AE%9E%E6%93%8D%E6%95%99%E7%A8%8B/ rel=next>6GB显存能跑35B MoE吗:FreeToken极限配置、性能压榨与实操教程 <i class="fa fa-angle-right"></i></a></div></div></footer></article></div><div class=comments id=disqus_thread><noscript>Please enable JavaScript to view the comments powered by Disqus.</noscript></div></div></main><footer class=footer><div class=footer-inner><div class=beian><a href=https://beian.miit.gov.cn/ rel=noopener target=_blank>浙ICP备16002580号-2 </a><img alt src=/uploads/beian.png><a href=https://beian.mps.gov.cn/#/query/webSearch?code=33010602011094 rel=noopener target=_blank>浙公网安备 33010602011094号</a></div><div class=copyright>© <span itemprop=copyrightYear>2026</span><span class=with-love><i class="fa fa-heart"></i> </span><span class=author itemprop=copyrightHolder>Nicksxs</span></div></div></footer><div class="toggle sidebar-toggle" role=button><span class=toggle-line></span><span class=toggle-line></span><span class=toggle-line></span></div><div class=sidebar-dimmer></div><div aria-label=返回顶部 class=back-to-top role=button><i class="fa fa-arrow-up fa-lg"></i><span>0%</span></div><a aria-label="在 GitHub 上关注我" title="在 GitHub 上关注我" class=github-corner href=https://github.com/nicksxs rel=noopener target=_blank><svg viewbox="0 0 250 250" aria-hidden=true height=80 width=80><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" style="transform-origin:130px 106px" class=octo-arm fill=currentColor></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" class=octo-body fill=currentColor></path></svg></a><noscript><div class=noscript-warning>Theme NexT works best with JavaScript enabled</div></noscript><script data-cloudflare-page-counter-config type=application/json>{"enable":true,"endpoint":"https://counter.nicksxs.me","site":"nicksxs","local_cache":true,"timeout":5000,"icon":"far fa-eye","item_text":true}</script><script src=/js/cloudflare-page-counter.js></script><script class=next-config data-name=disqus type=application/json>{"enable":true,"shortname":"nicksxs","count":false,"i18n":{"disqus":"disqus"}}</script><script defer src=/js/third-party/comments/disqus.js></script>
|