JVM探針與字節(jié)碼技術(shù)
JVM探針是自jdk1.5以來,由虛擬機提供的一套監(jiān)控類加載器和符合虛擬機規(guī)范的代理接口,結(jié)合字節(jié)碼指令能夠讓開發(fā)者實現(xiàn)無侵入的監(jiān)控功能。如:監(jiān)控生產(chǎn)環(huán)境中的函數(shù)調(diào)用情況或動態(tài)增加日志輸出等等。雖然在常規(guī)的業(yè)務(wù)中不會有太多用武之地,但是作為一項高級的技術(shù)手段也應(yīng)該是資深開發(fā)人員的必備技能之一。同時,它也是企業(yè)級開發(fā)和生產(chǎn)環(huán)境部署不可或缺的技術(shù)方案,是對當下流行的APM的一種補充,因為使用探針技術(shù)能夠?qū)崿F(xiàn)比常規(guī)APM平臺更細粒度的監(jiān)控。
哪些方面適合使用探針技術(shù):
(1) 如果你發(fā)現(xiàn)生產(chǎn)環(huán)境上有些問題無法在測試或開發(fā)環(huán)境中復(fù)現(xiàn)
(2)?如果你希望在不修改源碼的情況下為你的應(yīng)用添加一些輸出日志
(3)?如果在剛發(fā)布的生產(chǎn)包中發(fā)現(xiàn)了一個bug,而你又不希望被它阻斷,希望有一個臨時的補救措施
一、JVM探針:Instrumentation
?使用探針只需要一條附加選項:-javaagent:
package?aa.bb.cc;
public?class?PremainAgent?{
????public?static?void?premain(String?agentArgs,?Instrumentation?inst)?{
????????//?TODO
????}
}
MANIFEST.MF
premain-class:?aa.bb.cc.PremainAgent
如果使用maven作為構(gòu)建工具,需要在pom文件中添加構(gòu)建插件
??
????org.apache.maven.plugins ??
????maven-jar-plugin ??
????3.2.0 ??
??????
??????????
??????????????
????????????????aa.bb.cc.PremainAgent ??
???????????? ??
???????? ??
???? ??
如果你還引入了其它依賴希望同時打包,那么你應(yīng)該使用assembly插件替代
??
??org.apache.maven.plugins ??
??maven-assembly-plugin ??
??2.4 ??
????
??????
??????jar-with-dependencies ??
???? ??
??????
????????
????????.PremainAgent
????????true ??
????????true ??
?????? ??
???? ??
?? ??
????
??????
??????package ??
????????
????????single ??
?????? ??
???? ??
??
兩個重要的類
Instrumentation:?由JDK提供的一個探針類,它會負責加載用戶自定義的ClassFileTransformer
ClassFileTransformer:?字節(jié)碼轉(zhuǎn)換類,jvm在加載class文件前會先調(diào)用它,對所有類加載器有效
具體用法稍后會做詳細介紹。
總結(jié):JVM探針只是提供了一種讓開發(fā)人員能夠在類加載加載class文件前主動介入的一種方法,具體如何操作需要開發(fā)人員了解Java虛擬機規(guī)范以及字節(jié)碼的相關(guān)知識。
二、棧幀與指令集
棧幀(Stack Frame)是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。它是虛擬機運行時數(shù)據(jù)區(qū)中的虛擬機棧的棧元素。棧幀存儲了方法的局部變量表、操作數(shù)棧、動態(tài)連接和方法返回地址等信息。每一個方法從調(diào)用開始至執(zhí)行完成的過程,都對應(yīng)著一個棧幀在虛擬機里面從入棧到出棧的過程。
在編譯程序代碼的時候,棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了。因此一個棧幀需要分配多少內(nèi)存,不會受到程序運行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機實現(xiàn)。

