Java高階技術(JVM&ByteCode)及其運用

大部分人大談特談JAVA語言,這對於我來說也許聽起來很奇怪,但是我無法不去在意。JVM才是Java生態系統的核心啊。

-- Java之父 【James Gosling】

我很喜歡祖師爺的這句話,我覺得只有了解了最核心的技術,才算得上是精通了這門技術。當精通某項技術之後,在這個體系下的一切,學習、運用、創造才能做到遊刃有餘、手到擒來;才能更好的發揮個人和想象和創造力,做出更有價值的事情。

當我們從一個Java學徒,逐步會運用Java語言編寫項目的,只要1-2年時間,過幾年後,也許大家和我會有同樣的感覺,這門語言可以想象和發揮的空間似乎受到了限制,我們並不能很好的完成很多手上的工作。我猜測和我們的學習路徑有關係,因為大部分學習Java的人,都是從HelloWorld開始學習,然後開始學習變數、函數、邏輯控制、循環、異常等,為了滿足成就感,然後就開始項目了。很多速成教程也是為了讓我們快速使用,而沒有深入的體系化介紹Java的原理和技術棧,本文嘗試通過圍繞Java虛擬機(JVM)和位元組碼(ByteCode)相關的技術內容,以及其衍生出來的應用場景,進行一個整體的串聯,目的是希望大家對Java核心技術有一個重新的認識。

Advertisements

我先提供一下本文的KeyWord,用於用於讀者快速了解我接下來要講的內容,各位也可以根據自己了解這些技術關鍵字的數量和深度,估算一下自己是否對這些Java的核心技術了解狀況

JVM、ByteCode、ASM、AspectJ、CGLib、Instrumentation、javaagent、JVMTI、Btrace、byteman

Pluggable Annotation Processing API

Attach API

Java Compiler API

這些技術都是圍繞這JVM和ByteCode相關領域的,我將這些技術的核心內容定位於Java的高階技術,主要的原因是因為我覺得這些技術非常的Hack,表現在幾個方面:

Advertisements

  1. 這些技術在Java技術棧的偏底,一般Java程序員了解得不是很深入;

  2. 這些技術在實際運用非常的廣泛,現在所有主流的框架中的核心技術都有用到;

  3. 這些技術可以想象的空間非常大,能夠解決很多我們用常規方法解決不了的問題。

因為牽涉到的技術非常的多,我這裡不會對每項技術深入太多,主要是讓大家有一個體系化的認識,本文後面會附上我整理和篩選的相關資料,已被大家進一步學習和查閱。

JVM和ByteCode概述

JVM執行的不是Java,而是ByteCode

我先介紹下JVM的體系結構,JVM規範定義了一系列子系統以及它們的外部行為。JVM主要由以下子系統:

  1. 類載入器(Class Loader),用於讀入.class位元組碼文件並將類載入到數據區。

  2. 執行引擎(ExecutionEngine),用於執行數據區的指令,操作系統提供真實內存給JVM,用於JVM數據區存取數據。

如果要深入學習JVM的原理,查看本文末尾的參考文檔,本文不對JVM內部做很多的描述。從這個圖裡面,我們可以看出,JVM並不是執行的Java文件,而是class文件,也就是用於描述ByteCode的文件,這個點給我們提供了很多想象空間以,給我們留下了很多可以Hack的可能。我們可以修改ByteCode的值來插入一些代碼,來做一些我們之前做不到的事情;也可以通過分析ByteCode來做一些分析和統計的工作,比如分析鎖使用風險、分析代碼邏輯結構等。

class文件結構簡介

class文件有非常嚴謹的結構,Oracle公布的JVM標準規範中有詳細描述

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html

