JVM層面的切面實現 : jvm-sandbox 之 <應用啟動>

2020-08-31 程序猿啊駝

JVM-SANDBOX(沙箱)實現了一種在不重啟、不侵入目標JVM應用的AOP解決方案。就複製這一段用來開頭吧,具體介紹可以看官方github,傳送門 https://github.com/alibaba/jvm-sandbox

1. 啟動

sandbox源碼各模塊的結構如下:


sandbox安裝後的目錄結構如下:


按照官方的教程,啟動命令如下:

./sandbox.sh -p 2343

查看對應的腳本,函數定義如下:


翻譯過來就是執行如下命令

java -Xms128M -Xmx128M -Xnoclassgc -ea -jar /xxx/sandbox-core.jar 2342 /xxx/sandbox-agent.jar home=/xxx;token=xxx;server.ip=xxx;service.port=xxx;namespace=xxx

按順序將參數定義為

  1. targetJvmPid:目標應用pid
  2. agentJarPath:附加的agent包路徑
  3. cfg:agent-main的參數,包括home路徑;該次唯一標識toke;ip埠以及命名空間

查看sandbox-core模塊的pom.xml

<archive> <manifest> <mainClass>com.alibaba.jvm.sandbox.core.CoreLauncher</mainClass> </manifest></archive>

找到啟動類 com.alibaba.jvm.sandbox.core.CoreLauncher

該類存在一個main方法,接收3個參數,並實例化一個CoreLauncher實例,它在構造方法中完成agent的attach動作.

public CoreLauncher(final String targetJvmPid, final String agentJarPath, final String token) throws Exception { attachAgent(targetJvmPid, agentJarPath, token);}

主要是以agent-main的方式執行。

去到sandbox-agent模塊的pom.xml,有如下配置

<archive> <manifestEntries> <Premain-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Premain-Class> <Agent-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries></archive>

分別指定了premain和agent的啟動類com.alibaba.jvm.sandbox.agent.AgentLauncher

查看入口agentmain方法


處理agent傳入的cfg參數欄位,解析為k-v對,然後執行install方法,並將結果寫入到了${HOME}/.sandbox.token裡,該文件記錄的內容為

default;21988529;0.0.0.0;32165//命名空間;token;ip;port

2. AgentLauncher.install

sandbox-agent模塊裡AgentLauncher類的install方法將完成主要的加載過程,包括:

  1. 將系統參數序列化,用於後續反射調用
  2. 將sandbox-spy.jar 加入BootstrapClassLoader
  3. 分別為每個namespace初始化一個SandboxClassLoader自定義類加載器,其繼承自URLClassLoader,指向sandbox-core.jar,支持加載該jar包中的類。結合第2步,SandboxClassLoader目前加載的自定義jar包括sandbox-spy.jarsandbox-core.jar。其中sandbox-core.jar包括了源碼裡的sandbox-core-api模塊。注意,SandboxClassLoader位於sandbox-agent.jar包中,其父類加載器為AppClassLoader
  4. 使用SandboxClassLoader反射加載並實例化sandbox-core.jar裡的com.alibaba.jvm.sandbox.core.CoreConfigure,傳入第1步的參數以及sandbox.properties文件的路徑
  5. 使用SandboxClassLoader反射加載並實例化sandbox-core.jar裡的com.alibaba.jvm.sandbox.core.server.ProxyCoreServer,該步會實例化sandbox-core.jar裡的com.alibaba.jvm.sandbox.core.server.jetty.JettyCoreServer實例
  6. 執行JettyCoreServer的bind方法,加載自定義模塊、啟動http服務

3. JettyCoreServer.bind

JettyCoreServer的bind方法主要完成Http服務的啟動以及各模塊的加載,主要動作為:

  1. 初始化JvmSandbox對象
  2. 初始化Http服務
  3. 初始化Jetty處理器
  4. 啟動Http服務
  5. 重置所有用戶模塊,先卸載再加載

注,當前的ClassLoader為每個namespace對應的SandboxClassLoader,後續該包裡加載的類的類加載器都是這個,影響的類位於sandbox-core.jar中,對應源碼裡的sandbox-common-apisandbox-provider-apisandbox-apisandbox-core模塊。

