trace java_使用java動態字節碼技術簡單實現arthas的trace功能。

參考資料

用過[Arthas]的都知道,Arthas是alibaba開源的一個非常強大的Java診斷工具。

不管是線上還是線下,我們都可以用Arthas分析程序的線程狀態、查看jvm的實時運行狀態、打印方法的出入參和返回類型、收集方法中每個代碼塊耗時,

甚至可以監控類、方法的調用次數、成功次數、失敗次數、平均響應時長、失敗率等。

前幾天學習java動態字節碼技術時,突然想起這款java診斷工具的trace功能:打印方法中每個節點的調用耗時。簡簡單單的,正好拿來做動態字節碼入門學習的demo。

程序結構

src

├── agent-package.bat

├── java

│ ├── asm

│ │ ├── MANIFEST.MF

│ │ ├── TimerAgent.java

│ │ ├── TimerAttach.java

│ │ ├── TimerMethodVisitor.java

│ │ ├── TimerTrace.java

│ │ └── TimerTransformer.java

│ └── demo

│ ├── MANIFEST.MF

│ ├── Operator.java

│ └── Test.java

├── run-agent.bat

├── target-package.bat

└── tools.jar

編寫目標程序

代碼

package com.gravel.demo.test.asm;

/**

* @Auther: syh

* @Date: 2020/10/12

* @Description:

*/

public class Test {

public static boolean runnable = true;

public static void main(String[] args) throws Exception {

while (runnable) {

test();

}

}

// 目標:分析這個方法中每個節點的耗時

public static void test() throws Exception {

Operator.handler();

long time_wait = (long) ((Math.random() * 1000) + 2000);

Operator.callback();

Operator.pause(time_wait);

}

}

Operator.java

/**

* @Auther: syh

* @Date: 2020/10/28

* @Description: 輔助類,同樣可用于分析耗時

*/

public class Operator {

public static void handler() throws Exception {

long time_wait = (long) ((Math.random() * 10) + 20);

sleep(time_wait);

}

public static void callback() throws Exception {

long time_wait = (long) ((Math.random() * 10) + 20);

sleep(time_wait);

}

public static void pause(long time_wait) throws Exception {

sleep(time_wait);

}

public static void stop() throws Exception {

Test.runnable = false;

System.out.println("business stopped.");

}

private static void sleep(long time_wait) throws Exception {

Thread.sleep(time_wait);

}

}

MANIFEST.MF

編寫MANIFEST.MF文件,指定main-class。注意:冒號后面加空格,結尾加兩行空白行。

Manifest-Version: 1.0

Archiver-Version: Plexus Archiver

Built-By: syh

Created-By: Apache Maven

Build-Jdk: 1.8.0_202

Main-Class: com.gravel.demo.test.asm.Target

打包

偷懶寫了bat批命令,生成target.jar

@echo off & setlocal

attrib -s -h -r -a /s /d demo

rd /s /q demo

rd /q target.jar

javac -encoding utf-8 -d . ./java/demo/*.java

jar cvfm target.jar ./java/demo/MANIFEST.MF demo

rd /s /q demo

pause

java -jar target.jar

java agent探針

instrument 是 JVM 提供的一個可以修改已加載類文件的類庫。而要實現代碼的修改,我們需要實現一個 instrument agent。

jdk1.5時,agent有個內定方法premain。是在類加載前修改。所以無法做到修改正在運行的類。

jdk1.6后,agent新增了agentmain方法。agentmain是在虛擬機啟動以后加載的。所以可以做攔截、熱部署等。

講JAVA探針技術,實際上我自己也是半吊子。所以這里用的是邊分析別人例子邊摸索的思路來實現我的簡單的trace功能。

例子使用的是ASM字節碼生成框架

MANIFEST.MF

首先一個可用的jar,關鍵之一是MAINFEST.MF文件是吧。

Manifest-Version: 1.0

Archiver-Version: Plexus Archiver

Created-By: Apache Maven

Built-By: syh

Build-Jdk: 1.8.0_202

Agent-Class: asm.TimerAgent

Can-Retransform-Classes: true

Can-Redefine-Classes: true

Class-Path: ./tools.jar

Main-Class: asm.TimerAttach

我們從MANIFEST.MF中提取幾個關鍵的屬性

屬性

說明

Agent-Class

agentmain入口類

Premain-Class

premain入口類,與agent-class至少指定一個。

Can-Retransform-Classes

對于已經加載的類重新進行轉換處理,即會觸發重新加載類定義。

Can-Redefine-Classes

對已經加載的類不做轉換處理,而是直接把處理結果(bytecode)直接給JVM

Class-Path

asm動態字節碼技術依賴tools.jar,如果沒有可以從jdk的lib目錄下拷貝。

Main-Class

這里并不是agent的關鍵屬性,為了方便,我把加載虛擬機的程序和agent合并了。

代碼

然后我們來看看兩個入口類,首先分析一個可執行jar的入口類Main-Class。

public class TimerAttach {

public static void main(String[] args) throws Exception {

/**

* 啟動jar時,需要指定兩個參數:1目標程序的pid。 2 要修改的類路徑及方法,格式 package.class#methodName

*/

