本系列是用來記錄《深刻理解Java虛擬機》這本書的讀書筆記。方便本身查看,也方便你們查閱。java
欲速則不達,欲達則欲速!程序員
第九章 類加載與執行子系統的案例與實戰web
代碼編譯的結果從本地機器碼轉變爲字節碼是存儲格式發展的一小步,倒是編程語言發展的一大步。編程
1、案例分析swift
一、tomcat:正統的類加載器架構數組
主流的java web服務器,如tomcat、jetty、weblogic、websphere等服務器,都實現了自定義的類加載。由於一個功能健全的web服務器,要解決如下問題:瀏覽器
(1)部署在同一個服務器上的兩個web應用程序所使用的java類庫能夠實現相互隔離。服務器應當保證兩個應用程序的類庫能夠互相獨立使用。緩存
(2)部署在同一個服務器上的兩個web應用程序所使用的java類庫能夠互相分享。若是部分類庫不能分享,虛擬機的方法區就會很容易出現過分膨脹的風險。tomcat
(3)服務器須要儘量的保證自身的安全不受部署的web應用程序影響。基於安全考慮,服務器所使用的類庫應該與應用程序的類庫互相獨立。安全
(4)支持JSP應用的web服務器,大多數須要支持hotswap功能。咱們知道,JSP文件最終要編譯成java class才能由虛擬機執行,但JSP文件因爲其純文本存儲的特性,運行時修改的機率遠遠大於第三方類庫或程序自身class文件。
因爲存在上述問題,在部署web應用時,單獨的一個classpath就沒法知足需求了,因此各類web服務器都不約而同的提供了好幾個classpath路徑供影虎存放第三方類庫,這些路徑通常以lib或classes命名。不一樣路徑的類庫,具有不一樣的訪問範圍和服務對象。
在tomcat目錄結構中,有3種目錄( 「/common/*」、「/server/*」和「/shared/*」 )能夠存放java類庫,另外加上web應用程序自身目錄 「WEB-INF/*」,一共4組,把java類庫放置在這些目錄中的含義分別是:
爲了支持這套目錄結構,並對目錄裏面的類庫進行加載和隔離,tomcat自定義多個類加載器,這些類加載器按照經典的雙親委派模型來實現。
從圖中的委派關係能夠看出, CommonClassLoader 能加載的類均可以被 CatelinaClassLoader和SharedClassLoader使用 , 而CatelinaClassLoader和SharedClassLoader本身能加載的類則與對方相互隔離。
WebAppClassLoader可使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。
而JasperLoader的加載範圍則僅僅是這個JSP文件所編譯出來的哪個Class,它出現的目的就是爲了被拋棄:當服務器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並經過再創建一個新的Jsp類加載器來實現JSP文件的HotSwap功能。
注意:對於Tomcat6.x的版本,只有指定了 tomcat/conf/catalina.properties 配置文件的 server.loader 和 share.loader 項纔會真正創建對應的 *ClassLoader實例,不然會用到這兩個類加載器的地方使用CommonClassLoader 的實例來代替,默認配置中沒有設置這兩個loader 項。
二、OSGi:靈活的類加載器架構
(1)OSGi概述
在OSGi裏,Bundle之間的依賴關係從傳統的上層模塊依賴底層模塊轉變爲平級模塊之間的依賴。
OSGi特色,要歸功於它靈活的類加載架構。OSGi的Bundle類加載器之間只有規則,沒有固定的委派關係。
java程序社區流傳着這麼一個觀點:「學習J2EE規範,去看JBoss源碼;學習類加載器,就去看OSGi源碼」。儘管J2EE規範和類加載器的知識並非一個對等的概念。不過既然這個觀點能在程序員中流傳開來,也從側面說明了OSGi對類加載器的運用確實有其獨到之處。
OSGi(open service gateway initiative)是OSGI聯盟制定的一個基於java語言的動態模塊化規範,最著名的應用案例就是eclipse IDE。
(2)引入OSGi的一個重要理由:
OSGi之因此能有上述誘人的特色,要歸功於它靈活的類加載器結構。OSGi的Bundle類加載器之間只有規則,沒有固定的委派關係。例如,某個Bundel聲明瞭一個它依賴的Package,若是有其餘Bundle聲明發布了這個Package後,那個對這個Package的全部類加載動做都會給發佈它的Bundle類加載器去完成。不涉及某個具體的Package時,各個Bundle加載器都是平級的關係,只有具體使用到某個Package和Class的時候,纔會根據Package導入導出定義來構造Bundle間的委派。
另外,一個Bundle類加載器爲其它Bundle提供服務時,會根據 Export-Package 列表嚴格控制訪問範圍。若是一個類存在於Bundle的類加載器能找到這個類,但不會提供給其它bundle使用,並且OSGi平臺也不會把其它Bundle的類加載請求分配給這個Bundle來處理。
(3)代碼實例
咱們能夠舉一個更具體一點的簡單例子,加入存在BundleA,BundleB和BundleC三個模塊,而且這三個Bundle定義的依賴關係爲:
public class DynamicProxyTest { interface IHello{ void sayHello(); } static class Hello implements IHello{ @Override public void sayHello(){ System.out.println("hello world"); } } static class DynamicProxy implements InvocationHandler{ Object originalObj; Object bind(Object originalObj){ this.originalObj = originalObj; return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass(), getInterfaces(),this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("welcome"); return method.invoke(originalObj, args); } } public static void main(String[] args) { IHello hello = (IHello)new DynamicProxy().bind(new Hello()); hello.sayHello(); } }
上述代碼中,惟一的「黑匣子」就是Proxy.newProxyInstance()方法,除此以外再沒有任何特殊之處。這個方法返回一個實現了IHello的接口。而且代理了new Hello()實例行爲的對象。跟蹤這個方法的源碼,能夠看到程序進行了驗證,優化,緩存,同步,生成字節碼和顯式類加載等操做,前面的步驟並非咱們關注的重點,而最後它調用了sun.misc.ProxyGenerator.generateProxyClass()方法來完成生成字節碼的動做.。
三、字節碼生成技術與動態代理的實現
相信許多Java開發人員都是用過動態代理,例如 java.lang.reflect.Proxy 或實現過 java.lang.reflect.InvocationHandler 接口。
下面一個例子,在方法前面打印一句「welcome」。
package jvm; public interface IHello { void sayHello(); void sayHi(); }
package jvm; public class Hello implements IHello{ @Override public void sayHello() { System.out.println("hello world"); } @Override public void sayHi() { System.out.println("hi world"); } }
package jvm; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class DynamicProxy implements InvocationHandler { Object originalObj; Object bind(Object originalObj) { this.originalObj = originalObj; return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("welcome"); return method.invoke(originalObj,args); } }
package jvm; public class test { public static void main(String[] args) { IHello hel = (IHello) new DynamicProxy().bind(new Hello()); hel.sayHello(); hel.sayHi(); } }
運行結果
四、Retrotranslator:跨越JDK版本
把JDK1.5 中編寫的代碼放到 JDK1.4 或 1.3 的環境去部署使用。爲了解決這個問題,一種名爲「Java逆轉移植」的工具(Java Backporting Tools)應運而生,Retrotranslator 是這類工具中較爲出色的一個。
2、實戰:本身動手實現遠程執行功能
咱們將使用前面學到的類加載及虛擬機執行子系統的知識去實如今服務端執行臨時代碼的能力。
一、目標
首先,在實現「在服務端執行臨時代碼」這個需求以前,先明確一下本次實戰的具體目標,咱們但願最終的產品是這樣的:
(1)不依賴JDK版本,能在目前普通使用的JDK中部署。
(2)不改變原有服務端程序的部署,不依賴任何第三方類庫。
(3)不侵入原有程序,即無需改變原程序的任何代碼,也不會對原程序運行帶來任何影響。
(4)「臨時代碼」應當具有足夠的自由度,不須要依賴特定的類或特定的接口。
(5)「臨時代碼」的執行結果能返回客戶端,執行結果能夠包括程序中輸出的信息及拋出的異常等。
二、思路
在程序實現的過程當中,咱們須要解決如下3個問題:
(1) 如何編譯提交到服務器的Java代碼?
(2) 如何執行編譯後的Java代碼?
(3) 如何收集Java代碼的執行結果?
三、實現
第一個類用於實現「同一個類的代碼能夠被屢次加載」這個需求,具體代碼以下:
package org.swift.framework.RemotePlugin; /** * 爲了屢次載入執行類而加入的加載器 * 把defineClass方法開放出來,只有外部顯式調用的時候纔會使用到loadByte方法 * 由虛擬機調用時,仍然按照原有的雙親委派規則使用loadClass方法進行加載 * zww */ public class HotSwapClassLoader extends ClassLoader { public HotSwapClassLoader() { super(HotSwapClassLoader.class.getClassLoader()); //使用父類的加載器 } public Class loadByte(byte[] classByte) { return defineClass(null, classByte, 0, classByte.length); } }
HotSwapClassLoader 所作的事情僅僅是公開父類中的defineClass() ,這個類加載器的類查找範圍與它的父類加載器是徹底一致的。
第二個類實現將 java.lang.System 替換爲咱們本身定義的HackSystem 類的過程,它直接修改符合Class 文件格式的 byte[] 數組中的常量池部分,將常量池中指定內容的 CONSTANT_Utf8_info 常量替換爲新的字符串。
package org.swift.framework.RemotePlugin; /** * 修改Class文件,暫時只提供修改常量池常量的功能 */ public class ClassModifier { /** * Class文件中常量池的起始偏移 */ private static final int CONSTANT_POOL_COUNT_INDEX = 8; /** * CONSTANT_Utf8_info 常量的tag標誌 */ private static final int CONSTANT_Utf8_info = 1; /** * 常量池中11種常量所佔的長度,CONSTANT_Utf8_info型常量除外,由於不是定長的 */ private static final int[] CONSTANT_ITEM_LENGTH = {-1, -1, -1, 5, 5, 9, 9, 3, 3, 5, 5, 5, 5}; private static final int u1 = 1; private static final int u2 = 2; private byte[] classByte; public ClassModifier(byte[] classByte) { this.classByte = classByte; } public byte[] modifyUTF8Constant(String oldStr, String newStr) { int cpc = getConstantPoolCount(); //常量的數量 int offset = CONSTANT_POOL_COUNT_INDEX + u2; //CONSTANT_POOL 起始位置 for (int i = 0; i < cpc; i++) { int tag = ByteUtils.bytes2Int(classByte, offset, u1); //獲取常量型 if (tag == CONSTANT_Utf8_info) { //判斷常量型類型是不是CONSTANT_Utf8_info int len = ByteUtils.bytes2Int(classByte, offset + u1, u2); offset += (u1 + u2); String str = ByteUtils.bytes2String(classByte, offset, len); if (str.equalsIgnoreCase(oldStr)) { byte[] strBytes = ByteUtils.string2Bytes(newStr); byte[] strLen = ByteUtils.int2Bytes(newStr.length(), u2); classByte = ByteUtils.bytesReplace(classByte, offset - u2, u2, strLen); classByte = ByteUtils.bytesReplace(classByte, offset, len, strBytes); return classByte; } else { offset += len; } } else { offset += CONSTANT_ITEM_LENGTH[tag]; } } return classByte; } /** * 獲取常量池中常量的數量 * @return */ private int getConstantPoolCount() { return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2); } }
ByteUtils 工具的實現:
package org.swift.framework.RemotePlugin; public class ByteUtils { public static int bytes2Int(byte[] b, int start, int len) { int sum = 0; int end = start + len; for (int i = start; i < end; i++) { // 由於當系統檢測到byte可能會轉化成int或者說byte與int類型進行運算的時候, // 就會將byte的內存空間高位補1(也就是按符號位補位)擴充到32位 // 若是b[i]爲負數時:例如:10000001 & 11111111 ==》 1111111111111111111111111 10000001 & 11111111 = 000000000000000000000000 10000001 int n = ((int)b[i]) & 0xff; n <<= (--len) * 8; sum = n + sum; } return sum; } public static byte[] int2Bytes(int value, int len) { byte[] b = new byte[len]; for (int i = 0; i < len; i++) { b[len - i - 1] = (byte) ((value >> 8 * i) & 0xff); } return b; } public static String bytes2String(byte[] b, int start, int len) { return new String(b, start, len); } public static byte[] string2Bytes(String str) { return str.getBytes(); } public static byte[] bytesReplace(byte[] originalBytes, int offset, int len, byte[] replaceBytes) { // || |~offset| ~len || || byte[] newBytes = new byte[originalBytes.length - len + replaceBytes.length]; System.arraycopy(originalBytes, 0, newBytes, 0, offset); //替換位置以前 System.arraycopy(replaceBytes, 0, newBytes, offset, replaceBytes.length); //替換的位置 System.arraycopy(originalBytes, offset + len, newBytes, offset + replaceBytes.length, originalBytes.length - offset -len); //替換的位置以後 return newBytes; } }
通過ClassModifier 處理後的 byte[] 數據纔會傳給 HotSwapClassLoader.loadByte() 方法進行類加載
最後一個類就是前面提到的代替 java.lang.System 的 HackSystem ,這個類除了把 out 和 err 兩個靜態變量修改了,其餘都來自於 System類的 public方法。
package org.swift.framework.RemotePlugin; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.PrintStream; /** * 爲JavaClass 劫持 java.lang.System 提供支持 * 除了 out 和 err 外,其他的都直接轉發給 System 處理 */ public class HackSystem { public final static InputStream in = System.in; private static ByteArrayOutputStream buffer = new ByteArrayOutputStream(); public final static PrintStream out = new PrintStream(buffer); public final static PrintStream err = out; public static String getBufferString() { return buffer.toString(); } public static void clearBuffer() { buffer.reset(); } public static void setSecurityManager(final SecurityManager s) { System.setSecurityManager(s); } public static SecurityManager getSecurityManager() { return System.getSecurityManager(); } public static long currentTimeMillis() { return System.currentTimeMillis(); } //下面全部的方法都與 java.lang.System 的名稱同樣 //實現都是字節調System的對應方法 //因版面緣由,省略其餘方法 }
至此,4個支持類已經講解完畢,咱們來看看最後一個類 JavaClassExecuter ,它是提供外部調用的入口
package org.swift.framework.RemotePlugin; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * JavaClass 執行工具 */ public class JavaClassExecuter { /** * 執行外部傳過來的表明一個Java類的byte數組 * 將輸入類byte數組中表明 java.lang.System的CONTANT_Utf8_info常量修改成劫持後的HackSystem類 * 執行方法爲該類的 main 方法,輸出結構爲該類向System.out/err輸出的信息 * @param classByte * @return */ public static String execute(byte[] classByte) { HackSystem.clearBuffer(); ClassModifier classModifier = new ClassModifier(classByte); //修改Class字節碼,把HackSystem 替代 System byte[] modiBytes = classModifier.modifyUTF8Constant("java.lang.System", "org.swift.framework.RemotePlugin.HackSystem"); HotSwapClassLoader loader = new HotSwapClassLoader(); Class clazz = loader.loadByte(modiBytes); try { //調用其main方法 Method method = clazz.getMethod("main", new Class[] { String[].class}); method.invoke(null, new String[] {null}); } catch (Exception e) { e.printStackTrace(HackSystem.out); } return HackSystem.getBufferString(); } }
四、驗證
任意寫一個Jaca類,只須要向外System.out 信息便可,同事放到指定路徑 C://TestClass.class ,而後創建一個Jsp 文件,在瀏覽器能夠看到這個類的運行結果。
package org.swift.framework.RemotePlugin; public class TestClass { public static void main(String[] args) { System.out.println("this is a test class"); } }
<%@ page import="java.lang.*" %> <%@ page import="java.io.*" %> <%@ page import="org.swift.framework.RemotePlugin.*" %> <% InputStream is = new FileInputStream("C:/TestClass.class"); byte[] b = new byte[is.available()]; is.read(b); is.close(); out.println("<textarea style='width:1000;height:800'>"); out.println(JavaClassExecuter.execute(b)); out.println("</textarea>"); %>
其中主要須要學習的就是對 class文件的內容進行修改替換,並能夠正常提供使用。
3、總結
本書中第6~9章介紹了class文件格式,類加載及虛擬機執行引擎及部份內容,這些內容時虛擬機必不可少的組成部分,瞭解了虛擬機如何執行程序,才能更好的理解怎樣才能寫出優秀的代碼。
關於虛擬機執行子系統的介紹到此爲止就結束了,經過這4章的講解,咱們描繪了一個虛擬機應該如何運行class文件的概念模型。對於具體到某個虛擬機的實現,爲了使實現簡單清晰,或者爲了更快的運行速度,在虛擬機內部的運做跟概念模型可能會有很是大的差別,但從最終的執行結果來看應該是一致的。
推薦博文
鳴謝:特別感謝做者周志明提供的技術支持!