Java熱加載?操作class字節碼理論知識

2020-12-16 碼農登陸

前言

今天無意看到美團技術團隊的一篇文章,感覺非常的有意思,所以自己整了一篇文章,一起給大家分享一下:

正文

對於我們Java語言的開發者來說,下面的對話應該很熟悉:

Java的對象行為(方法、函數)是存儲在方法區的。

「方法區中的數據從哪來?」

「方法區中的數據是類加載時從class文件中提取出來的。」

「class文件從哪來?」

「從Java或者其他符合JVM規範的原始碼中編譯而來。」

「原始碼從哪來?」

「廢話,當然是手寫!」

這裡為啥要有這樣的對話?因為這裡討論一個有趣的問題:

在JSP時代,我們都有過這種經歷:修完Java代碼,對於瀏覽器來說,只需要刷新網頁就可以看到效果。按我們正常的思維。對於java程序來說,哪怕是Hello world也需要重新編譯然後重新run。那麼JSP是如何做的類似」熱更新「的呢?

讓我們繼續上面的對話:

「倒著推,手寫沒問題,編譯沒問題,至於加載……有沒有辦法加載一個已經加載過的類呢?如果有的話,我們就能修改字節碼中目標方法所在的區域,然後重新加載這個類,這樣方法區中的對象行為(方法)就被改變了,而且不改變對象的屬性,也不影響已經存在對象的狀態,那麼就可以搞定這個問題了。可是,這豈不是違背了JVM的類加載原理?畢竟我們不想改變ClassLoader。」

「少年,可以去看看java.lang.instrument.Instrumentation。」

java.lang.instrument.Instrumentation

看完文檔之後,我們發現這麼兩個接口:redefineClasses和retransformClasses。一個是重新定義class,一個是修改class。這兩個大同小異,看reDefineClasses的說明:

This method is used to replace the definition of a class without reference to the existing class file bytes, as one might do when recompiling from source for fix-and-continue debugging. Where the existing class file bytes are to be transformed (for example in bytecode instrumentation) retransformClasses should be used.

都是替換已經存在的class文件,redefineClasses是自己提供字節碼文件替換掉已存在的class文件,retransformClasses是在已存在的字節碼文件上修改後再替換之。

當然,運行時直接替換類很不安全。比如新的class文件引用了一個不存在的類,或者把某個類的一個field給刪除了等等,這些情況都會引發異常。所以如文檔中所言,instrument存在諸多的限制:

The redefinition may change method bodies, the constant pool and attributes. The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions. The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception.

我們能做的基本上也就是簡單修改方法內的一些行為,這對於我們開頭的問題,列印一段日誌來說,已經足夠了。當然,我們除了通過reTransform來列印日誌,還能做很多其他非常有用的事情,這個下文會進行介紹。

那怎麼得到我們需要的class文件呢?一個最簡單的方法,是把修改後的Java文件重新編譯一遍得到class文件,然後調用redefineClasses替換。但是對於沒有(或者拿不到,或者不方便修改)源碼的文件我們應該怎麼辦呢?其實對於JVM來說,不管是Java也好,Scala也好,任何一種符合JVM規範的語言的原始碼,都可以編譯成class文件。JVM的操作對象是class文件,而不是源碼。所以,從這種意義上來講,我們可以說「JVM跟語言無關」。既然如此,不管有沒有源碼,其實我們只需要修改class文件就行了。

直接操作字節碼

Java是軟體開發人員能讀懂的語言,class字節碼是JVM能讀懂的語言,class字節碼最終會被JVM解釋成機器能讀懂的語言。無論哪種語言,都是人創造的。所以,理論上(實際上也確實如此)人能讀懂上述任何一種語言,既然能讀懂,自然能修改。只要我們願意,我們完全可以跳過Java編譯器,直接寫字節碼文件,只不過這並不符合時代的發展罷了,畢竟高級語言設計之始就是為我們人類所服務,其開發效率也比機器語言高很多。