if (args.length < 2) {

System.out.println("pid and class must be specify.");

return;

}

if (!args[1].contains("#")) {

System.out.println("methodName must be specify.");

return;

}

VirtualMachine vm = VirtualMachine.attach(args[0]);

// 這里為了方便我把 vm和agent整合在一個jar里面了, args[1]就是agentmain的入參。

vm.loadAgent("agent.jar", args[1]);

}

}

代碼很簡單,1:args入參校驗;2:加載目標進程pid(args[0]);3:加載agent jar包(因為合并了,所以這個jar其實就是自己)。

其中vm.loadAgent(agent.jar, args[1])會調用agent-class中的agentmain方法,而args[1]就是agentmain的第一個入參。

public class TimerAgent {

public static void agentmain(String agentArgs, Instrumentation inst) {

String[] ownerAndMethod = agentArgs.split("#");

inst.addTransformer(new TimerTransformer(ownerAndMethod[1]), true);

try {

inst.retransformClasses(Class.forName(ownerAndMethod[0]));

System.out.println("agent load done.");

} catch (Exception e) {

e.printStackTrace();

System.out.println("agent load failed!");

}

}

}

在 agentmain 方法里,我們調用retransformClassess方法載入目標類,調用addTransformer方法加載TimerTransformer類實現對目標類的重新定義。

類轉換器

public class TimerTransformer implements ClassFileTransformer {

private String methodName;

public TimerTransformer(String methodName) {

this.methodName = methodName;

}

@Override

public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classFileBuffer) {

ClassReader reader = new ClassReader(classFileBuffer);

ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

ClassVisitor classVisitor = new TimerTrace(Opcodes.ASM5, classWriter, methodName);

reader.accept(classVisitor, ClassReader.EXPAND_FRAMES);

return classWriter.toByteArray();

}

}

對被匹配到的類中的方法進行修改

public class TimerTrace extends ClassVisitor implements Opcodes {

private String owner;

private boolean isInterface;

private String methodName;

public TimerTrace(int i, ClassVisitor classVisitor, String methodName) {

super(i, classVisitor);

this.methodName = methodName;

}

@Override

public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {

super.visit(version, access, name, signature, superName, interfaces);

owner = name;

isInterface = (access & ACC_INTERFACE) != 0;

}

@Override

public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,

String[] exceptions) {

MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);

// 匹配到指定methodName時,進行字節碼修改

if (!isInterface && mv != null && name.equals(methodName)) {

// System.out.println(" package.className:methodName()")

mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");

mv.visitTypeInsn(NEW, "java/lang/StringBuilder");

mv.visitInsn(DUP);

mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false);

mv.visitLdcInsn(" " + owner.replace("/", ".")

+ ":" + methodName + "() ");

mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",

"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);

mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

// 方法代碼塊耗時統計并打印

TimerMethodVisitor at = new TimerMethodVisitor(owner, access, name, descriptor, mv);

return at.getLocalVariablesSorter();

}

return mv;

}

public static void main(String[] args) throws IOException {

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);

TraceClassVisitor tv = new TraceClassVisitor(cw, new PrintWriter(System.out));

TimerTrace addFiled = new TimerTrace(Opcodes.ASM5, tv, "test");

ClassReader classReader = new ClassReader("demo.Test");

classReader.accept(addFiled, ClassReader.EXPAND_FRAMES);

File file = new File("out/production/asm-demo/demo/Test.class");

String parent = file.getParent();

File parent1 = new File(parent);

parent1.mkdirs();

file.createNewFile();

FileOutputStream fileOutputStream = new FileOutputStream(file);