局部變量表(Local Variable Table)是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。并且在Java編譯為Class文件時,就已經(jīng)確定了該方法所需要分配的局部變量表的最大容量。局部變量表類似一個數(shù)組結(jié)構(gòu),虛擬機在訪問局部變量表的時候會使用下標作為引用,普通方法的局部變量表中第0位索引默認是用于傳遞方法所屬對象實例的引用this。
操作數(shù)棧(Operand Stack)和局部變量表一樣,在編譯時期就已經(jīng)確定了該方法所需要分配的局部變量表的最大容量。當一個方法剛剛開始執(zhí)行的時候,這個方法的操作數(shù)棧是空的,在方法執(zhí)行的過程中,會有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容,也就是出棧/入棧操作。例如,在做算術(shù)運算的時候是通過操作數(shù)棧來進行的,又或者在調(diào)用其它方法的時候是通過操作數(shù)棧來進行參數(shù)傳遞的。
動態(tài)鏈接(Dynamic Linking)每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支付方法調(diào)用過程中的動態(tài)連接。在類加載階段中的解析階段會將符號引用轉(zhuǎn)為直接引用,這種轉(zhuǎn)化也稱為靜態(tài)解析。另外的一部分將在每一次運行時期轉(zhuǎn)化為直接引用,這部分稱為動態(tài)連接。
返回地址:當一個方法開始執(zhí)行后,只有2種方式可以退出這個方法,方法返回指令和異常退出。無論采用任何退出方式,在方法退出之后,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,方法返回時可能需要在棧幀中保存一些信息。一般來說,方法正常退出時,調(diào)用者的PC計數(shù)器的值可以作為返回地址,棧幀中會保存這個計數(shù)器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。
JVM指令集并非是對Java語句的直接翻譯,由于指令只使用1個字節(jié)表示,所以指令集最多只能包含256種指令。因此,一條Java語句一般會對應(yīng)多條底層指令。每一條指令都有與之對應(yīng)的助記符,我們可以通過官方資料查看它們對應(yīng)關(guān)系:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html。為了幫助大家更加直觀的理解字節(jié)碼指令,我將通過三個用例分別解釋。
從一個簡單的加法函數(shù)開始,我們可以使用javac將.java文件編譯成.class,再通過javap -c查看它的字節(jié)碼文件
public?int?add(int?x,?int?y)?{??
????return?x?+?y;??
}
public?add(II)I
????ILOAD?1?//?將局部變量表中#1變量入棧
????ILOAD?2?//?將局部變量表中#2變量入棧
????IADD?//?調(diào)用整型數(shù)相加(兩個數(shù)出棧,再將結(jié)果入棧)
????IRETURN?//?返回棧頂?shù)慕Y(jié)果
????MAXSTACK?=?2?//?最大棧數(shù)2
????MAXLOCALS?=?3?//?最大本地變量數(shù)3
第一行是它的函數(shù)簽名,2~7行的注釋分別是對指令的解釋。ILOAD,IADD,IRETURN分別是整型數(shù)的入棧,加法和返回操作。大家可以將add方法修改為靜態(tài)函數(shù)后重新編譯,看看MAXLOCALS是否有變化。
接下來我們把函數(shù)變得復(fù)雜一些,嘗試對函數(shù)的執(zhí)行時間做一個計算并輸出
public?int?add(int?x,?int?y)?{??
????long?t?=?System.nanoTime();??
????int?ret?=?x?+?y;??
????t?=?System.nanoTime()?-?t;??
????System.out.println(t);??
????return?ret;??
}
public?add(II)I
????INVOKESTATIC?java/lang/System.nanoTime?()J?//?調(diào)用靜態(tài)函數(shù),結(jié)果long入棧
????LSTORE?3?//?將棧頂?shù)膌ong保存到局部變量#3
????ILOAD?1
????ILOAD?2
????IADD
????ISTORE?5?//?將棧頂?shù)膇nt保存到局部變量#5
????INVOKESTATIC?java/lang/System.nanoTime?()J
????LLOAD?3?//?局部變量#3入棧
????LSUB?//?從棧頂彈出兩個long相減
????LSTORE?3?//?結(jié)果保存到變量#3
????GETSTATIC?java/lang/System.out?:?Ljava/io/PrintStream;?//?獲取靜態(tài)引用
????LLOAD?3?//?局部變量#3入棧
????INVOKEVIRTUAL?java/io/PrintStream.println?(J)V?//?調(diào)用函數(shù)
????ILOAD?5?//??局部變量#5入棧
????IRETURN
????MAXSTACK?=?4
????MAXLOCALS?=?6
第2行結(jié)尾的J表示函數(shù)返回值是long類型。第14行結(jié)尾的V表示println函數(shù)的返回值是void。第12行到第14行的指令對應(yīng)代碼的System.out.println(t),特別需要注意的是INVOKEVIRTUAL指令實際上需要從操作數(shù)棧獲取兩個數(shù),第一個數(shù)是在執(zhí)行了GETSTATIC后入棧的對象引用。
我們再次修改函數(shù),這一次我們引入比較和循環(huán)語句,盡管代碼的邏輯不太正常,但這并不妨礙我們理解
public?int?add(int?x,?int?y)?{??
????if(x?>?1)?{??
????????return?x?+?y;??
????}??
????for(int?i?=?0;?i?????????x?++;??
????}??
????return?x?-?y;??
}
public?add(II)I
????ILOAD?1
????ICONST_1?//?將一個常整型數(shù)1入棧
????IF_ICMPLE?L0?//?比較如果操兩個操作數(shù)是小于等于的關(guān)系則成立,否則跳轉(zhuǎn)到L0的位置繼續(xù)
????ILOAD?1
????ILOAD?2
????IADD
????IRETURN
???L0
????ICONST_0?//?將常整型數(shù)0入棧
????ISTORE?3?//?棧頂數(shù)保存到局部變量#3
???L1
????ILOAD?3
????ILOAD?2
????IF_ICMPGE?L2?//?比較棧頂?shù)膬蓚€操作數(shù)是否是大于等于的關(guān)系,如果不成立則跳轉(zhuǎn)到L2
????IINC?1?1?//?局部變量#1?自增1
????IINC?3?1?//?局部變量#3?自增1
????GOTO?L1?//?跳轉(zhuǎn)到L1執(zhí)行
???L2
????ILOAD?1
????ILOAD?2
????ISUB
????IRETURN
????MAXSTACK?=?2
????MAXLOCALS?=?4
當我們使用字節(jié)碼直接操作虛擬機中的底層代碼的時候,基本上就是通過改變局部變量表和操作數(shù)棧來改變程序的邏輯。還記得根據(jù)Java虛擬機規(guī)范,MAXSTACK和MAXLOCALS是在.java文件被編譯成.class就被確定下來的嗎,如果我們要對方法做出修改勢必會引入新的局部變量,這時就難免需要對MAXSTACK和MAXLOCALS做重新計算。好在目前流行的字節(jié)碼框架已經(jīng)可以自動幫助我們完成這項任務(wù)。
三、ASM框架?
ASM是一個比較硬核的字節(jié)碼框架,也是轉(zhuǎn)換效率最高的工具。下面是常用類的介紹:
1. ClassReader
按照Java虛擬機規(guī)范(JVMS)中定義的方式來解析class文件中的內(nèi)容,在遇到合適的字段時調(diào)用ClassVisitor中相對應(yīng)的方法。
ClassReader(final byte[] classFile)
構(gòu)造方法,通過class字節(jié)碼數(shù)據(jù)加載
ClassReader(final String className) throws IOException
通過class全路徑名從ClassLoader加載
2. ClassVisitor
java中類的訪問者,提供一系列方法由ClassReader調(diào)用。調(diào)用的順序如下:visit -> visitSource -> visitModule -> visitNestHost -> visitOuterClass -> visitAnnotation -> visitTypeAnnotation -> visitAttribute -> visitNestMember -> visitPermittedSubclass -> visitInnerClass -> visitRecordComponent -> visitField -> visitMethod -> visitEnd
3. ClassWriter
ClassVisitor的子類,通過它生成最后的字節(jié)碼。并且它可以幫助重新計算MAXSTACK和MAXLOCALS
4. ModuleVisitor
Java中模塊的訪問者,作為ClassVisitor.visitModule方法的返回值
5. AnnotationVisitor
Java中注解的訪問者,作為ClassVisito中visitTypeAnnotation和visitTypeAnnotation的返回值
6. FieldVisitor
Java中字段的訪問者,作為ClassVisito.visitField的返回值
7. MethodVisitor
Java中方法的訪問者,作為ClassVisito.visitMethod的返回值
visitMethodInsn?方法調(diào)用指令
visitVarInsn?局部變量調(diào)用指令
visitInsn(int)?訪問一個零參數(shù)要求的字節(jié)碼指令,如LSUB
visitLdcInsn?把一個常量放到棧頂
visitInvokeDynamicInsn?動態(tài)方法調(diào)用
visitFieldInsn?調(diào)用/訪問某個字段
8. AnalyzerAdapter
MethodVisitor的子類,使用它重新計算最大操作數(shù)棧(MAXSTACK)
9. LocalVariablesSorter
MethodVisitor的子類,使用它重新計算局部變量表(MAXLOCALS)的索引
newLocal?創(chuàng)建局部變量
通過IDEA的Plugins安裝ASM Bytecode Viewer Support Kotlin,我們可以借助這個插件來幫助我們生成大部分代碼,具體用法這里就贅述了。
總結(jié):有了以上知識基礎(chǔ),我們可以完成一個簡單的demo來感受探針和字節(jié)碼技術(shù)的強大。
一個計算函數(shù)執(zhí)行時間的完整用例
1. 在IDEA中創(chuàng)建一個典型的maven工程
2.編寫pom文件
"1.0"?encoding="UTF-8"?>??
"http://maven.apache.org/POM/4.0.0"??
?xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"??
?xsi:schemaLocation="http://maven.apache.org/POM/4.0.0?http://maven.apache.org/xsd/maven-4.0.0.xsd">??
????com.learnhow.study
????1.0
??????jar
????agent ??
??
??????
??????????
????????????org.ow2.asm ??
????????????asm ??
????????????9.2
???????? ??
??????????
????????????org.ow2.asm ??
????????????asm-commons
????????????9.2
???????? ??
???? ??
??
??????
??????????
??????????????
????????????????org.apache.maven.plugins ??
????????????????maven-compiler-plugin ??
????????????????3.8.1 ??
??????????????????
????????????????????<source>[your?jdk?version]source>??
????????????????????[your?jdk?version] ??
???????????????? ??
???????????? ??
??????????????
????????????????org.apache.maven.plugins ??
????????????????maven-assembly-plugin ??
????????????????2.4 ??
??????????????????
??????????????????????
????????????????????????jar-with-dependencies ??
???????????????????? ??
??????????????????????
??????????????????????????
????????????????????????????[your?package].PremainAgent
????????????????????????????true ??
????????????????????????????true ??
???????????????????????? ??
???????????????????? ??
???????????????? ??
??????????????????
??????????????????????
????????????????????????package ??
??????????????????????????
????????????????????????????single ??
???????????????????????? ??
???????????????????? ??
???????????????? ??
???????????? ??
???????? ??
???? ??
帶[]的部分請換成你的本地環(huán)境。
3.PremainAgent類
public?class?PremainAgent?{??
????public?static?void?premain(String?agentArgs,?Instrumentation?inst)?{??
????????inst.addTransformer(new?XClassFileTransformer());??
????}??
}4.XClassFileTransformer類
public?class?XClassFileTransformer?implements?ClassFileTransformer?{
????@Override
????public?byte[]?transform(ClassLoader?loader,
????????????????????????????String?className,
????????????????????????????Class>?classBeingRedefined,
????????????????????????????ProtectionDomain?protectionDomain,
????????????????????????????byte[]?classfileBuffer)?throws?IllegalClassFormatException?{
????????try?{
????????????ClassReader?cr?=?new?ClassReader(classfileBuffer);
????????????ClassWriter?cw?=?new?ClassWriter(ClassWriter.COMPUTE_MAXS);
????????????cr.accept(new?NanoTimerClassVisitor(cw),?ClassReader.SKIP_DEBUG);
????????????byte[]?cc?=?cw.toByteArray();
????????????return?cc;
????????}?catch?(IOException?e)?{
????????}
????????return?null;
????}
}
transform方法返回null或者new byte[0]表示對當前字節(jié)碼文件不進行修改。ClassWriter.COMPUTE_MAXS表示框架會自動計算MAXSTACK和MAXLOCALS,ClassReader.SKIP_DEBUG表示當字節(jié)碼中包含調(diào)試信息的時候,會忽略不會觸發(fā)回調(diào)。
5.NanoTimerClassVisitor類
public?class?NanoTimerClassVisitor?extends?ClassVisitor?{
????private?String?className;
????public?NanoTimerClassVisitor(ClassVisitor?classVisitor)?{
????????super(ASM9,?classVisitor);
????}
????@Override
????public?void?visit(int?version,?int?access,?String?name,?String?signature,?String?superName,?String[]?interfaces)?{
????????this.className?=?name;
????????super.visit(version,?access,?name,?signature,?superName,?interfaces);
????}
????@Override
????public?MethodVisitor?visitMethod(int?access,?String?name,?String?descriptor,?String?signature,?String[]?exceptions)?{
????????MethodVisitor?mv?=?super.visitMethod(access,?name,?descriptor,?signature,?exceptions);
????????if?(Objects.nonNull(mv)?&&?!name.equals("" )?&&?!name.equals("" ))?{
????????????NanoTimerMethodVisitor?methodVisitor?=?new?NanoTimerMethodVisitor(mv,?className,?access,?name,?descriptor);
????????????return?methodVisitor.refactor();
????????}
????????return?mv;
????}
????class?NanoTimerMethodVisitor?extends?MethodVisitor?{
????????private?AnalyzerAdapter?analyzerAdapter;
????????private?LocalVariablesSorter?localVariablesSorter;
????????private?int?timeOpcode;
????????private?int?outOpcode;
????????private?String?className;
????????private?int?methodAccess;
????????private?String?methodName;
????????private?String?methodDescriptor;
????????public?NanoTimerMethodVisitor(MethodVisitor?methodVisitor,?String?className,?int?methodAccess,
?????????????????????????????????String?methodName,?String?methodDescriptor)?{
????????????super(ASM9,?methodVisitor);
????????????this.className?=?className;
????????????this.methodAccess?=?methodAccess;
????????????this.methodName?=?methodName;
????????????this.methodDescriptor?=?methodDescriptor;
????????????//?使用AnalyzerAdapter計算最大操作數(shù)棧
????????????analyzerAdapter?=?new?AnalyzerAdapter(className,?methodAccess,?methodName,?methodDescriptor,?this);
????????????//?LocalVariablesSorter重新計算局部變量的索引并自動更新字節(jié)碼中的索引引用
????????????localVariablesSorter?=?new?LocalVariablesSorter(methodAccess,?methodDescriptor,?analyzerAdapter);
????????}
????????public?MethodVisitor?refactor()?{
????????????return?localVariablesSorter;
????????}
????????@Override
????????public?void?visitCode()?{
????????????super.visitCode();
????????????mv.visitMethodInsn(INVOKESTATIC,?"java/lang/System",?"nanoTime",?"()J",?false);
????????????timeOpcode?=?localVariablesSorter.newLocal(Type.LONG_TYPE);
????????????mv.visitVarInsn(LSTORE,?timeOpcode);
????????}
????????@Override
????????public?void?visitInsn(int?opcode)?{
????????????if?((opcode?>=?IRETURN?&&?opcode?<=?RETURN)?||?opcode?==?ATHROW)?{
????????????????mv.visitMethodInsn(INVOKESTATIC,?"java/lang/System",?"nanoTime",?"()J",?false);
????????????????mv.visitVarInsn(LLOAD,?timeOpcode);
????????????????mv.visitInsn(LSUB);
????????????????mv.visitVarInsn(LSTORE,?timeOpcode);
????????????????mv.visitLdcInsn(className?+?"."?+?methodName?+?"(ns):");
????????????????outOpcode?=?localVariablesSorter.newLocal(Type.getType(String.class));
????????????????mv.visitVarInsn(ASTORE,?outOpcode);
????????????????mv.visitVarInsn(ALOAD,?outOpcode);
????????????????mv.visitVarInsn(LLOAD,?timeOpcode);
????????????????mv.visitInvokeDynamicInsn("makeConcatWithConstants",?"(Ljava/lang/String;J)Ljava/lang/String;",?new?Handle(Opcodes.H_INVOKESTATIC,?"java/lang/invoke/StringConcatFactory",?"makeConcatWithConstants",?"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;",?false),?new?Object[]{"\u0001\u0001"});
????????????????mv.visitVarInsn(ASTORE,?outOpcode);
????????????????mv.visitFieldInsn(GETSTATIC,?"java/lang/System",?"out",?"Ljava/io/PrintStream;");
????????????????mv.visitVarInsn(ALOAD,?outOpcode);
????????????????mv.visitMethodInsn(INVOKEVIRTUAL,?"java/io/PrintStream",?"println",?"(Ljava/lang/String;)V",?false);
????????????}
????????????super.visitInsn(opcode);
????????}
????}
}
6. 通過assembly插件對項目進行打包生成:agent-1.0-jar-with-dependencies.jar
7. 運行一個目標項目,并添加虛擬機指令-javaagent,就可以看到執(zhí)行效果
如何查看生成后的代碼
計算函數(shù)執(zhí)行時間是一個非常簡單的功能,我們很容易一次性寫正確。但是如果需要代理的邏輯比較復(fù)雜,而探針程序又不像普通程序一樣方便做斷點調(diào)試。我們?nèi)绾尾拍軌蚝芊奖阒郎傻拇a是否正確呢?這里告訴大家一個訣竅。回到我們XClassFileTransformer類,增加兩行代碼:
public?class?XClassFileTransformer?implements?ClassFileTransformer?{
????@Override
????public?byte[]?transform(ClassLoader?loader,
????????????????????????????String?className,
????????????????????????????Class>?classBeingRedefined,
????????????????????????????ProtectionDomain?protectionDomain,
????????????????????????????byte[]?classfileBuffer)?throws?IllegalClassFormatException?{
????????try?{
????????????ClassReader?cr?=?new?ClassReader(classfileBuffer);
????????????ClassWriter?cw?=?new?ClassWriter(ClassWriter.COMPUTE_MAXS);
????????????cr.accept(new?NanoTimerClassVisitor(cw),?ClassReader.SKIP_DEBUG);
????????????byte[]?cc?=?cw.toByteArray();
????????????FileOutputStream?fos?=?new?FileOutputStream("./cc.class");
????????????fos.write(cc);
????????????return?cc;
????????}?catch?(IOException?e)?{
????????}
????????return?null;
????}
}
第13、14行代碼的功能是將生成的字節(jié)碼輸出到本地文件中,然后我們通過IDEA打開這個.class文件,看看新增加的代碼是否如我們預(yù)期的那樣。
總結(jié):JVM代理發(fā)生在類加載器加載.class文件前,因此我們能夠動態(tài)修改字節(jié)碼。通過ASM這類字節(jié)碼框架,使得開發(fā)人員即使對字節(jié)碼指令不是很熟悉依然能夠操作。當然,Java的探針技術(shù)除了和被代理的項目同時啟動以外還提供了一種熱部署的方案,受篇幅限制不再贅述,如果大家有興趣可以給我留言。
? 作者?|??冷豪
來源 |??cnblogs.com/learnhow/p/15364955.html