Code_attribute {

u2 attribute_name_index; //常量池中的uft8類型的索引,值固定為」Code「

u4 attribute_length; //屬性值長度,為整個屬性表長度-6

u2 max_stack; //操作數棧的最大深度值,jvm運行時根據該值佩服棧幀

u2 max_locals; //局部變數表最大存儲空間,單位是slot

u4 code_length; // 位元組碼指令的個數

u1 code[code_length]; // 具體的位元組碼指令

u2 exception_table_length; //異常的個數

{ u2 start_pc;

u2 end_pc;

u2 handler_pc; //當位元組碼在[start_pc,end_pc)區間出現catch_type或子類,則轉到handler_pc行繼續處理。

u2 catch_type; //當catch_type=0,則任意異常都需轉到handler_pc處理

} exception_table[exception_table_length]; //具體的異常內容

u2 attributes_count; //屬性的個數

attribute_info attributes[attributes_count]; //具體的屬性內容

}

修改class文件的工具

要在實現一些Hack的能力,我們首先第一步要解決的是解決class文件的增、改、讀的問題。我們有很多現成位元組碼工具可以用,這裡介紹位元組碼工具中使用最廣泛的一個,無處不在的ASM(也許你從沒有注意到過它神一樣的存在),ASM關注的是使用和性能的簡單性,使它的設計和實現都儘可能的小和快,這使得它在動態系統中非常有吸引力。

AMS提供了兩套API修改class文件,一套基於事件模型、一套基於樹的數據結構模型,這兩種API可以簡單的理解為類似於的XMLAPI(SAX)和XML文檔對象模型(DOM)API文檔:基於事件的API類似於SAX,基於對象的API是類似於DOM。基於對象的API建立在基於事件的基礎之上,比如DOM可以在SAX上提供。

基於事件的API定義了一組可能的事件和他們必須發生的順序,並提供一個類解析器生成一個事件解析每個元素,從這些事件序列生成編譯類。

基於樹的數據結構的API是面向對象的一種設計,類被表示為一個對象,原理是將class文件的結構隱射成了一套標準的樹形結構,方便對calss文件進行編輯操作。

在ASM基礎之上,進一步衍生出了一些工具,比如 AspectJ、CGLib 這兩個鼎鼎大名的AOP工具。

除了ASM還有很多其他的位元組碼工具,這裡例舉幾個比較常用的位元組碼工具,我這裡整理一下前三甲位元組碼工具,供大家參考和擴展學習。

  1. OW2獨立開源組織提供的ASM http://asm.ow2.org

  2. Jboss-javassist維護的 Javassisthttp://jboss-javassist.github.io/javassist/

  3. Apache 維護的BCELhttp://commons.apache.org/proper/commons-bcel/

ByteCode技術原理和運用

calss文件的涉及到了3個階段 編譯 -- 載入--運行,為了體系化的介紹,接下來我從這三個階段進行串聯概述其原理和運用場景。

在編譯的位元組碼技術

我們可以在非運行時,當然是可以對class文件進行修改或是分析操作的,所以原理上不需要過多介紹,雖說是在編譯階段對靜態位元組碼的處理,但是其應用場景也非常多。

1、代碼分析

利用位元組碼靜態分析,對代碼計算度量、查找bug和檢查編碼約定

  • SemmleCode https://semmle.comSemmleCode工程分析平台幫助快速識別和應對關鍵漏洞,開發安全、高質量的軟體;

  • Sonargraph http://www.hello2morrow.com/products/sonargraphSonargraph是一個強大的靜態代碼分析器,它允許您監視軟體系統的技術質量,並在開發過程的所有階段中執行有關軟體體系結構、度量和其他方面的規則;

  • TamiFlex http://secure-software-engineering.github.io/tamiflex/是一個工具套件,用於幫助對使用反射和自定義類裝入器的Java程序進行靜態分析。

  • JCarder是在併發多線程Java程序中尋找潛在死鎖的開源工具。它通過動態地檢測Java位元組碼來實現這一點(即:它不是用於靜態代碼分析的工具,而是在獲得的鎖的圖中尋找循環。)

2、代碼生成