fileOutputStream.write(cw.toByteArray());

}

}

要統計方法中每行代碼耗時,只需要在每一行代碼的前后加上當前時間戳然后相減即可。

所以我們的代碼是這么寫的。

public class TimerMethodVisitor extends MethodVisitor implements Opcodes {

private int start;

private int end;

private int maxStack;

private String lineContent;

public boolean instance = false;

private LocalVariablesSorter localVariablesSorter;

private AnalyzerAdapter analyzerAdapter;

public TimerMethodVisitor(String owner, int access, String name, String descriptor, MethodVisitor methodVisitor) {

super(Opcodes.ASM5, methodVisitor);

this.analyzerAdapter = new AnalyzerAdapter(owner, access, name, descriptor, this);

localVariablesSorter = new LocalVariablesSorter(access, descriptor, this.analyzerAdapter);

}

public LocalVariablesSorter getLocalVariablesSorter() {

return localVariablesSorter;

}

/**

* 進入方法后,最先執行

* 所以我們可以在這里定義一個最開始的時間戳, 然后創建一個局部變量var_end

* Long var_start = System.nanoTime();

* Long var_end;

*/

@Override

public void visitCode() {

mv.visitCode();

mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);

mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);

start = localVariablesSorter.newLocal(Type.LONG_TYPE);

mv.visitVarInsn(ASTORE, start);

end = localVariablesSorter.newLocal(Type.LONG_TYPE);

maxStack = 4;

}

/**

* 在每行代碼后面增加以下代碼

* var_end = System.nanoTime();

* System.out.println("[" + String.valueOf((var_end.doubleValue() - var_start.doubleValue()) / 1000000.0D) + "ms] " + "package.className:methodName() #lineNumber");

* var_start = var_end;

* @param lineNumber

* @param label

*/

@Override

public void visitLineNumber(int lineNumber, Label label) {

super.visitLineNumber(lineNumber, label);

if (instance) {

mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);

mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);

mv.visitVarInsn(ASTORE, end);

// System.out

mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");

// new StringBuilder();

mv.visitTypeInsn(NEW, "java/lang/StringBuilder");

mv.visitInsn(DUP);

mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false);

mv.visitLdcInsn(" -[");

mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",

"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

mv.visitVarInsn(ALOAD, end);

mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "doubleValue", "()D", false);

mv.visitVarInsn(ALOAD, start);

mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "doubleValue", "()D", false);

mv.visitInsn(DSUB);

mv.visitLdcInsn(new Double(1000 * 1000));

mv.visitInsn(DDIV);

// String.valueOf((end - start)/1000000)

mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(D)Ljava/lang/String;", false);

mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",

"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

mv.visitLdcInsn("ms] ");

mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",

"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

// .append("owner:methodName() #line")

mv.visitLdcInsn(this.lineContent + "#" + lineNumber);

mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",

"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

// stringBuilder.toString()

mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);

// println stringBuilder.toString()

mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

// start = end

mv.visitVarInsn(ALOAD, end);

mv.visitVarInsn(ASTORE, start);

maxStack = Math.max(analyzerAdapter.stack.size() + 4, maxStack);

}

instance = true;

}

/**

* 拼接字節碼內容

* @param opcode

* @param owner

* @param methodName

* @param descriptor

* @param isInterface

*/

@Override

public void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) {

super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface);

if (!isInterface && opcode == Opcodes.INVOKESTATIC) {

this.lineContent = owner.replace("/", ".")

+ ":" + methodName + "() ";

}

}

@Override

public void visitMaxs(int maxStack, int maxLocals) {

super.visitMaxs(Math.max(maxStack, this.maxStack), maxLocals);

}

}

如果初學者不會改字節碼。可以利用idea自帶的asm插件做參考。

dd9c8f9c8ebe94f068300aed32d17004.png

打包

這樣,一個可執行的agent jar就寫完了,然后打包

@echo off

attrib -s -h -r -a /s /d asm

rd /s /q asm

rd /q agent.jar

