當前位置:
首頁 > 知識 > JVM類載入以及執行的實戰

JVM類載入以及執行的實戰

前幾篇文章主要是去理解JVM類載入的原理和應用,這一回講一個可以自己動手的例子,希望能從頭到尾的理解類載入以及執行的整個過程。

這個例子是從周志明的著作《深入理解Java虛擬機》第9章里抄來的。原作者因為有豐富的經驗,可以站在一個很高的高度去描述整個過程。而我只能以現有的水平,簡單的理解這個例子。

如果讀者感覺不錯,那都是原作者的智慧;如果覺得不過爾爾,那就是我水平有限。

先說說日誌。原先,我特別不喜歡在自己的程序里輸出日誌。寫的時候那叫一個爽,可是一旦運行出錯,那就麻煩了。因為不知道具體執行到哪一步出的錯,所以就要調試一大片代碼。尤其是大的項目,是要經常去分析日誌的。所以,我們都盡量在代碼里輸出詳細的日誌。

但是,我們不可能把所有的情況考慮到。也就是說,當程序在伺服器上跑的時候,我們想查看某個運行時的狀態和數據,如果沒有日誌輸出,就無能為力。

當然,並不是真的無能為力。這篇文章就是教你一些思考,以及解決這個問題的一個思路。

說白了,要是伺服器能夠臨時去執行一段代碼,輸出日誌,問題迎刃而解。有了前面類載入的知識,我們應該會想到:我們自己寫一個類,然後動態載入到伺服器的JVM進程的方法區,最後反射調用輸出日誌的那個方法。

但是,仔細想想,需要考慮的事情還有許多:

1)這個類可能會經常的被修改,經常的被載入,所以,執行完之後,要能夠從方法區卸載。而能夠被卸載的條件之一,就是它的類載入器被回收。之前已經載入了多個類的類載入器,是不可能那麼快被回收的。所以,這裡要自定義一個類載入器去載入待執行的類。

2)待執行的類要能夠訪問原來項目里的類,比如說WEB-INF下面的那些類。那怎麼辦呢?就要用到雙親委派模型了,將自定義類載入器的父類載入器設置為載入這個類載入器的類載入器。聽起來有點繞,沒關係,直接上代碼

/**
* 為了多次載入執行類而加入的載入器
* 把defineClass方法開放出來,只有外部顯式調用的時候才會使用到loadByte方法
* 由虛擬機調用時,仍然按照原有的雙親委派規則使用loadClass方法進行類載入
*
* @author zzm
*/
public class HotSwapClassLoader extends ClassLoader {

public HotSwapClassLoader {
// 設置父類載入器,用以訪問JVM進程中的原來的類
super(HotSwapClassLoader.class.getClassLoader);
}

/**
* 載入待執行的類
*/
public Class loadByte(byte[] classByte) {
return defineClass(null, classByte, 0, classByte.length);
}

}

3)待執行類的方法裡面的日誌輸出到哪裡?你可能脫口而出,System.out.println。但是System.out是標準輸出,是整個JVM進程的資源,也不利於查看。也許,你會想通過System.setOut指定一個文件作為輸出。可是,一旦設定,那以後整個JVM進程的輸出都會寫到這個文件裡面,這樣就影響了原來的程序,這不是我們想要的。所以,我們必須寫一個類來代替System類的作用。

/**
* 為JavaClass劫持java.lang.System提供支持
* 除了out和err外,其餘的都直接轉發給System處理
*
* @author zzm
*/
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;
}

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) {
System.arraycopy(src, srcPos, dest, destPos, length);
}

public static int identityHashCode(Object x) {
return System.identityHashCode(x);
}

// 下面所有的方法都與java.lang.System的名稱一樣
// 實現都是位元組轉調System的對應方法
// 因版面原因,省略了其他方法
}

那就有人問了,既然能代替System類,就直接用這個類不就完了唄,也沒有System類的事了?問得好,這就是下面第4點。

4)我們在客戶端編寫待執行類時,不能依賴特定的類;如果依賴了特定的類,就只有在能夠訪問到特定類的地方才能編譯通過,受限制太多。也就是說,我們在寫執行類時,不能用到HackSystem類,但是執行的時候,卻又必須是HackSystem類。所以思路應該是這樣的:在執行類裡面輸出時,還是用System.out,編譯完成後,再去修改編譯成的class文件,將常量池中java.lang.System這個符號替換成HackSystem。這裡的難點是在程序中修改class文件,需要你特別熟悉class文件的每個數據項。

/**
* 修改Class文件,暫時只提供修改常量池常量的功能
* @author zzm
*/
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;
}