對於人類來說,字節碼文件的可讀性遠遠沒有Java代碼高。儘管如此,還是有一些傑出的程式設計師們創造出了可以用來直接編輯字節碼的框架,提供接口可以讓我們方便地操作字節碼文件,進行注入修改類的方法,動態創造一個新的類等等操作。其中最著名的框架應該就是ASM了,cglib、Spring等框架中對於字節碼的操作就建立在ASM之上。

我們都知道,Spring的AOP是基於動態代理實現的,Spring會在運行時動態創建代理類,代理類中引用被代理類,在被代理的方法執行前後進行一些神秘的操作。那麼,Spring是怎麼在運行時創建代理類的呢?動態代理的美妙之處,就在於我們不必手動為每個需要被代理的類寫代理類代碼,Spring在運行時會根據需要動態地創造出一個類,這裡創造的過程並非通過字符串寫Java文件,然後編譯成class文件,然後加載。Spring會直接「創造」一個class文件,然後加載,創造class文件的工具,就是ASM了。

到這裡,我們知道了用ASM框架直接操作class文件,在類中加一段列印日誌的代碼,然後調用retransformClasses就可以了。

相關焦點

  • 這一次,徹底弄懂 Java 字節碼文件!
    」Java字節碼十六進位Mac作業系統下建議使用 Hex Fiend 工具查看 MyTest1.class 文件的十六進位格式。Java字節碼整體結構如下圖所示,以下圖示以不同緯度展示了字節碼結構中所包含的關鍵內容。Java字節碼整體結構圖:完整的Java字節碼結構圖:接下來結合十六進位格式的 class 文件,參照 Java字節碼文件來剖析下都包含了哪些內容。
  • Java動態字節技術之Javassist
    概述Javassist是一個開源的分析、編輯和創建Java字節碼的類庫,可以直接編輯和生成Java生成的字節碼。相對於bcel, asm等這些工具,開發者不需要了解虛擬機指令,就能動態改變類的結構,或者動態生成類。javassist簡單易用, 快速。
  • 字節碼文件結構詳解
    在學習 Java 之初,就了解到了我們所寫的.java會被編譯期編譯成.class文件之後被 JVM 加載運行。字節碼是各種不同平臺的虛擬機與所有平臺都統一使用的程序儲存格式。是構成Run Anywhere 的基石。因此了解 Class 字節碼文件對於我們開發、逆向都是十分有幫助的。
  • Java程式設計師必備基礎:Java代碼是怎麼運行的?
    本文主要講解流程如下: java源文件編譯為class字節碼 類加載器把字節碼加載到虛擬機的方法區。 因此,在運行Java程序之前,需要編譯器把代碼編譯成java虛擬機所能識別的指令程序,這就是Java字節碼,即class文件。 所以,Java代碼運行的第一步是:把Java原始碼編譯成.class 字節碼文件。
  • 你知道java反射機制中class.forName和classloader的區別嗎?
    前兩天頭條有朋友留言說使用class.forName找不到類,可以使用classloader加載。趁此機會總結一下,正好看到面試中還經常問到。一、類加載機制上面兩種加載類的方式說到底還是為了加載一個java類,因此需要先對類加載的過程進行一個簡單的了解。
  • get新技能,Java的Class與反射機制原理,讓你寫出更靈活的代碼
    ;這種動態獲取的信息以及動態調用對象的方法的功能稱為java語言的反射機制。要想解剖一個類,必須先要獲取到該類的字節碼文件對象,而解剖使用的就是Class類中的方法。所以先要獲取到每一個字節碼文件對應的Class類型的對象。
  • 簡單分析方法在字節碼文件中的表述
    藍色選中區域為上一篇最後分析的一段字節碼,它是構造方法最後可以追溯的字節碼,說明接下來的字節碼在另一個方法的開始。"2A B4 00 02 AC"這5個字節就是Code結構code屬性,它表示的是方法真正的操作過程,對應虛擬機字節碼指令表得到如下:2A對應aload_0表示將第一個引用類型本地變量推送至棧頂;B4對應getfield表示獲取指定類的實例域
  • Java基礎學習:一篇文章讓你搞懂Java字符串的前世今生
    來逐一看一下非對象嚴格地說,字面量在代碼運行到它所在語句之前,它還不是字符串對象要理解從字面量變成字符串對象的過程,需要從字節碼的角度來分析在上面的 java 代碼被編譯為 class 文件後,"abc" 存儲於【類文件常量池】中
  • 新手學Java編程應該學那些Java基礎知識
    自己總結的一些java基礎知識,想入行java的跟新手都可以看看!  經過這麼多年的Java開發,以及結合平時面試Java開發者的一些經驗,我覺得對於J2SE方面主要就是要掌握以下的一些內容。  1.
  • Java高級特性:反射機制
    然後操作其屬性、調用其方法;在編譯時能夠確定創建一個什麼類對象,調用什麼屬性和方法。Java運行分為兩種狀態:編譯時:通過 javac 命令,生成一個或多個.class字節碼文件,(每個.class字節碼文件對應一個類);運行時
  • Java編程中基礎反射詳細解析
    類加載指的是將類的class文件讀入內存中,並為之創建一個 java.lang.Class對象,也就是說程序使用任何類的時候,都會為其創建一個class對象。類加載器負責加載所有的類,系統為所有加載到內存中的類生成一個java.lang.Class 的實例。
  • 一起學JAVA——反射技術
    反射技術是java動態特性的基石,java之所以有很多開發框架就是因為反射技術的存在。反射機制:所謂的反射機制就是java語言在運行時擁有一項自觀的能力。通過這種能力可以徹底地了解自身的情況為下一步的動作做準備。
  • ClassLoader——JAVA成長之路
    類加載器基本概念顧名思義,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經過 Java 編譯器編譯之後就被轉換成 Java 字節代碼(.class 文件)。
  • 阿里P8教你Java註解與反射
    在編譯器生成類文件時,標註可以被嵌入到字節碼中。Java 虛擬機可以保留標註內容,在運行時可以獲取到標註內容 。當然它也支持自定義 Java 標註。使用反射基本上是一種解釋操作,我們可以告訴JVM,我們想要做什麼然後它滿足我們的要求,這類操作總是慢於直接執行相同的操作。
  • 給Java新手的一些建議——Java知識點歸納(Java基礎部分)
    JVM相關(包括了各個版本的特性)對於剛剛接觸Java的人來說,JVM相關的知識不一定需要理解很深,對此裡面的概念有一些簡單的了解即可。不過對於一個有著3年以上Java經驗的資深開發者來說,不會JVM幾乎是不可接受的。JVM作為java運行的基礎,很難相信對於JVM一點都不了解的人可以把java語言吃得很透。
  • Java反射:框架設計的靈魂
    比如 C 語言;Java 嚴格來說也是編譯型語言,但又介於編譯型和解釋型之間;Java 不直接生成機器碼而是生成中間碼:編譯期間,是將源碼交給編譯器生成 class 文件(字節碼),這個過程中只做了翻譯的工作,並沒有把代碼放入內存運行;當進入運行期,字節碼才被 Java 虛擬機加載、解釋成機器語言並運行。
  • 關於java泛型的那些事
    千裡之行,始於足下java泛型小夥伴們肯定都見過或者使用過,例如:「List<String>」,表示這是一個存放String類型的List集合,但是在java編譯之後其實會將這個String類型擦除。
  • Java程序是如何運行的
    Java程序的代碼是什麼樣的Java誕生之初最大的賣點就是編寫的代碼跨平臺可移植性,實現這種可移植性,是因為Java通過平臺特定的虛擬機,運行中間的字節碼,而不是直接編譯成本地二進位代碼實現,中間字節碼也就是java文件編譯後生成的.class文件,Jar包的話,實際上只是一系列.class文件的集合
  • 深入JAVA 字節碼驗證 for 循環中 list.size()是否會重複調用?
    接下來我查看了字節碼發現,這裡確實會調用多次list.size()方法,字節碼如下:下面我們來談談具體字節碼指令解析Java二進位指令代碼解析Java源碼在運行之前都要編譯成為字節碼格式(如.class文件),然後由ClassLoader將字節碼載入運行