Java字節碼,你還可以搲的更深一些!

                  上一篇 / 下一篇  2023-02-20 10:43:44

                    Java真的是長盛不衰,擁有頑強的生命力。其中,字節碼機制功不可沒。字節碼,就像是 Linux 的 ELF。有了它,JVM直接搖身一變,變成了類似操作系統的東西。
                    要學習字節碼,不能僅僅靠看枯燥的文檔。本文會介紹幾個有用的工具,可以非常容易的上手,來實際觀測class文件這個小魔獸,助你搲的更深一些。
                    1、字節碼結構
                    1.1、基本結構
                    在開始之前,我們先簡要的介紹一下class文件的內容。這個結構,可以使用jclasslib工具來查看。
                    上圖是class文件基本內容。這部分內容枯燥乏味,關于它的細節在Java的官方都能非常容易的找到。
                    如下圖,展示了一個簡單方法的字節碼描述,我們可以看到真正的執行指令在整個文件結構中的具體位置。
                    1.2、實際觀測
                    為了讓大家避免避免枯燥的二進制對比分析,直接定位到真正的數據結構,這里介紹一個小工具,使用這種方式學習字節碼會節省很多時間。
                    https://wiki.openjdk.java.net/display/CodeTools/asmtools
                    這個工具就是asmtools,執行下面的命令,將看到類的 JCED 語法結果。
                    java -jar asmtools-7.0.jar jdec LambdaDemo.class
                    輸出的結果類似于下面的結構,它與我們上面介紹的字節碼組成是一一對應的,對照官網或者書籍,學習速度飛快。
                    class LambdaDemo {
                      0xCAFEBABE;
                      0; // minor version
                      52; // version
                      [] { // Constant Pool
                        ; // first element is empty
                        Method #8 #25; // #1
                        InvokeDynamic 0s #30; // #2
                        InterfaceMethod #31 #32; // #3
                        Field #33 #34; // #4
                        String #35; // #5
                        Method #36 #37; // #6
                        class #38; // #7
                        class #39; // #8
                        Utf8 "<init>"; // #9
                        Utf8 "()V"; // #10
                        Utf8 "Code"; // #11
                    了解了類的文件組織方式,下面我們來看一下,類文件在加載到內存中以后,是一個什么表現形式。
                    2、內存表示
                    準備以下代碼,使用javac -g InvokeDemo.java進行編譯。然后使用java命令執行。程序將阻塞在sleep函數上,我們來看一下它的內存分布。
                    interface I {
                        default void infMethod() { }
                        void inf();
                    }
                    abstract class Abs {
                        abstract void abs();
                    }
                    public class InvokeDemo extends Abs implements I {
                        static void staticMethod() { }
                        private void privateMethod() { }
                        public void publicMethod() { }
                        @Override
                        public void inf() { }
                        @Override
                        void abs() { }
                        public static void main(String[] args) throws Exception{
                            InvokeDemo demo = new InvokeDemo();
                            InvokeDemo.staticMethod();
                            demo.abs();
                            ((Abs) demo).abs();
                            demo.inf();
                            ((I) demo).inf();
                            demo.privateMethod();
                            demo.publicMethod();
                            demo.infMethod();
                            ((I) demo).infMethod();
                            Thread.sleep(Integer.MAX_VALUE);
                        }
                    }
                    為了更加明顯的看到這個過程,下面介紹一下 jhsdb 這個工具,這是在 Java9 之后 JDK 先加入的調試工具,我們可以在命令行使用 jhsdb hsdb 來啟動它。注意,要加載相應的進程時,必須確保是同一個版本的應用進程,否則會產生報錯。
                    attach啟動后的Java進程后,可以在 Class Browser 菜單查看加載的所有類信息。我們在搜索框輸入 InvokeDemo,找到要查看的類。
                    @符號后面的,就是具體的內存地址,我們可以復制一個,然后在Inspector 視圖查看具體的屬性?梢源篌w認為這就是類在方法區的具體存儲。
                    在Inspector視圖中,我們找到方法相關的屬性 _methods,可惜的是它無法點開,也無法查看。
                    接下來可以使用命令行來檢查這個數組里面的值。打開菜單中Console,然后輸入examine命令?梢钥吹竭@個數組里的內容,對應的地址就是Class視圖中的方法地址。
                    examine 0x000000010e650570/10
                    我們可以在Inspect視圖看到方法所對應的內存信息,這確實是一個Method方法的表示。
                    相比較起來,對象就簡單的,它只需要保存一個到達Class對象的指針即可。我們需要先從對象視圖進入,然后找到它,一步步進入Inspect視圖。
                    由以上的這些分析,我們可以得出下面這張圖。執行引擎想要運行某個對象的方法,需要先在棧上找到這個對象的引用,然后再通過的對象的指針,找到相應的方法字節碼。
                    3、方法調用指令
                    關于方法的調用,Java一共提供了5個指令,用來調用不同類型的函數。
                    ·invokestatic 
                    · invokevirtual 
                    · invokeinterface 和上面這條指令類似,不過是作用于接口類。
                    · invokespecial 用于調用私有實例方法、構造器,以及super關鍵字等。
                    · invokedynamic 用于調用動態方法。
                    我們依然使用上面的代碼片段看一下前四個指令的使用場景。代碼中包含一個接口I,一個抽象類Abs,一個實現和繼承了兩者的類InvokeDemo。
                    參考Java的類加載機制,在class文件被加載到方法區以后,就完成了從符號引用到具體地址的轉換過程。
                    我們可以看一下編譯后的main方法字節碼。尤其需要注意的是對于接口方法的調用。使用實例對象直接調用,和強制轉化成接口調用,所調用的字節碼指令分別是 invokevirtual 和invokeinterface,它們是不同的。
                    public static void main(java.lang.String[]);
                        descriptor: ([Ljava/lang/String;)V
                        flags: ACC_PUBLIC, ACC_STATIC
                        Code:
                          stack=2, locals=2, args_size=1
                             0: new           #2                  // class InvokeDemo
                             3: dup
                             4: invokespecial #3                  // Method "<init>":()V
                             7: astore_1
                             8: invokestatic  #4                  // Method staticMethod:()V
                            11: aload_1
                            12: invokevirtual #5                  // Method abs:()V
                            15: aload_1
                            16: invokevirtual #6                  // Method Abs.abs:()V
                            19: aload_1
                            20: invokevirtual #7                  // Method inf:()V
                            23: aload_1
                            24: invokeinterface #8,  1            // InterfaceMethod I.inf:()V
                            29: aload_1
                            30: invokespecial #9                  // Method privateMethod:()V
                            33: aload_1
                            34: invokevirtual #10                 // Method publicMethod:()V
                            37: aload_1
                            38: invokevirtual #11                 // Method infMethod:()V
                            41: aload_1
                            42: invokeinterface #12,  1           // InterfaceMethod I.infMethod:()V
                            47: return
                    另外還有一點,和我們想象中的不同,大多數普通方法調用,使用的是 invokevirtual 指令,它其實是和invokeinterface 一類的,都屬于虛方法調用。很多時候,JVM需要根據調用者的動態類型,來確定調用的目標方法,這就是動態綁定的過程。
                    invokevirtual指令有多態查找的機制,該指令的運行時解析過程步驟如下:
                    找到操作數棧頂的第一個元素所指向的對象的實際類型,記做c。
                    如果在類型c中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法的直接引用,查找過程結束,不通過則返回java.lang.IllegalAccessError。
                    否則,按照繼承關系從下往上依次對c的各個父類進行第二步的搜索和驗證過程。
                    始終沒找到合適的方法,拋出java.lang.AbstractMethodError異常。這就是java語言中方法重寫的本質。
                    相對比,invokestatic指令,加上invokespecial指令,就屬于靜態綁定過程。
                    所以靜態綁定,指的是能夠直接識別目標方法的情況,而動態綁定指的是需要在運行過程中根據調用者的類型來確定目標方法的情況。
                    可以想象,相對于靜態綁定的方法調用來說,動態綁定的調用就更加耗時一些。由于方法的調用非常的頻繁,JVM對動態調用的代碼進行了比較多的優化。比如使用方法表來加快對具體方法的尋址,以及使用更快的緩沖區來直接尋址( 內聯緩存)。
                    4、invokedynamic
                    有時候在寫一些python腳本或者js腳本的時候,會特別羨慕這些動態語言。如果把查找目標方法的決定權,從虛擬機轉嫁給用戶代碼,我們就會有更高的自由度。
                    我們單獨把invokedynamic抽離出來介紹,是因為它比較復雜。和反射類似,它用于一些動態的調用場景,但它和反射有著本質的不同,效率也比反射要高的多。
                    這個指令通常在lambda語法中出現,我們來看一下一小段代碼。
                    public class LambdaDemo {
                        public static void main(String[] args) {
                            Runnable r = () -> System.out.println("Hello Lambda");
                            r.run();
                        }
                    }
                    使用javap -p -v 命令可以在main方法中看到invokedynamic指令。
                    public static void main(java.lang.String[]);
                        descriptor: ([Ljava/lang/String;)V
                        flags: ACC_PUBLIC, ACC_STATIC
                        Code:
                          stack=1, locals=2, args_size=1
                             0: invokedynamic #2,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
                             5: astore_1
                             6: aload_1
                             7: invokeinterface #3,  1            // InterfaceMethod java/lang/Runnable.run:()V
                            12: return
                    另外,我們在javap的輸出中找到了一些奇怪的東西。
                    BootstrapMethods:
                      0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
                        Method arguments:
                          #28 ()V
                          #29 invokestatic LambdaDemo.lambda$main$0:()V
                          #28 ()V
                    BootstrapMethods屬性在Java1.7以后才有,位于類文件的屬性列表中,這個屬性用于保存 invokedynamic 指令引用的引導方法限定符。
                    和上面介紹的四個指令不同,invokedynamic并沒有確切的接收對象,取而代之的,是一個叫做 CallSite 的對象。
                    static CallSite bootstrap(MethodHandles.Lookup caller, String name, MethodType type);
                    其實,invokedynamic指令的底層,是使用方法句柄(MethodHandle)來實現的。方法句柄是一個能夠被執行的引用,它可以指向靜態方法和實例方法,以及虛構的get和set方法,從IDE中可以看到這些函數。
                    句柄類型(MethodType)就是我們對方法的具體描述,配合方法名稱,能夠定位到一類函數。訪問方法句柄和調用原來的指令是基本一致的,但它的調用異常,包括一些權限檢查,是在運行時才能被發現的。
                    lambda語言實際上是通過方法句柄來完成的,在調用鏈上自然也多了一些調用步驟,那么在性能上,是否就意味著lambda性能低呢?對于大部分“非捕獲”的lambda表達式來說,JIT編譯器的逃逸分析能夠優化這部分差異,性能和傳統方式無異;但對于“捕獲型”的表達式來說,就需要通過方法句柄,不斷的生成適配器,性能自然就低了很多(不過和便捷性相比,一丁點性能損失是可接受的)。
                    除了lambda表達式,我們還沒有其他的方式來產生invokedynamic指令。但是我們可以使用一些外部的字節碼修改工具,比如ASM,來生成一些帶有這個指令的字節碼,這通常能夠完成一些非?岬墓δ,比如完成一門弱類型檢查的JVM-Base語言。
                    END
                    本文從Java字節碼的頂層結構介紹開始,通過一個實際代碼,了解了類加載以后,在JVM內存里的表現形式,并了解了jhsdb對Java進程的觀測方式。
                    我們了解到Java7之后的invokedynamic指令,它實際上是通過方法句柄來實現的。和我們關系最大的就是Lambda語法,了解了這些原理,可以忽略那些對Lambda性能高低的爭論,要盡量寫一些“非捕獲”的Lambda表達式。
                    什么?你問什么叫非捕獲?那就需要你自己搲了。

                  TAG: 軟件開發 Java java

                   

                  評分:0

                  我來說兩句

                  日歷

                  « 2023-04-02  
                        1
                  2345678
                  9101112131415
                  16171819202122
                  23242526272829
                  30      

                  數據統計

                  • 訪問量: 114807
                  • 日志數: 546
                  • 建立時間: 2020-08-11
                  • 更新時間: 2023-03-31

                  RSS訂閱

                  Open Toolbar
                  亚洲欧洲自拍图片专区123_久久久精品人妻无码专区不卡_青青精品视频国产色天使_A免看的日黄亚洲