您好,登錄后才能下訂單哦!
這篇文章主要講解了“Java字節(jié)碼結(jié)構(gòu)是什么”,文中的講解內(nèi)容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Java字節(jié)碼結(jié)構(gòu)是什么”吧!
圖3 JVM規(guī)定的字節(jié)碼結(jié)構(gòu)
(1) 魔數(shù)(Magic Number)
圖4 常量池的結(jié)構(gòu)
圖5 前十個字節(jié)及含義
圖6 各類型的cp_info
具體以CONSTANT_utf8_info為例,它的結(jié)構(gòu)如下圖7左側(cè)所示。首先一個字節(jié)“tag”,它的值取自上圖6中對應項的Tag,由于它的類型是utf8_info,所以值為“01”。接下來兩個字節(jié)標識該字符串的長度Length,然后Length個字節(jié)為這個字符串具體的值。從圖2中的字節(jié)碼摘取一個cp_info結(jié)構(gòu),如下圖7右側(cè)所示。將它翻譯過來后,其含義為:該常量類型為utf8字符串,長度為一字節(jié),數(shù)據(jù)為“a”。
圖7 CONSTANT_utf8_info的結(jié)構(gòu)(左)及示例(右)
其他類型的cp_info結(jié)構(gòu)在本文不再贅述,整體結(jié)構(gòu)大同小異,都是先通過Tag來標識類型,然后后續(xù)n個字節(jié)來描述長度和(或)數(shù)據(jù)。先知其所以然,以后可以通過javap -verbose ByteCodeDemo命令,查看JVM反編譯后的完整常量池,如下圖8所示??梢钥吹椒淳幾g結(jié)果將每一個cp_info結(jié)構(gòu)的類型和值都很明確地呈現(xiàn)了出來。
圖8 常量池反編譯結(jié)果
(4) 訪問標志
圖9 訪問標志
(5) 當前類名
圖11 字段表示例
(9)方法表
字段表結(jié)束后為方法表,方法表也是由兩部分組成,第一部分為兩個字節(jié)描述方法的個數(shù);第二部分為每個方法的詳細信息。方法的詳細信息較為復雜,包括方法的訪問標志、方法名、方法的描述符以及方法的屬性,如下圖所示:
圖12 方法表結(jié)構(gòu)
方法的權(quán)限修飾符依然可以通過圖9的值查詢得到,方法名和方法的描述符都是常量池中的索引值,可以通過索引值在常量池中找到。而“方法的屬性”這一部分較為復雜,直接借助javap -verbose將其反編譯為人可以讀懂的信息進行解讀,如圖13所示??梢钥吹綄傩灾邪ㄒ韵氯齻€部分:
“Code區(qū)”:源代碼對應的JVM指令操作碼,在進行字節(jié)碼增強時重點操作的就是“Code區(qū)”這一部分。
“LineNumberTable”:行號表,將Code區(qū)的操作碼和源代碼中的行號對應,Debug時會起到作用(源代碼走一行,需要走多少個JVM指令操作碼)。
我們在上文所說的操作碼或者操作集合,其實控制的就是這個JVM的操作數(shù)棧。為了更直觀地感受操作碼是如何控制操作數(shù)棧的,以及理解常量池、變量表的作用,將add()方法的對操作數(shù)棧的操作制作為GIF,如下圖14所示,圖中僅截取了常量池中被引用的部分,以指令iconst_2開始到ireturn結(jié)束,與圖13中Code區(qū)0~17的指令一一對應:
圖15 jclasslib查看字節(jié)碼
在上文中,著重介紹了字節(jié)碼的結(jié)構(gòu),這為我們了解字節(jié)碼增強技術(shù)的實現(xiàn)打下了基礎(chǔ)。字節(jié)碼增強技術(shù)就是一類對現(xiàn)有字節(jié)碼進行修改或者動態(tài)生成全新字節(jié)碼文件的技術(shù)。接下來,我們將從最直接操縱字節(jié)碼的實現(xiàn)方式開始深入進行剖析。
圖16 字節(jié)碼增強技術(shù)
ClassReader:用于讀取已經(jīng)編譯好的.class文件。
ClassWriter:用于重新構(gòu)建編譯后的類,如修改類名、屬性以及方法,也可以生成新的類的字節(jié)碼文件。
各種Visitor類:如上所述,CoreAPI根據(jù)字節(jié)碼從上到下依次處理,對于字節(jié)碼文件中不同的區(qū)域有不同的Visitor,比如用于訪問方法的MethodVisitor、用于訪問類變量的FieldVisitor、用于訪問注解的AnnotationVisitor等。為了實現(xiàn)AOP,重點要使用的是MethodVisitor。
public class Base {
public void process(){
System.out.println("process");
}
}
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
public class Generator {
public static void main(String[] args) throws Exception {
//讀取
ClassReader classReader = new ClassReader("meituan/bytecode/asm/Base");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//處理
ClassVisitor classVisitor = new MyClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
//輸出
File f = new File("operation-server/target/classes/meituan/bytecode/asm/Base.class");
FileOutputStream fout = new FileOutputStream(f);
fout.write(data);
fout.close();
System.out.println("now generator cc success!!!!!");
}
}
MyClassVisitor繼承自ClassVisitor,用于對字節(jié)碼的觀察。它還包含一個內(nèi)部類MyMethodVisitor,繼承自MethodVisitor用于對類內(nèi)方法的觀察,整體代碼如下:
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class MyClassVisitor extends ClassVisitor implements Opcodes {
public MyClassVisitor(ClassVisitor cv) {
super(ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
//Base類中有兩個方法:無參構(gòu)造以及process方法,這里不增強構(gòu)造方法
if (!name.equals("<init>") && mv != null) {
mv = new MyMethodVisitor(mv);
}
return mv;
}
class MyMethodVisitor extends MethodVisitor implements Opcodes {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
|| opcode == Opcodes.ATHROW) {
//方法在返回之前,打印"end"
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}
}
}
利用這個類就可以實現(xiàn)對字節(jié)碼的修改。詳細解讀其中的代碼,對字節(jié)碼做修改的步驟是:
首先通過MyClassVisitor類中的visitMethod方法,判斷當前字節(jié)碼讀到哪一個方法了。跳過構(gòu)造方法"<init>"后,將需要被增強的方法交給內(nèi)部類MyMethodVisitor來進行處理。
接下來,進入內(nèi)部類MyMethodVisitor中的visitCode方法,它會在ASM開始訪問某一個方法的Code區(qū)時被調(diào)用,重寫visitCode方法,將AOP中的前置邏輯就放在這里。
MyMethodVisitor繼續(xù)讀取字節(jié)碼指令,每當ASM訪問到無參數(shù)指令時,都會調(diào)用MyMethodVisitor中的visitInsn方法。我們判斷了當前指令是否為無參數(shù)的“return”指令,如果是就在它的前面添加一些指令,也就是將AOP的后置邏輯放在該方法中。
綜上,重寫MyMethodVisitor中的兩個方法,就可以實現(xiàn)AOP了,而重寫方法時就需要用ASM的寫法,手動寫入或者修改字節(jié)碼。通過調(diào)用methodVisitor的visitXXXXInsn()方法就可以實現(xiàn)字節(jié)碼的插入,XXXX對應相應的操作碼助記符類型,比如mv.visitLdcInsn("end")對應的操作碼就是ldc "end",即將字符串“end”壓入棧。
完成這兩個Visitor類后,運行Generator中的main方法完成對Base類的字節(jié)碼增強,增強后的結(jié)果可以在編譯后的Target文件夾中找到Base.class文件進行查看,可以看到反編譯后的代碼已經(jīng)改變了(如圖18左側(cè)所示)。然后寫一個測試類MyTest,在其中new Base(),并調(diào)用base.process()方法,可以看到下圖右側(cè)所示的AOP實現(xiàn)效果:
圖18 ASM實現(xiàn)AOP的效果
CtClass(compile-time class):編譯時類信息,它是一個Class文件在代碼中的抽象表現(xiàn)形式,可以通過一個類的全限定名來獲取一個CtClass對象,用來表示這個類文件。
ClassPool:從開發(fā)視角來看,ClassPool是一張保存CtClass信息的HashTable,Key為類名,Value為類名對應的CtClass對象。當我們需要對某個類進行修改時,就是通過pool.getCtClass("className")方法從pool中獲取到相應的CtClass。
CtMethod、CtField:這兩個比較好理解,對應的是類中的方法和屬性。
import com.meituan.mtrace.agent.javassist.*;
public class JavassistTest {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, IOException {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("meituan.bytecode.javassist.Base");
CtMethod m = cc.getDeclaredMethod("process");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
Class c = cc.toClass();
cc.writeFile("/Users/zen/projects");
Base h = (Base)c.newInstance();
h.process();
}
}
3. 運行時類的重載
import java.lang.management.ManagementFactory;
public class Base {
public static void main(String[] args) {
String name = ManagementFactory.getRuntimeMXBean().getName();
String s = name.split("@")[0];
//打印當前Pid
System.out.println("pid:"+s);
while (true) {
try {
Thread.sleep(5000L);
} catch (Exception e) {
break;
}
process();
}
}
public static void process() {
System.out.println("process");
}
}
我們定義一個實現(xiàn)了ClassFileTransformer接口的類TestTransformer,依然在其中利用Javassist對Base類中的process()方法進行增強,在前后分別打印“start”和“end”,代碼如下:
import java.lang.instrument.ClassFileTransformer;
public class TestTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
System.out.println("Transforming " + className);
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("meituan.bytecode.jvmti.Base");
CtMethod m = cc.getDeclaredMethod("process");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
return cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
現(xiàn)在有了Transformer,那么它要如何注入到正在運行的JVM呢?還需要定義一個Agent,借助Agent的能力將Instrument注入到JVM中。我們將在下一小節(jié)介紹Agent,現(xiàn)在要介紹的是Agent中用到的另一個類Instrumentation。在JDK 1.6之后,Instrumentation可以做啟動后的Instrument、本地代碼(Native Code)的Instrument,以及動態(tài)改變Classpath等等。我們可以向Instrumentation中添加上文中定義的Transformer,并指定要被重加載的類,代碼如下所示。這樣,當Agent被Attach到一個JVM中時,就會執(zhí)行類字節(jié)碼替換并重載入JVM的操作。
import java.lang.instrument.Instrumentation;
public class TestAgent {
public static void agentmain(String args, Instrumentation inst) {
//指定我們自己定義的Transformer,在其中利用Javassist做字節(jié)碼替換
inst.addTransformer(new TestTransformer(), true);
try {
//重定義類并載入新的字節(jié)碼
inst.retransformClasses(Base.class);
System.out.println("Agent Load Done.");
} catch (Exception e) {
System.out.println("agent load failed!");
}
}
}
定義Agent,并在其中實現(xiàn)AgentMain方法,如上一小節(jié)中定義的代碼塊7中的TestAgent類;
圖22 Manifest.mf
import com.sun.tools.attach.VirtualMachine;
public class Attacher {
public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
// 傳入目標 JVM pid
VirtualMachine vm = VirtualMachine.attach("39333");
vm.loadAgent("/Users/zen/operation_server_jar/operation-server.jar");
}
}
以下為運行時重新載入類的效果:先運行Base中的main()方法,啟動一個JVM,可以在控制臺看到每隔五秒輸出一次"process"。接著執(zhí)行Attacher中的main()方法,并將上一個JVM的pid傳入。此時回到上一個main()方法的控制臺,可以看到現(xiàn)在每隔五秒輸出"process"前后會分別輸出"start"和"end",也就是說完成了運行時的字節(jié)碼增強,并重新載入了這個類。
熱部署:不部署服務而對線上服務做修改,可以做打點、增加日志等操作。
Mock:測試時候?qū)δ承┓兆鯩ock。
性能診斷工具:比如bTrace就是利用Instrument,實現(xiàn)無侵入地跟蹤一個正在運行的JVM,監(jiān)控到類和方法級別的狀態(tài)信息。
感謝各位的閱讀,以上就是“Java字節(jié)碼結(jié)構(gòu)是什么”的內(nèi)容了,經(jīng)過本文的學習后,相信大家對Java字節(jié)碼結(jié)構(gòu)是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。