溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么

發(fā)布時(shí)間:2022-10-12 10:21:08 來(lái)源:億速云 閱讀:71 作者:iii 欄目:開(kāi)發(fā)技術(shù)

本篇內(nèi)容主要講解“JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么”吧!

    字節(jié)碼增強(qiáng)技術(shù)

    字節(jié)碼增強(qiáng)技術(shù)就是一類對(duì)現(xiàn)有字節(jié)碼進(jìn)行修改或者動(dòng)態(tài)生成全新字節(jié)碼文件的技術(shù)。接下來(lái),我們將從最直接操縱字節(jié)碼的實(shí)現(xiàn)方式開(kāi)始深入進(jìn)行剖析

    JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么

    ASM

    對(duì)于需要手動(dòng)操縱字節(jié)碼的需求,可以使用ASM,它可以直接生產(chǎn) .class字節(jié)碼文件,也可以在類被加載入JVM之前動(dòng)態(tài)修改類行為(如下圖17所示)。ASM的應(yīng)用場(chǎng)景有AOP(Cglib就是基于ASM)、熱部署、修改其他jar包中的類等。當(dāng)然,涉及到如此底層的步驟,實(shí)現(xiàn)起來(lái)也比較麻煩。接下來(lái),本文將介紹ASM的兩種API,并用ASM來(lái)實(shí)現(xiàn)一個(gè)比較粗糙的AOP。但在此之前,為了讓大家更快地理解ASM的處理流程,強(qiáng)烈建議讀者先對(duì)訪問(wèn)者模式進(jìn)行了解。簡(jiǎn)單來(lái)說(shuō),訪問(wèn)者模式主要用于修改或操作一些數(shù)據(jù)結(jié)構(gòu)比較穩(wěn)定的數(shù)據(jù),而通過(guò)第一章,我們知道字節(jié)碼文件的結(jié)構(gòu)是由JVM固定的,所以很適合利用訪問(wèn)者模式對(duì)字節(jié)碼文件進(jìn)行修改。

    JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么

    ASM API

    核心API

    ASM Core API可以類比解析XML文件中的SAX方式,不需要把這個(gè)類的整個(gè)結(jié)構(gòu)讀取進(jìn)來(lái),就可以用流式的方法來(lái)處理字節(jié)碼文件。好處是非常節(jié)約內(nèi)存,但是編程難度較大。然而出于性能考慮,一般情況下編程都使用Core API。在Core API中有以下幾個(gè)關(guān)鍵類:

    • ClassReader:用于讀取已經(jīng)編譯好的.class文件。

    • ClassWriter:用于重新構(gòu)建編譯后的類,如修改類名、屬性以及方法,也可以生成新的類的字節(jié)碼文件。

    • 各種Visitor類:如上所述,CoreAPI根據(jù)字節(jié)碼從上到下依次處理,對(duì)于字節(jié)碼文件中不同的區(qū)域有不同的Visitor,比如用于訪問(wèn)方法的MethodVisitor、用于訪問(wèn)類變量的FieldVisitor、用于訪問(wèn)注解的AnnotationVisitor等。為了實(shí)現(xiàn)AOP,重點(diǎn)要使用的是MethodVisitor。

    樹(shù)形API

    ASM Tree API可以類比解析XML文件中的DOM方式,把整個(gè)類的結(jié)構(gòu)讀取到內(nèi)存中,缺點(diǎn)是消耗內(nèi)存多,但是編程比較簡(jiǎn)單。TreeApi不同于CoreAPI,TreeAPI通過(guò)各種Node類來(lái)映射字節(jié)碼的各個(gè)區(qū)域,類比DOM節(jié)點(diǎn),就可以很好地理解這種編程方式。

    直接利用ASM實(shí)現(xiàn)AOP

    利用ASM的CoreAPI來(lái)增強(qiáng)類。這里不糾結(jié)于AOP的專業(yè)名詞如切片、通知,只實(shí)現(xiàn)在方法調(diào)用前、后增加邏輯,通俗易懂且方便理解。首先定義需要被增強(qiáng)的Base類:其中只包含一個(gè)process()方法,方法內(nèi)輸出一行“process”。增強(qiáng)后,我們期望的是,方法執(zhí)行前輸出“start”,之后輸出”end”。

    public class Base {
        public void process(){
            System.out.println("process");
        }
    }

    為了利用ASM實(shí)現(xiàn)AOP,需要定義兩個(gè)類:一個(gè)是MyClassVisitor類,用于對(duì)字節(jié)碼的visit以及修改;另一個(gè)是Generator類,在這個(gè)類中定義ClassReader和ClassWriter,其中的邏輯是,classReader讀取字節(jié)碼,然后交給MyClassVisitor類處理,處理完成后由ClassWriter寫(xiě)字節(jié)碼并將舊的字節(jié)碼替換掉。Generator類較簡(jiǎn)單,我們先看一下它的實(shí)現(xiàn),如下所示,然后重點(diǎn)解釋MyClassVisitor類。

    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,用于對(duì)字節(jié)碼的觀察。它還包含一個(gè)內(nèi)部類MyMethodVisitor,繼承自MethodVisitor用于對(duì)類內(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è)方法:無(wú)參構(gòu)造以及process方法,這里不增強(qiáng)構(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);
            }
        }
    }

    利用這個(gè)類就可以實(shí)現(xiàn)對(duì)字節(jié)碼的修改。詳細(xì)解讀其中的代碼,對(duì)字節(jié)碼做修改的步驟是:

    • 首先通過(guò)MyClassVisitor類中的visitMethod方法,判斷當(dāng)前字節(jié)碼讀到哪一個(gè)方法了。跳過(guò)構(gòu)造方法 <init> 后,將需要被增強(qiáng)的方法交給內(nèi)部類MyMethodVisitor來(lái)進(jìn)行處理。

    • 接下來(lái),進(jìn)入內(nèi)部類MyMethodVisitor中的visitCode方法,它會(huì)在ASM開(kāi)始訪問(wèn)某一個(gè)方法的Code區(qū)時(shí)被調(diào)用,重寫(xiě)visitCode方法,將AOP中的前置邏輯就放在這里。 MyMethodVisitor繼續(xù)讀取字節(jié)碼指令,每當(dāng)ASM訪問(wèn)到無(wú)參數(shù)指令時(shí),都會(huì)調(diào)用MyMethodVisitor中的visitInsn方法。我們判斷了當(dāng)前指令是否為無(wú)參數(shù)的“return”指令,如果是就在它的前面添加一些指令,也就是將AOP的后置邏輯放在該方法中。

    • 綜上,重寫(xiě)MyMethodVisitor中的兩個(gè)方法,就可以實(shí)現(xiàn)AOP了,而重寫(xiě)方法時(shí)就需要用ASM的寫(xiě)法,手動(dòng)寫(xiě)入或者修改字節(jié)碼。通過(guò)調(diào)用methodVisitor的visitXXXXInsn()方法就可以實(shí)現(xiàn)字節(jié)碼的插入,XXXX對(duì)應(yīng)相應(yīng)的操作碼助記符類型,比如mv.visitLdcInsn(“end”)對(duì)應(yīng)的操作碼就是ldc “end”,即將字符串“end”壓入棧。 完成這兩個(gè)visitor類后,運(yùn)行Generator中的main方法完成對(duì)Base類的字節(jié)碼增強(qiáng),增強(qiáng)后的結(jié)果可以在編譯后的target文件夾中找到Base.class文件進(jìn)行查看,可以看到反編譯后的代碼已經(jīng)改變了。然后寫(xiě)一個(gè)測(cè)試類MyTest,在其中new Base(),并調(diào)用base.process()方法,可以看到下圖右側(cè)所示的AOP實(shí)現(xiàn)效果:

    JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么

    ASM工具

    利用ASM手寫(xiě)字節(jié)碼時(shí),需要利用一系列visitXXXXInsn()方法來(lái)寫(xiě)對(duì)應(yīng)的助記符,所以需要先將每一行源代碼轉(zhuǎn)化為一個(gè)個(gè)的助記符,然后通過(guò)ASM的語(yǔ)法轉(zhuǎn)換為visitXXXXInsn()這種寫(xiě)法。第一步將源碼轉(zhuǎn)化為助記符就已經(jīng)夠麻煩了,不熟悉字節(jié)碼操作集合的話,需要我們將代碼編譯后再反編譯,才能得到源代碼對(duì)應(yīng)的助記符。第二步利用ASM寫(xiě)字節(jié)碼時(shí),如何傳參也很令人頭疼。ASM社區(qū)也知道這兩個(gè)問(wèn)題,所以提供了工具ASM ByteCode Outline (opens new window)。

    安裝后,右鍵選擇“Show Bytecode Outline”,在新標(biāo)簽頁(yè)中選擇“ASMified”這個(gè)tab,如圖19所示,就可以看到這個(gè)類中的代碼對(duì)應(yīng)的ASM寫(xiě)法了。圖中上下兩個(gè)紅框分別對(duì)應(yīng)AOP中的前置邏輯于后置邏輯,將這兩塊直接復(fù)制到visitor中的visitMethod()以及visitInsn()方法中,就可以了。

    JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么

    Javassist

    ASM是在指令層次上操作字節(jié)碼的,閱讀上文后,我們的直觀感受是在指令層次上操作字節(jié)碼的框架實(shí)現(xiàn)起來(lái)比較晦澀。故除此之外,我們?cè)俸?jiǎn)單介紹另外一類框架:強(qiáng)調(diào)源代碼層次操作字節(jié)碼的框架Javassist。

    利用Javassist實(shí)現(xiàn)字節(jié)碼增強(qiáng)時(shí),可以無(wú)須關(guān)注字節(jié)碼刻板的結(jié)構(gòu),其優(yōu)點(diǎn)就在于編程簡(jiǎn)單。直接使用java編碼的形式,而不需要了解虛擬機(jī)指令,就能動(dòng)態(tài)改變類的結(jié)構(gòu)或者動(dòng)態(tài)生成類。其中最重要的是ClassPool、CtClass、CtMethod、CtField這四個(gè)類:

    • CtClass(compile-time class):編譯時(shí)類信息,它是一個(gè)class文件在代碼中的抽象表現(xiàn)形式,可以通過(guò)一個(gè)類的全限定名來(lái)獲取一個(gè)CtClass對(duì)象,用來(lái)表示這個(gè)類文件。

    • ClassPool:從開(kāi)發(fā)視角來(lái)看,ClassPool是一張保存CtClass信息的HashTable,key為類名,value為類名對(duì)應(yīng)的CtClass對(duì)象。當(dāng)我們需要對(duì)某個(gè)類進(jìn)行修改時(shí),就是通過(guò)pool.getCtClass(“className”)方法從pool中獲取到相應(yīng)的CtClass。

    • CtMethod、CtField:這兩個(gè)比較好理解,對(duì)應(yīng)的是類中的方法和屬性。

    了解這四個(gè)類后,我們可以寫(xiě)一個(gè)小Demo來(lái)展示Javassist簡(jiǎn)單、快速的特點(diǎn)。我們依然是對(duì)Base中的process()方法做增強(qiáng),在方法調(diào)用前后分別輸出”start”和”end”,實(shí)現(xiàn)代碼如下。我們需要做的就是從pool中獲取到相應(yīng)的CtClass對(duì)象和其中的方法,然后執(zhí)行method.insertBefore和insertAfter方法,參數(shù)為要插入的Java代碼,再以字符串的形式傳入即可,實(shí)現(xiàn)起來(lái)也極為簡(jiǎn)單。

    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();
        }
    }

    運(yùn)行時(shí)類的重載

    問(wèn)題引出

    上一章重點(diǎn)介紹了兩種不同類型的字節(jié)碼操作框架,且都利用它們實(shí)現(xiàn)了較為粗糙的AOP。其實(shí),為了方便大家理解字節(jié)碼增強(qiáng)技術(shù),在上文中我們避重就輕將ASM實(shí)現(xiàn)AOP的過(guò)程分為了兩個(gè)main方法:第一個(gè)是利用MyClassVisitor對(duì)已編譯好的class文件進(jìn)行修改,第二個(gè)是new對(duì)象并調(diào)用。這期間并不涉及到JVM運(yùn)行時(shí)對(duì)類的重加載,而是在第一個(gè)main方法中,通過(guò)ASM對(duì)已編譯類的字節(jié)碼進(jìn)行替換,在第二個(gè)main方法中,直接使用已替換好的新類信息。另外在Javassist的實(shí)現(xiàn)中,我們也只加載了一次Base類,也不涉及到運(yùn)行時(shí)重加載類。

    如果我們?cè)谝粋€(gè)JVM中,先加載了一個(gè)類,然后又對(duì)其進(jìn)行字節(jié)碼增強(qiáng)并重新加載會(huì)發(fā)生什么呢?模擬這種情況,只需要我們?cè)谏衔闹蠮avassist的Demo中main()方法的第一行添加Base b=new Base(),即在增強(qiáng)前就先讓JVM加載Base類,然后在執(zhí)行到c.toClass()方法時(shí)會(huì)拋出錯(cuò)誤,如下圖20所示。跟進(jìn)c.toClass()方法中,我們會(huì)發(fā)現(xiàn)它是在最后調(diào)用了ClassLoader的native方法defineClass()時(shí)報(bào)錯(cuò)。也就是說(shuō),JVM是不允許在運(yùn)行時(shí)動(dòng)態(tài)重載一個(gè)類的。

    JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么

    顯然,如果只能在類加載前對(duì)類進(jìn)行強(qiáng)化,那字節(jié)碼增強(qiáng)技術(shù)的使用場(chǎng)景就變得很窄了。我們期望的效果是:在一個(gè)持續(xù)運(yùn)行并已經(jīng)加載了所有類的JVM中,還能利用字節(jié)碼增強(qiáng)技術(shù)對(duì)其中的類行為做替換并重新加載。為了模擬這種情況,我們將Base類做改寫(xiě),在其中編寫(xiě)main方法,每五秒調(diào)用一次process()方法,在process()方法中輸出一行“process”。

    我們的目的就是,在JVM運(yùn)行中的時(shí)候,將process()方法做替換,在其前后分別打印“start”和“end”。也就是在運(yùn)行中時(shí),每五秒打印的內(nèi)容由”process”變?yōu)榇蛴 眘tart process end”。那如何解決JVM不允許運(yùn)行時(shí)重加載類信息的問(wèn)題呢?為了達(dá)到這個(gè)目的,我們接下來(lái)一一來(lái)介紹需要借助的Java類庫(kù)。

    import java.lang.management.ManagementFactory;
    
    public class Base {
        public static void main(String[] args) {
            String name = ManagementFactory.getRuntimeMXBean().getName();
            String s = name.split("@")[0];
            //打印當(dāng)前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");
        }
    }

    Instrument

    instrument是JVM提供的一個(gè)可以修改已加載類的類庫(kù),專門(mén)為Java語(yǔ)言編寫(xiě)的插樁服務(wù)提供支持。它需要依賴JVMTI的Attach API機(jī)制實(shí)現(xiàn),JVMTI這一部分,我們將在下一小節(jié)進(jìn)行介紹。在JDK 1.6以前,instrument只能在JVM剛啟動(dòng)開(kāi)始加載類時(shí)生效,而在JDK 1.6之后,instrument支持了在運(yùn)行時(shí)對(duì)類定義的修改。要使用instrument的類修改功能,我們需要實(shí)現(xiàn)它提供的ClassFileTransformer接口,定義一個(gè)類文件轉(zhuǎn)換器。接口中的transform()方法會(huì)在類文件被加載時(shí)調(diào)用,而在transform方法里,我們可以利用上文中的ASM或Javassist對(duì)傳入的字節(jié)碼進(jìn)行改寫(xiě)或替換,生成新的字節(jié)碼數(shù)組后返回。

    我們定義一個(gè)實(shí)現(xiàn)了ClassFileTransformer接口的類TestTransformer,依然在其中利用Javassist對(duì)Base類中的process()方法進(jìn)行增強(qiáng),在前后分別打印“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,那么它要如何注入到正在運(yùn)行的JVM呢?還需要定義一個(gè)Agent,借助Agent的能力將Instrument注入到JVM中。我們將在下一小節(jié)介紹Agent,現(xiàn)在要介紹的是Agent中用到的另一個(gè)類Instrumentation。在JDK 1.6之后,Instrumentation可以做啟動(dòng)后的Instrument、本地代碼(Native Code)的Instrument,以及動(dòng)態(tài)改變Classpath等等。我們可以向Instrumentation中添加上文中定義的Transformer,并指定要被重加載的類,代碼如下所示。這樣,當(dāng)Agent被Attach到一個(gè)JVM中時(shí),就會(huì)執(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!");
            }
        }
    }

    JVMTI & Agent & Attach API

    上一小節(jié)中,我們給出了Agent類的代碼,追根溯源需要先介紹JPDA(Java Platform Debugger Architecture)。如果JVM啟動(dòng)時(shí)開(kāi)啟了JPDA,那么類是允許被重新加載的。在這種情況下,已被加載的舊版本類信息可以被卸載,然后重新加載新版本的類。正如JDPA名稱中的Debugger,JDPA其實(shí)是一套用于調(diào)試Java程序的標(biāo)準(zhǔn),任何JDK都必須實(shí)現(xiàn)該標(biāo)準(zhǔn)。

    JPDA定義了一整套完整的體系,它將調(diào)試體系分為三部分,并規(guī)定了三者之間的通信接口。三部分由低到高分別是Java 虛擬機(jī)工具接口(JVMTI),Java 調(diào)試協(xié)議(JDWP)以及 Java 調(diào)試接口(JDI),三者之間的關(guān)系如下圖所示:

    JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么

    現(xiàn)在回到正題,我們可以借助JVMTI的一部分能力,幫助動(dòng)態(tài)重載類信息。JVM TI(JVM TOOL INTERFACE,JVM工具接口)是JVM提供的一套對(duì)JVM進(jìn)行操作的工具接口。通過(guò)JVMTI,可以實(shí)現(xiàn)對(duì)JVM的多種操作,它通過(guò)接口注冊(cè)各種事件勾子,在JVM事件觸發(fā)時(shí),同時(shí)觸發(fā)預(yù)定義的勾子,以實(shí)現(xiàn)對(duì)各個(gè)JVM事件的響應(yīng),事件包括類文件加載、異常產(chǎn)生與捕獲、線程啟動(dòng)和結(jié)束、進(jìn)入和退出臨界區(qū)、成員變量修改、GC開(kāi)始和結(jié)束、方法調(diào)用進(jìn)入和退出、臨界區(qū)競(jìng)爭(zhēng)與等待、VM啟動(dòng)與退出等等。

    而Agent就是JVMTI的一種實(shí)現(xiàn),Agent有兩種啟動(dòng)方式,一是隨Java進(jìn)程啟動(dòng)而啟動(dòng),經(jīng)常見(jiàn)到的java -agentlib就是這種方式;二是運(yùn)行時(shí)載入,通過(guò)attach API,將模塊(jar包)動(dòng)態(tài)地Attach到指定進(jìn)程id的Java進(jìn)程內(nèi)。

    Attach API 的作用是提供JVM進(jìn)程間通信的能力,比如說(shuō)我們?yōu)榱俗屃硗庖粋€(gè)JVM進(jìn)程把線上服務(wù)的線程Dump出來(lái),會(huì)運(yùn)行jstack或jmap的進(jìn)程,并傳遞pid的參數(shù),告訴它要對(duì)哪個(gè)進(jìn)程進(jìn)行線程Dump,這就是Attach API做的事情。在下面,我們將通過(guò)Attach API的loadAgent()方法,將打包好的Agent jar包動(dòng)態(tài)Attach到目標(biāo)JVM上。具體實(shí)現(xiàn)起來(lái)的步驟如下:

    • 定義Agent,并在其中實(shí)現(xiàn)AgentMain方法,如上一小節(jié)中定義的代碼塊7中的TestAgent類;

    • 然后將TestAgent類打成一個(gè)包含MANIFEST.MF的jar包,其中MANIFEST.MF文件中將Agent-Class屬性指定為T(mén)estAgent的全限定名,如下圖所示;

    JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么

    最后利用Attach API,將我們打包好的jar包Attach到指定的JVM pid上,代碼如下:

    import com.sun.tools.attach.VirtualMachine;
    
    public class Attacher {
        public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
            // 傳入目標(biāo) JVM pid
            VirtualMachine vm = VirtualMachine.attach("39333");
            vm.loadAgent("/Users/zen/operation_server_jar/operation-server.jar");
        }
    }

    由于在MANIFEST.MF中指定了Agent-Class,所以在Attach后,目標(biāo)JVM在運(yùn)行時(shí)會(huì)走到TestAgent類中定義的agentmain()方法,而在這個(gè)方法中,我們利用Instrumentation,將指定類的字節(jié)碼通過(guò)定義的類轉(zhuǎn)化器TestTransformer做了Base類的字節(jié)碼替換(通過(guò)javassist),并完成了類的重新加載。由此,我們達(dá)成了“在JVM運(yùn)行時(shí),改變類的字節(jié)碼并重新載入類信息”的目的。

    以下為運(yùn)行時(shí)重新載入類的效果:先運(yùn)行Base中的main()方法,啟動(dòng)一個(gè)JVM,可以在控制臺(tái)看到每隔五秒輸出一次”process”。接著執(zhí)行Attacher中的main()方法,并將上一個(gè)JVM的pid傳入。此時(shí)回到上一個(gè)main()方法的控制臺(tái),可以看到現(xiàn)在每隔五秒輸出”process”前后會(huì)分別輸出”start”和”end”,也就是說(shuō)完成了運(yùn)行時(shí)的字節(jié)碼增強(qiáng),并重新載入了這個(gè)類。

    JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么

    使用場(chǎng)景

    至此,字節(jié)碼增強(qiáng)技術(shù)的可使用范圍就不再局限于JVM加載類前了。通過(guò)上述幾個(gè)類庫(kù),我們可以在運(yùn)行時(shí)對(duì)JVM中的類進(jìn)行修改并重載了。通過(guò)這種手段,可以做的事情就變得很多了:

    • 熱部署:不部署服務(wù)而對(duì)線上服務(wù)做修改,可以做打點(diǎn)、增加日志等操作。

    • Mock:測(cè)試時(shí)候?qū)δ承┓?wù)做Mock。

    • 性能診斷工具:比如bTrace就是利用Instrument,實(shí)現(xiàn)無(wú)侵入地跟蹤一個(gè)正在運(yùn)行的JVM,監(jiān)控到類和方法級(jí)別的狀態(tài)信息。

    到此,相信大家對(duì)“JVM基礎(chǔ)之字節(jié)碼的增強(qiáng)技術(shù)是什么”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

    向AI問(wèn)一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    jvm
    AI