/**
* 修改常量池中CONSTANT_Utf8_info常量的內容
* @param oldStr 修改前的字元串
* @param newStr 修改後的字元串
* @return 修改結果
*/
public byte modifyUTF8Constant(String oldStr, String newStr) {
int cpc = getConstantPoolCount;
int offset = CONSTANT_POOL_COUNT_INDEX + u2;
for (int i = 0; i < cpc; i++) { int tag = ByteUtils.bytes2Int(classByte, offset, u1); if (tag == 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 常量池數量 */ public int getConstantPoolCount { return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2); } } /** * Bytes數組處理工具 * @author */ 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++) { 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) {
byte newBytes = new byte[originalBytes.length + (replaceBytes.length - len)];
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;
}
}

最後,來看看實現替換符號引用以及得到輸出日誌的類

/**
* JavaClass執行工具
*
* @author zzm
*/
public class JavaClassExecuter {

/**
* 執行外部傳過來的代表一個Java類的Byte數組
* 將輸入類的byte數組中代表java.lang.System的CONSTANT_Utf8_info常量修改為劫持後的HackSystem類
* 執行方法為該類的static main(String[] args)方法,輸出結果為該類向System.out/err輸出的信息
* @param classByte 代表一個Java類的Byte數組
* @return 執行結果
*/
public static String execute(byte[] classByte) {
HackSystem.clearBuffer;
ClassModifier cm = new ClassModifier(classByte);
byte modiBytes = cm.modifyUTF8Constant("java/lang/System", "org/fenixsoft/classloading/execute/HackSystem");
HotSwapClassLoader loader = new HotSwapClassLoader;
Class clazz = loader.loadByte(modiBytes);
try {
Method method = clazz.getMethod("main", new Class[] { String[].class });
method.invoke(null, new String[] { null });
} catch (Throwable e) {
e.printStackTrace(HackSystem.out);
}
return HackSystem.getBufferString;
}
}

傳進來待執行類的class文件的位元組數組,先將符號替換,然後載入該類,反射調用該類的main方法,最後將HackSystem類收集到的輸出日誌返回。

為了更直觀的看到運行的結果,可以寫一個jsp文件,通過瀏覽器去訪問。

<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="org.fenixsoft.classloading.execute.*" %>
<% InputStream is = new FileInputStream("c:/TestClass.class"); byte b = new byte[is.available()]; is.read(b); is.close; out.println("");
%>

這裡將待執行類TestClass.class放到伺服器的C盤。只要TestClass裡面main方法,有調用System.out,就可以將輸出內容展現到頁面上。我自己在Tomcat上面的項目里也測試了一把,現在把代碼也貼出來

public class TestClass {
public static void main(String[] args) {
System.out.println("hello world!!!");
ClassLoader cl = TestClass.class.getClassLoader;
System.out.println("self: " + cl);
while (cl.getParent != null) {
System.out.println(cl.getParent.getClass);
cl = cl.getParent;
}
}
}

大家可以那我這個類去試一試,而且還可以根據輸出結果去溫習一下Tomcat的類載入體系。

整體流程講完了,感覺還是很燒腦。不經意間,我們就充當了一回黑客,將系統類的調用變成了調用我們自己的邏輯。Java引入JVM的目的就是提高靈活性,可以動態的運行,但是也引入了一定的安全問題。

回想整個流程,其實也有可替代的方案。比如jdk1.6引入了動態編譯,可以在運行時動態的編譯和執行我們的待執行類,但還是依賴了特定類。

我這裡只是拋磚引玉,推薦大家去看原作者的書,去看看更詳細的講解。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 科技優家 的精彩文章:

WebService入門實例教程
Web緩存相關知識整理
Spring Boot的properties配置文件讀取
藍橋杯 最短路徑問題
一不小心,陷入TCP的性能問題

TAG:科技優家 |

您可能感興趣

PHP的文件載入
VasSonic源碼之並行載入
一場載入史冊的DNA大通緝
解讀MySQL驅動載入邏輯
XML DOM 載入函數
MAX管家載入、顯示慢的原因及解決方法
越獄插件:讓iOS也能有Mac OS的載入風格效果!
Xbox One X主機換裝SSD:遊戲啟動、載入大提速
Angular CLI發布,增加新的部署命令並改進差異化載入
載入克隆大提速 WD Black NVMe SSD重裝上陣專攻遊戲電競
VG成為LPL史上首個零支持的隊伍?太真實了,這支持率將載入史冊
Unity資源載入入門
亞洲杯VAR回放改判!日本VS越南,載入史冊的一次改判!
測試MIUI10的AI預載入功能!另外小米6X也將加入!
《怪獵:世界》CPU利用率過高致遊戲崩潰!PC載入速度優於PS4
第一次載入武器系統的F-35戰機進行測試,你知道是A還是B么?
你的 Vans Sk8-Hi 正在載入中……
新一代Xbox將提高幀率和載入方式
PG One註定將載入中國嘻哈屎冊
cocos2dx實現載入頁loading頁,載入進度條和載入字樣(附代碼)