3.1 初始化JvmSandbox對象

JvmSandbox表示一個沙箱對象,關係如下,依賴CoreConfigure和CoreModuleManager,分別表示系統配置信息和模塊管理。CoreModuleManager的實現類為DefaultCoreModuleManager,為主要功能。


同時在初始化的時候還會調用SpyUtils,為每個namespace分配公共的EventListenerHandler(SpyHandler實現)類

private void init() { SpyUtils.init(cfg.getNamespace());//初始化SpyUtils,分配公共的EventListenerHandler(SpyHandler實現)類}

3.2 初始化Http服務

初始化Http服務,啟動一個Jetty Server,並設置為daemon

3.3 初始化Jetty處理器

初始化Jetty處理器,實現一個請求分發器,可以通過http的方式來觸發@Command註解的方法。

註冊的處理器有兩種:

  1. web-scoket-servet訪問路徑為 /sandbox/${namespace}/module/websocket/* 處理類為WebSocketAcceptorServlet
  2. module-http-servlet訪問路徑為 /sandbox/${namespace}/module/http/* 處理類為ModuleHttpServlet,這邊重點介紹http處理器

3.3.1 ModuleHttpServlet

該Servlet要求請求URL按照如下格式:

/sandbox/${namespace}/module/http/${uniqueId}/${command}?${k1}=${v1}&${k2}=${v2}

  1. ${uniqueId}:模塊id
  2. ${command}:模塊下的方法,需要加@Command註解
  3. {k1}=${v1}&${k2}=${v2}:方法參數

對於一個http請求,具體流程為:

  1. 解析url得到模塊id,如果模塊不存在則返回,否則從模塊管理類CoreModuleManager中獲取對應的模塊CoreModule
  2. 匹配對應的方法,獲得Method對象查找有@Command註解且匹配/${uniqueId}/${command}的方法,具體會將模塊id和@Command的值用/進行拼接
  3. 生成方法調用參數,遍歷方法的入參類型,按照類型設置對應的入參列表,支持的類型包括:

3.1. HttpServletRequest

3.2. HttpServletResponse

3.3. Map<String,String[]>:使用http請求的參數

3.4. String:使用url中?後面的內容

3.5. PrintWriter:HttpServletResponse.getWriter()

3.6. OutputStream:HttpServletResponse.getOutputStream()

4. 反射執行Method

相當於自己實現了一個http請求分發器

3.4 啟動Http服務

啟動Jetty http服務

3.5 重置所有用戶模塊

主要調用CoreModuleManager的reset方法,完成模塊的重置。先卸載各個模塊再進行加載。

在這之前先介紹模塊的生命周期


(來自官網github wiki,官網都有,懶的搬了)

上面少了一個loadCompleted事件,會在模塊加載完成,模塊完成加載後調用。作者的意思是方法比較常用,所以單獨出來成為一個接口。

模塊的卸載會先將模塊進行凍結,然後再將模塊卸載,期間會觸發onFrozen和onUnload事件。有點需要注意的是凍結和卸載的不同,凍結只是把sandbox的事件通知機制給屏蔽掉,模塊的插樁代碼還在。凍結後可以解凍,插代碼可以復用。而卸載則會移除模塊對應的ClassFileTransformer,並重新加載原始的class,重新處理class,達到去除模塊插樁的目的。

這裡先只關注模塊的加載流程,模塊的加載過程如下:


  1. DefaultCoreModuleManager構造ModuleLibLoader對象,傳入模塊jar包所在路徑和兩個Callback,再調用load方法
  2. ModuleLibLoader在load方法裡會遍歷路徑下的所有Jar包,對每個Jar包,構造ModuleJarLoader對象,傳入Jar包所在路徑,再調用load方法觸發加載
  3. ModuleJarLoader會構造一個ModuleJarClassLoader,用於加載Jar包,如下:由於ModuleJarClassLoader當前線程的類加載器為SandboxClassLoader,為了讓Jar包中的類獨立開來,避免不同自定義模塊包中的類衝突,執行真正的加載動作loadingModules方法前,會把當前線程的類加載器設置為剛創建的ModuleJarClassLoader,加載完後再設置回namespace公用的SandboxClassLoader。



3.5.1 ModuleJarLoader.loadingModules

該方法會使用ModuleJarClassLoader通過SPI機制加載jar包裡的Module類。由於通過SPI機制,因而需要符合SPI規範:

  1. 必須擁有publish的無參構造函數
  2. 必須實現com.alibaba.jvm.sandbox.api.Module接口
  3. 必須完成META-INF/services/com.alibaba.jvm.sandbox.api.Module文件中的註冊(Java SPI規範要求)

sandbox裡的模塊都是通過sandbox-module-starter插件完成,只要實現Module接口然後再類上加上org.kohsuke.MetaInfServices註解,為插件點讚。

加載完Module類後會讀取類似的com.alibaba.jvm.sandbox.api.Information註解,獲得模塊id,如果沒有該註解,則不會加載該模塊。

加載完後會通知InnerModuleLoadCallback進行回調,完成後續動作。然後將該模塊id加入已加載模塊列表中。

InnerModuleLoadCallback經過過濾鏈處理後,會委託給DefaultCoreModuleManager的load方法完成實際的處理動作。

3.5.2 DefaultCoreModuleManager.load

該方法主要完成如下動作:

  1. 構造CoreModule對象,初始化模塊信息
  2. 為Module實現類上有@Resource的屬性欄位注入資源,當前只支持

2.1. LoadedClassDataSource,已加載類數據源,注入已有的DefaultCoreLoadedClass

2.2. DataSourceModuleEventWatcher,事件觀察者,注入新建的DefaultModuleEventWatcher.該對象被標記為一個可釋放資源,由ReleaseResource引用,會在模塊卸載的時候進行回收。具體到這裡是觸發ModuleEventWatcher的delete動作,在釋放模塊資源時,清除模塊上的事件,該動作會觸發class重新加載,去除該模塊的插樁代碼。

2.3. ModuleController,模塊控制接口,注入新建的DefaultModuleController

2.4. ModuleManager,模塊管理器,注入新建的DefaultModuleManager

2.5. ConfigInfo,沙箱配置信息,注入新建的DefaultConfigInfo

  1. 模塊生命周期回調,MODULE_LOAD
  1. 標記模塊已加載
  1. 如果模塊標記了加載時自動激活,則需要在加載完成之後激活模塊
  1. 註冊到模塊列表中
  1. 模塊生命周期回調,MODULE_LOAD_COMPLETED

3.5.3 ModuleJarClassLoader

回到3.5.2的第2步,這裡新建了很多對象注入到自定義Module實現類中,這些類都位於sandbox-core.jar包中。前面說過,這些類由SandboxClassLoader來加載,而ModuleJarClassLoader只複製加載各自模塊jar包的類,因而ModuleJarClassLoader需要繼承SandboxClassLoader,看下它的定義


RoutingURLClassLoader預先給指定的包路徑正則表達式指定ClassLoader,命中後將加載動作委託給指定的ClassLoader,沒命中才委託給父類加載器。

而ModuleJarClassLoader繼承自RoutingURLClassLoader,構造方法如下


指定了sandbox-core.jar裡類都由ModuleJarClassLoader的父類加載器即SandboxClassLoader來處理。

綜上,得到jvm-sandbox的類加載關係如下:


(圖片來自官網github)

除了sandbox-spy.jar包外,其他都包跟業務包進行了分離,各個模塊的包也互不影響。而sandbox-spy.jar裡的類由於s在完成插樁後會出現在業務代理裡,所以需要全局加載,故由Bootstrap加載器加載。

Ok,用一張圖總結jvm-sandbox的的啟動過程:

相關焦點

  • JVM層面的切面實現 : jvm-sandbox 之 <事件機制>
    這節介紹jvm-sandbox的事件機制,事件機制提供了切面通知的核心功能,內部主要結合asm和觀察者模式來實現。概括的說就是,jvm-sandbox圍繞了目標方法這個切面點,提供了多種通知機制。下面來看jvm-sandbox如何實現該過程。2.
  • JVM源碼分析之Attach機制實現完全解讀
    說簡單點就是jvm提供一種jvm進程間通信的能力,能讓一個進程傳命令給另外一個進程,並讓它執行內部的一些操作,比如說我們為了讓另外一個jvm進程把線程dump出來,那麼我們跑了一個jstack的進程,然後傳了個pid的參數,告訴它要哪個進程進行線程dump,既然是兩個進程,那肯定涉及到進程間通信,以及傳輸協議的定義,比如要執行什麼操作,傳了什麼參數等Attach
  • 程式設計師:深入理解JVM,從JVM層面來講Java多態
    與之對應的是,在JVM裡面提供了5條方法調用字節碼指令,分別如下:invokestatic:調用靜態方法invokespecial:調用實例構造器<init>方法、私有方法和父類方法。靜態分派的典型應用是方法重載。靜態分派發生在編譯階段。因此確定靜態分派的動作實際上不是有虛擬機來執行。另外雖然編譯器能確定出方法的重載版本,但在很多情況下這個重載版本並不是「唯一的」,往往只能確定一個「更加合適」的版本。主要原因是字面量不需要定義,所以字面量沒有顯式的靜態類型,它的靜態類型只能通過語言上的規則去理解和推斷。
  • Java面試總結之JVM
    程序計數器(和系統相關) 本地方法棧通常在面試中會圍繞這5個空間展開三、GC算法及垃圾回收器常見的垃圾回收算法:標記-清除,複製,標記-壓縮,分代收集常用的垃圾回收集器:Serial收集器,ParNew收集器,Paralle收集器,Paralle Old收集器,Cms收集器,G1收集器在實際應用中,我們可以根據不同的應用需求及伺服器的配置來配置相應的垃圾回收器
  • JVM:可視化 JVM 故障處理工具
    at sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:304) at sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:140) at sun.jvm.hotspot.HSDB.attach(HSDB.java:1184) at sun.jvm.hotspot.HSDB.access
  • 大白話談JVM的class類加載機制
    1.啟動類加載器首先就是jvm啟動的第一道關口,啟動類加載器Bootstrap ClassLoader,它主要是加載java的核心類。相信大家都知道,無論是什麼環節下運行java程序,都是要安裝jvm虛擬機環境的,而在這個環境的目錄中是有一個lib文件夾的,這個文件下就是java最核心的類庫,支撐著java系統的運行。
  • JVM性能調優
    1、了解jvm啟動流程:2、了解硬體、系統、進程三個層面的內存之間的概要內存分配,一張圖你就懂
  • JVM入門(一)類的加載過程
    無論是那種方式,當我們執行的時候,就會啟動jvm虛擬機去加載所要執行的Hello.class文件到虛擬機中。然後在jvm中有字節碼執行引擎負責去執行Hello.class中的main方法,在main方法中使用到了Person類,此時jvm又會去加載Person。也就是說jvm用到哪個類,然後就去加載哪個類。
  • 面試必問之JVM
    一、jvm運行時的數據區域jvm運行時數據區域 在jvm運行時的數據區域,方法區和堆是線程共享的區域,而java 棧,本地方法棧,程序計數器 這三部分是每個線程私有的空間。4.java 堆 堆是內存區域最大的一塊,是所有線程所共享的區域,在虛擬機啟動的時候創建,主要用來存放實例對象,幾乎所有的對象實例都是在堆中分配內存,堆也是垃圾收集器主要工作的地方,java堆可以處在不連續的物理空間上,只要邏輯上連續即可。5.方法區 方法區也是被線程所共享的區域,存儲的是已被虛擬機加載的類的信息,靜態變量等。
  • jmap 查看jvm堆內存信息
    Parallel GC with 4 thread(s)//GC 方式Heap Configuration: //堆內存初始化配置 MinHeapFreeRatio = 0 //對應jvm啟動參數-XX:MinHeapFreeRatio設置JVM堆最小空閒比率(default 40) MaxHeapFreeRatio =
  • jvm系列二:內存區域如何區分
    內存區域如何劃分我們都知道,jvm啟動後會將class文件加載到內存,那麼內存是一大整塊,還是有區域劃分呢?答案自然是,jvm內存劃分了五個區域:分別是方法區、程序計數器、虛擬機棧、堆內存、本地方法棧。
  • JVM的藝術—類加載器篇
    自定義類加載器需要加載類時,先委託應用類加載器去加載,然後應用類加載器又向擴展類加載器去委託,擴展類加載器在向啟動類加載器去委託。如果啟動類加載器不能加載該類。,啟動類加載器加載不了就向下交給擴展類加載器去加載,擴展類加載器加載不了就繼續向下委託交給應用類加載器去加載,以此類推。
  • 弄懂 JRE、JDK、JVM 之間的區別與聯繫,你知道多少?
    其實很多 Java 程式設計師在寫了很多代碼後,你問他 jre 和 jdk 之間有什麼關係,jvm 又是什麼東西,很多人不知所云。本篇不會講述 jvm 底層是如何與不同的系統進行交互的,而主要理清楚三者之間的區別,搞清楚我們寫的 xxx.java 文件是被誰編譯,又被誰執行,為什麼能夠跨平臺運行。首先,我們分別對這三者進行闡述。
  • JVM CPU Profiler技術原理及源碼深度解析
    Profiling技術是一種在應用運行時收集程序相關信息的動態分析手段,常用的JVM Profiler可以從多個方面對程序進行動態分析,如CPU、Memory、Thread、Classes、GC等,其中CPU Profiling的應用最為廣泛。
  • jvm系列一:我們的java程序如何跑起來
    是的,我們把我們寫的java文件打包編譯成class文件,然後通過java -jar啟動一個jvm進程,將這些class字節碼加載到我們的jvm內存中,然後就運行起來了。2、jvm如何加載class字節碼上文提到將class字節碼加載到jvm內存中,其實就是一個類加載過程。那麼我們的類加載過程是怎麼樣的呢?
  • jvm - jstatd工具
    RMI伺服器應用程式,用於監視測試的HotSpot Java虛擬機的創建和終止,並提供一個界面,允許遠程監控工具附加到在本地系統上運行的Java虛擬機。jstatd工具是一個RMI伺服器應用程式,主要用於監控HotSpot Java 虛擬機的創建與終止,並提供一個接口以允許遠程監控工具附加到本地主機上運行的JVM上。jstatd伺服器需要在本地主機上存在一個RMI註冊表。
  • JAVA HEAP SPACE解決方法和JVM參數設置
    接著我又把啟動的參數添上一個 -Xmx256M,這回就可以了。想一想,還是對於垃圾回收的原理不太了解,就在網上查了一下,發現了幾篇不錯的文章。XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimestamps -XX:+PrintGCApplicationStopedTime,jvm將會按照這些參數順序輸出gc概要信息,詳細信息,gc時間信息,gc造成 的應用暫停時間。
  • JVM中方法調用的底層實現,看這一篇就夠了
    方法調用的底層實現我們編寫的Java代碼,經過編譯後變成class文件,最後經過類加載器加載後進入了JVM的運行時數據區。方法句柄(MethodHandle)invokedynamic指令的底層,是使用方法句柄(MethodHandle)來實現的
  • JDK、JRE、JVM,是什麼關係?
    JRE(Java Runtime Environment Java運行環境) 是 JDK 的子集,也就是包括 JRE 所有內容,以及開發應用程式所需的編譯器和調試器等工具。JRE 提供了庫、Java 虛擬機(JVM)和其他組件,用於運行 Java 程式語言、小程序、應用程式。
  • JVM中Java類的生命周期,一文搞定
    ClassLoader),該類加載器使用C++語言實現,屬於虛擬機自身的一部分。四種類加載器:啟動(Bootstrap)類加載器啟動類加載器負責加載最為基礎、最為重要的類。負責將 JAVA_HOME/lib 下面的類庫加載到內存中(比如rt.jar)。