通過位元組碼修改,結合Pluggable Annotation ProcessingAPI生成代碼,減少代碼量,比如ORM框架、EJB框架、IOC框架、JDO框架、UnitTest框架等,這裡有很多的著名框架都有用到相關的技術,除此之外,還有一些輔助編程工具等等。

  • Spring 核心技術中結合AspectJ實現靜態代理的IOC框架;

  • Hibernate 使用位元組碼技術自動生成DO類;

  • AgitarOne 使用位元組碼技術自動生成測試類和測試代碼;

  • Lombok 通用的代碼簡化工具,結合標註和Pluggable Annotation ProcessingAPI,有效減少代碼的編寫,比如get、set、構造函數、hashCode、log等,都可以實現標籤化;

  • fun4j它是一個將函數式編程的主要概念集成到Java平台的框架。它的核心是一個lambda-to-JVM位元組碼編譯器。

3、語言擴展

JVM上運行的是class文件,所以說,我們可以將各種圖靈完備的語言,編譯成class文件,移植到JVM上面來。甚至自己在為了解決一些特定場景情況下,構造自己的領域語言(DSL),將其編譯成class文件來運行。下面給出了JVM上一些主流的其他語言。

Groovy、Scala、JRuby、Kotlin、Jython、NetRexx

類載入和運行時位元組碼技術介紹

本來想將啟動時和運行時用到的位元組碼技術分開介紹,但是由於啟動時和運行時這兩塊技術耦合得比較緊,而且很多運行時技術都是基於啟動時技術發展起來的,所以合在了一起介紹。

Instrumentation

在Java SE5 中,提供了一種為JVM提供代理能力,如此一來我們可以支持JVM級別的 AOP操作,就可以在類載入的時候,對class類文件進行替換和修改了。在 Java SE 5及其後續版本當中,通過使用java.lang.instrument.Instrumentation這個介面,我們可以實現對JVM的攔截操作,可以在一個普通Java 程序(帶有 main 函數的 Java 類),通過 -javaagent參數指定一個特定的 jar 文件(包含Instrumentation 代理)來啟動 Instrumentation 的代理程序。

Java SE 6中,instrumentation 包被賦予了更強大的功能:啟動后的instrument、本地代碼(native code)instrument,以及動態改變 classpath等等。這些改變,意味著Java 具有了更強的動態控制、解釋能力,它使得 Java 語言變得更加靈活多變。另外,對native 的Instrumentation也是 Java SE 6 的一個嶄新的功能,這使以前無法完成的功能 —— 對 native 介面的instrumentation 可以在 Java SE 6 中,通過一個或者一系列的 prefix 添加而得以完成。最後,JavaSE 6 里的 Instrumentation 也增加了動態添加 class path的功能。所有這些新的功能,都使得instrument 包的功能更加豐富,從而使 Java 語言本身更加強大。

在 Java SE 5 中,Instrument要求在運行前利用命令行參數或者系統參數來設置代理類,在實際的運行之中,虛擬機在初始化之時(在絕大多數的 Java類庫被載入之前),instrumentation的設置已經啟動,並在虛擬機中設置了回調函數,檢測特定類的載入情況,並完成實際工作。但是在實際的很多的情況下,我們沒有辦法在虛擬機啟動之時就為其設定代理,這樣實際上限制了instrument 的應用。而 Java SE 6 的新特性改變了這種情況,通過 Java Tool API 中的 AttachAPT 方式,我們可以很方便地在運行過程中動態地設置載入代理類,以達到 instrumentation 的目的。

下圖介紹了javagent實現代理的方式以及內部的一些關鍵方法。

Attach API

javaagent可以在JVM啟動后再載入,就是通過Attach API實現的。當然,AttachAPI可不僅僅是為了實現動態載入agent,AttachAPI其實是跨JVM進程通訊的工具,能夠將某種指令從一個JVM進程發送給另一個JVM進程。載入javaagent只是AttachAPI發送的各種指令中的一種, 諸如jstack列印線程棧、jps列出Java進程、jmap做內存dump等功能,都屬於AttachAPI可以發送的指令。

JVM Tool Interface(JVMTI)