javac -XDignore.symbol.file=true -encoding utf-8 -d . ./java/asm/*.java

jar cvfm agent.jar ./java/asm/MANIFEST.MF asm

rd /s /q asm

exit

測試

運行目標程序 target.jar

java -jar target.jar

打印Test.test中每個節點耗時

java -jar agent.jar [pid] demo.Test#test

結果

d5fd908ed21219c25b684c8975bad266.png

打印Operator.handler方法每個節點耗時

28ec0610cfb373c33d2ebd111b4d91cd.png

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/529386.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/529386.shtml
英文地址,請注明出處:http://en.pswp.cn/news/529386.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

順時針小球圓周運動Java編程_如何使用CSS實現圓周運動小球的實例

我們時常在頁面中見到一些動畫效果&#xff0c;這些動畫效果&#xff0c;很多可以僅通過CSS來實現。在這里我們用到了CSS3的animation屬性。animation 屬性是一個簡寫屬性&#xff0c;用于設置六個動畫屬性&#xff1a;animation-name 規定需要綁定到選擇器的 keyframe 名稱。a…

java swing 外觀框架_【GUI】一、Swing外觀框架BeautyEye使用

一、Swing外觀框架BeautyEye使用1.1 導包1.2 使用BeautyEye L&Fpublic static void main(String[] args) {EventQueue.invokeLater(new Runnable() {public void run() {// 國人牛逼主題&#xff0c;值得學習// 初始化字體InitGlobalFont(new Font("微軟雅黑", F…

static詳解java_java中static作用詳解

static表示“全局”或者“靜態”的意思&#xff0c;用來修飾成員變量和成員方法&#xff0c;也可以形成靜態static代碼塊&#xff0c;但是Java語言中沒有全局變量的概念。被static修飾的成員變量和成員方法獨立于該類的任何對象。也就是說&#xff0c;它不依賴類特定的實例&…

java基礎案例教程前4章知識點_java學習記錄4 Java基礎知識點

java學習記錄4 Java基礎知識點1. 注釋單行注釋使用“//”開頭&#xff0c;后面是注釋的內容一般在要注釋的語句的上面加注釋多行注釋以“/*”開頭&#xff0c;以“*/”結尾可以做行內注釋不可嵌套使用文檔注釋以“/**”開頭&#xff0c;以“*/”結尾用來給類和方法做注釋&#…

打印狗的健康值Java_嵌入式狗的JAVA之路 HTML 補課

學了JAVA WEB應用&#xff0c;補充一下HTML的標簽&#xff0c;常用的&#xff0c;不然頁面都畫不出來了~~test 標題&#xff0c;h1h2h3 3個字號test 段落link 鏈接 使用 Target 屬性&#xff0c;你可以定義被鏈接的文檔在何處顯示。下面的這行會在新窗口打開文檔&#xff1a;Vi…

mysql 表2符合表1_MYSQL-表1和表2中所有可能性的所有行

表用戶id, name1, Jay Bob2, An Other表格頁id, name, html1, Welcome, Welcome to this page2, Goodbye, Thanks for visiting表user_pages **存儲頁面的用戶特定版本**user_id, page_id, html1, 1, User id 1 Welcome page我基本上需要一個查詢,該查詢將為我返回以下數據集-即…

java引擎組件_Java 腳本引擎入門

Java Script EngineJava 腳本引擎可以將腳本嵌入Java代碼中&#xff0c;可以自定義和擴展Java應用程序&#xff0c;自JDK1.6被引入&#xff0c;基于Rhino引擎&#xff0c;JDK1.8后使用Nashorn引擎&#xff0c;支持ECMAScript 5&#xff0c;但后期還可能會換。腳本引擎包位于jav…

python 角度傳感器模擬_python樹莓派紅外反射傳感器

本文實例為大家分享了python樹莓派紅外反射傳感器的程序&#xff0c;供大家參考&#xff0c;具體內容如下1、工具rpi3&#xff0c;微雪ARPI600&#xff0c;Infrared Reflective Sensor2、基本原理Infrared Reflective Sensor 輸出數字和模擬信號模擬信號通過ARPI600上的AD轉換芯…

JAVA awt eventqueue_線程“AWT-EventQueue-1”中的異常java.lang.NullPointerException

嗨&#xff0c;我收到錯誤Exception in thread "AWT-EventQueue-1" java.lang.NullPointerExceptionat P6.itemStateChanged(P6.java:215)at javax.swing.AbstractButton.fireItemStateChanged(AbstractButton.java:2023)at javax.swing.AbstractButton$Handler.item…

java構造方法賦值內存圖_java 面向對象(九):類的結構:構造器(一)簡介;屬性賦值順序;JavaBean的概念...