JVM ToolInterface(JVMTI)是JVM提供的native編程介面,開發者可以通過JVMTI向JVM監控狀態、執行指令,其目的是開放出一套JVM介面用於profile、debug、監控、線程分析、代碼覆蓋分析等工具。

JVMTI和InstumentationAPI的作用很相似,都是一套JVM操作和監控的介面,且都需要通過agent來啟動:

  • Instumentation API需要打包成jar,並通過Java agent載入(-javaagent)

  • JVMTI需要打包成動態鏈接庫(隨操作系統,如.dll/.so文件),並通過JVMTIagent載入(-agentlib/-agentpath)

既然都是agent,那麼載入時機也同樣有兩種:啟動時(Agent_OnLoad)和運行時Attach(Agent_OnAttach)。

JVMTI能做的事情包括:

  • 獲取所有線程、查看線程狀態、線程調用棧、查看線程組、中斷線程、查看線程持有和等待的鎖、獲取線程的CPU時間、甚至將一個運行中的方法強制返回值……

  • 獲取Class、Method、Field的各種信息,類的詳細信息、方法體的位元組碼和行號、向Bootstrap/SystemClass Loader添加jar、修改System Property……

  • 堆內存的遍歷和對象獲取、獲取局部變數的值、監測成員變數的值……

  • 各種事件的callback函數,事件包括:類文件載入、異常產生與捕獲、線程啟動和結束、進入和退出臨界區、成員變數修改、gc開始和結束、方法調用進入和退出、臨界區競爭與等待、VM啟動與退出……

  • 設置與取消斷點、監聽斷點進入事件、單步執行事件……

前面說的InstumentationAPI也是基於JVMTI來實現的,具體以addTransformer來說,通過Instrumentation註冊的ClassFileTransformer,實際上是註冊了JVMTI針對類文件載入事件(ClassFileLoadHook)的callback函數。

類載入和運行期間位元組碼技術的運用場景

熱部署領域

熱部署一般有兩種實現方式,一種是通過使用ClassLoader載入新類,但是因為JVM不允許相同的類多次載入,所以在載入之前,可能需要先卸載老的類,這樣一來,在運行過程中的狀態可能會丟失,也可能造成短時間不不可用,這就違背了熱部署的一些初衷。另外一種是通過javaagent的方式修改內存中class的位元組碼,或是攔截默認載入器的行為這種方式使用場景很多。可以參考《深入探索 Java 熱部署》 http://www.importnew.com/17115.html

  • JRebel:目前最常用的熱部署工具,是一款收費的商業軟體,因此在穩定性和兼容性上做的都比較好。

  • Spring-Loaded:Spring旗下的子項目,也是一款開源的熱部署工具。

  • Hotcode2:阿里內部開發和使用的熱部署工具,功能和上面基本一樣,同時針對各種框架做了很多適配。

  • IDE提供的HotSwap

  • 使用eclipse或IntelliJIDEA通過debug模式啟動時,默認會開啟一項HotSwap功能。用戶可以在IDE里修改代碼時,直接替換到目標程序的類里。不過這個功能只允許修改方法體,而不允許對方法進行增刪改。該功能的實現與debug有關。

  • debug其實也是通過JVMTI agent來實現的,JVITIagent會在debug連接時載入到debugee的JVM中。debuger(IDE)通過JDI(Java debuginterface)與debugee(目標Java程序)通過進程通訊來設置斷點、獲取調試信息。除了這些debug的功能之外,JDI還有一項redefineClass的方法,可以直接修改一個類的位元組碼。沒錯,它其實就是暴露了JVMTI的bytecodeinstrument功能,而IDE作為debugger,也順帶實現了這種HotSwap功能。

線上診斷領域

Btrace 這是一個線上診斷神器,基本原理是Java Agent+ASM+Java instrument+ JavaComplier Api來實現了運行是代碼功能的動態調整注入。http://www.btrace.com

Greys 實現原理類似,可以參考阿里雲的文章 《GreysJava在線問題診斷工具》https://yq.aliyun.com/articles/2390