1.構造器(或構造方法)&#xff1a;Constructor構造器的作用&#xff1a;* 1.創建對象* 2.初始化對象的信息2.使用說明&#xff1a;* 1.如果沒顯式的定義類的構造器的話&#xff0c;則系統默認提供一個空參的構造器* 2.定義構造器的格式&#xff1a;權限修飾符 類名(形參列表){}…

java 集合modcount_源碼|jdk源碼之LinkedList與modCount字段

鏈表是對上一篇博文所說的順序表的一種實現。與ArrayList思路截然不同&#xff0c;鏈表的實現思路是&#xff1a;不同元素實際上是存儲在離散的內存空間中的。每一個元素都有一個指針指向下一個元素&#xff0c;這樣整個離散的空間就被“串”成了一個有順序的表。從鏈表的概念來…

idea 新建ssm java ee_IDEA搭建SSM項目實現增刪改查

首先打開IDEA&#xff0c;File—>New—>Project創建項目選擇左側導航欄里的Maven&#xff0c;勾上勾&#xff0c;選擇webapp按如下圖進行填寫創建完成后進入項目&#xff0c;右下角彈出的提示點擊右邊的Enable Auto-Import&#xff0c;自動配置連接數據庫&#xff0c;我用…

php mail centos_centos怎么發送郵件

一、安裝sendmail與mail1、安裝sendmail&#xff1a;1) centos下可以安裝命令&#xff1a;yum -y install sendmail2) 安裝完后啟動sendmail命令&#xff1a;service sendmail start2、安裝mail安裝命令&#xff1a;yum install -y mailx二、發送郵件1、通過文件內容發送發送命…

php文件的作用,php入口文件的作用-PHP問題

php入口文件的作用php入口文件能夠完成主動加載性能。解析PHP入口文件的主動加載性能php的主動加載&#xff1a;正在php5之前&#xff0c;咱們要用某個類或類的辦法&#xff0c;那必需include或許require&#xff0c;之后能力應用&#xff0c;每一次用一個類&#xff0c;都需求…

emacs php 配置文件,如何配置emacs進行正確的PHP開發?

我使用web模式(http://web-mode.org/)混合HTML / PHP文件和php模式為純PHP文件.最新版本的php-mode還推薦使用混合HTML / PHP文件的Web模式&#xff1a;https://github.com/ejmr/php-mode#avoid-html-template-compatibility.不同于其他模式,如mmm模式,mumamo或多網絡模式,嘗試…

php 5.3.9 漏洞,PHP-5.3.9遠程執行任意代碼漏洞(CVE-2012-0830) 詳解

這個新的修復方法初衷是好的, 但是卻帶來一個嚴重的問題(5.3.10中已經修復), 這個問題最初是由Stefan Esser發現的. 請看之前(5.3.9)最終的修復方案(php_register_variable_ex):代碼如下while (1) {if (zend_symtable_find(symtable1, escaped_index, index_len 1, (void **) …

java中隨機數邊界問題,java 簡單Dice問題(隨機數的運用)

[java]代碼庫/*** Dice Write a program that simulates rolling two dice using the following* steps: 1. Prompt the user for the number of sides for two dice. 2. “Roll” the* dice three times by generating a random number between 1 (inclusive) and the* number…

php 正則替換 ubb,php實現過濾UBB代碼的類

本文實例講述了php實現過濾UBB代碼的類。分享給大家供大家參考。具體如下&#xff1a;PHP代碼如下&#xff1a;class Day{function ubb($Text) { /// UBB代碼轉換//$Texthtmlspecialchars($Text);//$Textereg_replace("\r\n","",$Text);$Textereg_rep…

java單詞測試,java單詞 - 在線打字測試(dazi.kukuw.com)

java單詞貢獻者&#xff1a;15533470608類別&#xff1a;英文 時間&#xff1a;2018-08-04 22:32:16 收藏數&#xff1a;20 評分&#xff1a;0返回上頁舉報此文章請選擇舉報理由&#xff1a;廣告/謠言/欺詐政治敏感色情/違法信息垃圾文章其他收藏到我的文章改錯字public static…

java vector list,Java基礎之:List——ArrayList Vector

Java基礎之&#xff1a;List——ArrayList & VectorArrayList簡單介紹ArrayList實現了List接口&#xff0c;底層是一個數組&#xff0c;并實現了可變的功能。底層屬性(transient Object[] elementData;)在序列化時&#xff0c;忽略該屬性。ArrayList實現了List接口&#xf…