Byteman Byteman的原理是在運行時修改應用程序類的位元組碼 http://byteman.jboss.org

代碼覆蓋率

JaCoCoJVM中通過-javaagent參數指定特定的jar文件啟動Instrumentation的代理程序,代理程序在通過ClassLoader裝載一個class前判斷是否轉換修改class文件,將統計代碼插入class,測試覆蓋率分析可以在JVM執行測試代碼的過程中完成。

應用性能監控(APM)工具

現在市場上大部分的監控產品都是基於代理啟動instrumentation實現的,原理上和JaCoco類似為你準備了前5個可用的工具:APM工具集合https://dzone.com/articles/java-performance-monitoring-5-open-source-tools-you-should-know

  • Stagemonitor http://www.stagemonitor.org

  • Pinpoint https://github.com/naver/pinpoint

  • MoSKito https://www.moskito.org

  • Glowroot https://glowroot.org

  • Kamon http://kamon.io/documentation/get-started/

動態代理技術實現AOP

  • CGLIB 在SpringAOP中,通常會用CGLIB來生成AopProxy對象。在Hibernate中PO(Persistant Object持久化對象)位元組碼的生成工作也要靠它來完成; https://github.com/cglib/cglib/wiki

  • DynamicAspects 通過instrumentation和javaanent實現的動態代理http://dynamicaspects.sourceforge.net

參考資料

JVM介紹https://anturis.com/blog/java-virtual-machine-the-essential-guide/

JVM內幕http://blog.jamesdbloom.com/JVMInternals.html

JAVA語言和JVM規範https://docs.oracle.com/javase/specs/index.html

JVM指令集https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokeinterface

ASM User http://asm.ow2.org/users.html

ASM4手冊http://download.forge.objectweb.org/asm/asm4-guide.pdf

CGLib Userhttps://github.com/cglib/cglib/wiki

開源位元組碼工具http://www.java-source.net/open-source/bytecode-libraries

BCEL官方文檔http://commons.apache.org/proper/commons-bcel/

javaagenthttps://www.jianshu.com/p/1557dc1b1094

JVMTIhttps://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html

JVMTM Tool Interfacehttps://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html

JVM源碼分析之javaagent原理完全解讀http://www.infoq.com/cn/articles/javaagent-illustrated

JSR 199 Java Compiler APIhttps://www.jcp.org/en/jsr/detail?id=199

Annotation processor APIhttps://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Processor.html

IBM Java SE 6 新特性 編譯器APIhttps://www.ibm.com/developerworks/cn/java/j-lo-jse64/index.html

JSR 269: Pluggable Annotation Processing APIhttp://www.jcp.org/en/jsr/detail?id=269

Btrace https://github.com/btraceio/btrace

BTrace 簡要介紹https://www.jianshu.com/p/93e94b724476

IBM Java SE 6 新特性 Instrumentation 新功能https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html

Instrument package infohttps://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html

JCarder http://www.jcarder.org

AspectJhttps://en.wikipedia.org/wiki/AspectJ

The AspectJTM Programming Guidehttp://www.eclipse.org/aspectj/doc/released/progguide/index.html

Java Performance Monitoring: 5 Open Source Tools YouShould Knowhttps://dzone.com/articles/java-performance-monitoring-5-open-source-tools-you-should-know

JVM Attach機制實現http://lovestblog.cn/blog/2014/06/18/jvm-attach/

Attach APIhttps://docs.oracle.com/javase/7/docs/technotes/guides/attach/

Attach API 標準https://docs.oracle.com/javase/8/docs/jdk/api/attach/spec/

關於作者

頭條號:Java深度思考的作者,現就職於支付寶金融核心技術部,任高級技術專家,花名叢英,技術愛好廣泛喜歡,熱愛Java技術,也愛研究現在流行的區塊鏈和機器學習相關的內容;對於三方支付業務、金融技術架構、技術管理方面有比較豐富的經驗。

歡迎大家關注的我的頭條號,和我一起分享技術工程師熱愛的話題。

Advertisements

你可能會喜歡