Java热替换
前面有提到過Tomcat的熱部署,所謂熱部署就是在應(yīng)用運(yùn)行時(shí)更新Java類文件以升級軟件功能,升級過程不需要關(guān)停和重啟應(yīng)用。要進(jìn)行熱部署需要做class熱替換。Class熱替換實(shí)現(xiàn)了將修改的class再次加載到JVM中,以動(dòng)態(tài)替換內(nèi)存中原有的class字節(jié)碼。
實(shí)現(xiàn)class的熱替換就與Java類加載過程相關(guān),關(guān)于Java類加載過程的文章或書籍早些年就已經(jīng)很多了,這里從” 深入探討 Java 類加載器(http://www.ibm.com/developerworks/cn/java/j-lo-classloader/)’’一文中摘錄了部分內(nèi)容說明Java類的加載過程。
?
類加載器基本概念
基本上所有的類加載器都是 java.lang.ClassLoader類的一個(gè)實(shí)例。java.lang.ClassLoader類的基本職責(zé)就是根據(jù)一個(gè)指定的類的名稱,找到或者生成其對應(yīng)的字節(jié)代碼,然后從這些字節(jié)代碼中定義出一個(gè) Java 類,即 java.lang.Class類的一個(gè)實(shí)例。除此之外,ClassLoader還負(fù)責(zé)加載 Java 應(yīng)用所需的資源,如圖像文件和配置文件等。
Java 中的類加載器大致可以分成兩類,一類是系統(tǒng)提供的,另外一類則是由 Java 應(yīng)用開發(fā)人員編寫的。系統(tǒng)提供的類加載器主要有下面三個(gè):
? ?? 引導(dǎo)類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實(shí)現(xiàn)的,并不繼承自 java.lang.ClassLoader。
? ?? 擴(kuò)展類加載器(extensions class loader):它用來加載 Java 的擴(kuò)展庫。Java 虛擬機(jī)的實(shí)現(xiàn)會(huì)提供一個(gè)擴(kuò)展庫目錄。該類加載器在此目錄里面查找并加載 Java 類。
? ?? 系統(tǒng)類加載器(system class loader):它根據(jù) Java 應(yīng)用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應(yīng)用的類都是由它來完成加載的??梢酝ㄟ^ ClassLoader.getSystemClassLoader()來獲取它。
除了系統(tǒng)提供的類加載器以外,開發(fā)人員可以通過繼承 java.lang.ClassLoader類的方式實(shí)現(xiàn)自己的類加載器,以滿足一些特殊的需求。
?
除了引導(dǎo)類加載器之外,所有的類加載器都有一個(gè)父類加載器。通過getParent()方法可以得到。對于系統(tǒng)提供的類加載器來說,系統(tǒng)類加載器的父類加載器是擴(kuò)展類加載器,而擴(kuò)展類加載器的父類加載器是引導(dǎo)類加載器;對于開發(fā)人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。因?yàn)轭惣虞d器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發(fā)人員編寫的類加載器的父類加載器是系統(tǒng)類加載器。類加載器通過這種方式組織起來,形成樹狀結(jié)構(gòu)。樹的根節(jié)點(diǎn)就是引導(dǎo)類加載器。
?
1、Bootstrap Loader(啟動(dòng)類加載器):加載System.getProperty("sun.boot.class.path")所指定的路徑或jar。
2、Extended Loader(標(biāo)準(zhǔn)擴(kuò)展類加載器ExtClassLoader):加載System.getProperty("java.ext.dirs")所指定的路徑或jar。在使用Java運(yùn)行程序時(shí),也可以指定其搜索路徑.
3、AppClass Loader(系統(tǒng)類加載器AppClassLoader):加載System.getProperty("java.class.path")所指定的路徑或jar。在使用Java運(yùn)行程序時(shí),也可以加上-cp來覆蓋原有的Classpath設(shè)置.????????
?
類加載器的代理模式
類加載器在嘗試自己去查找某個(gè)類的字節(jié)代碼并定義它時(shí),會(huì)先代理給其父類加載器,由父類加載器先去嘗試加載這個(gè)類,依次類推。代理模式是為了保證 Java 核心庫的類型安全。通過代理模式,對于 Java 核心庫的類的加載工作由引導(dǎo)類加載器來統(tǒng)一完成,保證了 Java 應(yīng)用所使用的都是同一個(gè)版本的 Java 核心庫的類,是互相兼容的。
不同的類加載器為相同名稱的類創(chuàng)建了額外的名稱空間。相同名稱的類可以并存在 Java 虛擬機(jī)中,只需要用不同的類加載器來加載它們即可。不同類加載器加載的類之間是不兼容的,這就相當(dāng)于在 Java 虛擬機(jī)內(nèi)部創(chuàng)建了一個(gè)個(gè)相互隔離的 Java 類空間。
?
加載類的過程
真正完成類的加載工作是通過調(diào)用 defineClass來實(shí)現(xiàn)的;而啟動(dòng)類的加載過程是通過調(diào)用 loadClass來實(shí)現(xiàn)的。前者稱為一個(gè)類的定義加載器(defining loader),后者稱為初始加載器(initiating loader)。在 Java 虛擬機(jī)判斷兩個(gè)類是否相同的時(shí)候,使用的是類的定義加載器。也就是說,哪個(gè)類加載器啟動(dòng)類的加載過程并不重要,重要的是最終定義這個(gè)類的加載器。兩種類加載器的關(guān)聯(lián)之處在于:一個(gè)類的定義加載器是它引用的其它類的初始加載器。如類 com.example.Outer引用了類 com.example.Inner,則由類 com.example.Outer的定義加載器負(fù)責(zé)啟動(dòng)類 com.example.Inner的加載過程。
方法 loadClass()拋出的是 java.lang.ClassNotFoundException異常;方法 defineClass()拋出的是 java.lang.NoClassDefFoundError異常。
類加載器在成功加載某個(gè)類之后,會(huì)把得到的 java.lang.Class類的實(shí)例緩存起來。下次再請求加載該類的時(shí)候,類加載器會(huì)直接使用緩存的類的實(shí)例,而不會(huì)嘗試再次加載。也就是說,對于一個(gè)類加載器實(shí)例來說,相同全名的類只加載一次,即 loadClass方法不會(huì)被重復(fù)調(diào)用。
?
類加載過程:
1、尋找jre目錄,尋找jvm.dll,并初始化JVM;
2、產(chǎn)生一個(gè)Bootstrap Loader(啟動(dòng)類加載器);
3、Bootstrap Loader自動(dòng)加載Extended Loader(標(biāo)準(zhǔn)擴(kuò)展類加載器),并將其父Loader設(shè)為Bootstrap Loader。
4、Bootstrap Loader自動(dòng)加載AppClass Loader(系統(tǒng)類加載器),并將其父Loader設(shè)為Extended Loader。
5、最后由AppClass Loader加載應(yīng)用程序類。
?
我們再來回顧一下類的加載過程:前面提到,某一特定類加載器在加載類時(shí),首先將加載任務(wù)委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務(wù),就成功返回;如果父類加載器無法完成加載,則由該特定類加載器完成加載。這個(gè)機(jī)制就是所謂的雙親委派機(jī)制。
?
我們再來看一下Java 虛擬機(jī)是如何判定兩個(gè) Java 類是相同的。Java 虛擬機(jī)不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認(rèn)為兩個(gè)類是相同的。即便是同樣的字節(jié)代碼,被不同的類加載器加載之后所得到的類,也是不同的。具體的示例可以參見”深入探討 Java 類加載器”一文。
?
了解了基本概念,我們知道要實(shí)現(xiàn)Java類的熱替換,首先就需要繼承java.lang.ClassLoader類實(shí)現(xiàn)自己的類加載器。
定義一個(gè)類加載器,如下:
public class MyClassLoader extends URLClassLoader {
??? public static Map<String, Long> cacheLastModifyTimeMap = new HashMap<String, Long>();
??? public static URL url = null;
?
??? public MyClassLoader(URL url) {
??????? super(new URL[]{url});
??????? this.url = url;
??? }
?
??? public Class<?> loadClass(String name) throws ClassNotFoundException {
??????? return loadClass(name, false);
??? }
?
??? @Override
??? public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
??????? Class clazz = null;
??????? //首先檢查請求的類型是否已經(jīng)被這個(gè)類裝載器裝載到命名空間中了,如果已經(jīng)裝載,直接返回;否則繼續(xù)。
??????? clazz = findLoadedClass(name); //查找名稱為 name的已經(jīng)被加載過的類
??????? if (clazz != null) {
??????????? if (resolve) {
??????????????? resolveClass(clazz); //鏈接指定的 Java 類
??????????? }
??????????? //如果class類被修改過,則重新加載
??????????? if (isModify(name)) {
??????????????? MyClassLoader hcl = new MyClassLoader(url);
??????????????? clazz = customLoad(name, hcl);
??????????? }
??????????? return (clazz);
??????? }
?
??????? //如果類的包名為"java."開始,則有系統(tǒng)默認(rèn)加載器加載
??????? if (!name.startsWith("org.jevo.")) {
??????????? try {
??????????????? //得到系統(tǒng)默認(rèn)的加載cl
??????????????? ClassLoader system = ClassLoader.getSystemClassLoader();
??????????????? clazz = system.loadClass(name);? //加載名稱為 name的類
??????????????? if (clazz != null) {
??????????????????? if (resolve)
??????????????????????? resolveClass(clazz);
??????????????????? return (clazz);
??????? ????????}
??????????? } catch (ClassNotFoundException e) {
??????????????? // Ignore
??????????? }
??????? }
?
??????? return customLoad(name, this);
??? }
?
??? public Class customLoad(String name, ClassLoader cl) throws ClassNotFoundException {
??????? return customLoad(name, false, cl);
??? }
?
??? public Class customLoad(String name, boolean resolve, ClassLoader cl)
??????????? throws ClassNotFoundException {
??????? // //調(diào)用本類加載器的findClass(…)方法,試圖獲取對應(yīng)的字節(jié)碼,如果獲取的到,則調(diào)用defineClass(…)導(dǎo)入類型到方法區(qū);否則拋出異常
??????? Class clazz = ((MyClassLoader) cl).findClass(name); //查找名稱為 name的類
??????? if (resolve)
??????????? ((MyClassLoader) cl).resolveClass(clazz);
??????? //緩存加載class文件的最后修改時(shí)間
??????? long lastModifyTime = getClassLastModifyTime(name);
??????? cacheLastModifyTimeMap.put(name, lastModifyTime);
??????? return clazz;
??? }
?
??? private boolean isModify(String name) {
??????? long lastmodify = getClassLastModifyTime(name);
??????? long previousModifyTime = cacheLastModifyTimeMap.get(name);
??????? if (lastmodify > previousModifyTime) {
??????????? return true;
??????? }
??????? return false;
??? }
?
??? private long getClassLastModifyTime(String name) {
??????? String path = getClassCompletePath(name);
??????? File file = new File(path);
??????? if (!file.exists()) {
?? ?????????throw new RuntimeException(new FileNotFoundException(name));
??????? }
??????? return file.lastModified();
??? }
?
??? private String getClassCompletePath(String name) {
??????? String simpleName = name.replaceAll("\\.", "/");
??????? return url.getPath() + simpleName + ".class";
??? }
}
?
測試代碼如下:
public class DyService {
??? public String doBusiness() {
//??????? return "do something here..";
??????? return "do otherthings here..";
??? }
}
?
public class Main {
??? static ClassLoader cl;
??? static Object server;
??? static Class hotClazz = null;
?
??? public static void loadNewVersionOfServer() throws Exception {
??????? synchronized (Main.class) {
??????????? if (cl == null)
??????????????? cl = new MyClassLoader(new URL("file://D:/Project/test/out/"));
??????? }
??????? hotClazz = cl.loadClass("org.jevo.hotswap.sample.DyService");
??????? server = hotClazz.newInstance();
??? }
?
??? public static void test() throws Exception {
??????? BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
??????? loadNewVersionOfServer();
??????? while (true) {
??????????? System.out.println("Enter DOBUS, RELOAD, or QUIT: ");
??????????? String cmdRead = br.readLine();
??????????? String cmd = cmdRead.toUpperCase();
??????????? if (cmd.equals("QUIT")) {
??????????????? return;
??????????? } else if (cmd.equals("DOBUS")) {
??????????????? Method m = hotClazz.getMethod("doBusiness");
??????????????? System.out.println(m.invoke(server, null)); //這里使用反射機(jī)制來執(zhí)行事務(wù)。
??????????? } else if (cmd.equals("RELOAD")) {
??????????????? loadNewVersionOfServer();
??????????? }
??????? }
??? }
?
??? public static void main(String[] args) {
??????? try {
??????????? test();
??????? } catch (Exception e) {
??????????? e.printStackTrace();
??????? }
??? }
}
?
在上面的代碼中,使用了反射機(jī)制來執(zhí)行事務(wù)。這里不能將hotClazz.newInstance()得到的實(shí)例強(qiáng)制轉(zhuǎn)換成DyService對象實(shí)例來使用doBussiness方法,否則會(huì)拋出java.lang.ClassCastException異常。拋出ClassCastException異常的原因就在于前面提到的“Java 虛擬機(jī)如何判定兩個(gè) Java 類是相同的”,這里例子中的hotClazz 是由MyClassLoader加載的,而server變量類型聲明和類是由loadNewVersionOfServer方法所屬的類的加載器加載的,因此屬于不同的兩個(gè)類型,轉(zhuǎn)換時(shí)并不兼容,所以會(huì)拋出ClassCastException異常。
同樣,在上面例子中,當(dāng)class類被修改過后重新加載時(shí),我們是通過重新new一個(gè)MyClassLoader來加載被修改類的,MyClassLoader hcl = new MyClassLoader(url);
在前面我們提到:對于一個(gè)類加載器實(shí)例來說,相同全名的類只加載一次。所以同一個(gè)ClassLoader實(shí)例只能加載Class一次,一個(gè)class被一個(gè)ClassLoader實(shí)例加載過的話,就不能再被這個(gè)ClassLoader實(shí)例再次加載,即不再重復(fù)調(diào)defineClass()方法關(guān)聯(lián)字節(jié)碼,重復(fù)裝載將拋出重復(fù)類定義異常。同樣,系統(tǒng)默認(rèn)的ClassLoader加載器內(nèi)部會(huì)緩存加載過的class,重新加載的話,就直接取緩存。因此這里只能重新創(chuàng)建一個(gè)ClassLoader,然后再去加載已經(jīng)被加載過的class文件。
?
AppClass Loader(系統(tǒng)類加載器AppClassLoader):加載System.getProperty("java.class.path")所指定的路徑或jar
?
前面我們通過模擬loadClass方法的過程來加載Java類,我們也提到” 真正完成類的加載工作是通過調(diào)用 defineClass來實(shí)現(xiàn)的;而啟動(dòng)類的加載過程是通過調(diào)用 loadClass來實(shí)現(xiàn)的?!蓖瑫r(shí)我們也知道系統(tǒng)類加載器一般加載ClassPath所指定的路徑下的class,所以我們可以使用類定義加載器來加載ClassPath指定路徑下的class來實(shí)現(xiàn)熱替換,代碼如下:
??????? String classPath = System.getProperty("java.class.path");
??????? List classRepository = new ArrayList();
?
??????? if ((classPath != null) && !(classPath.equals(""))) {
??????????? StringTokenizer tokenizer = new StringTokenizer(classPath,
??????? ????????????File.pathSeparator);
??????????? while (tokenizer.hasMoreTokens()) {
??????????????? classRepository.add(tokenizer.nextToken());
??????????? }
??????? }
??????? Iterator dirs = classRepository.iterator();
??????? byte[] classBytes = null;
?
??????? while (dirs.hasNext()) {
??????????? String dir = (String) dirs.next();
??????????? //replace '.' in the class name with File.separatorChar & append .class to the name
??????????? String classFileName = className.replace('.', File.separatorChar);
??????????? classFileName += ".class";
??????????? try {
??????????????? File file = new File(dir + File.separatorChar + classFileName);
??????????????? if (file.exists()) {
??????????????????? InputStream is = new FileInputStream(file);
?
????????????????? ??classBytes = new byte[is.available()];
??????????????????? is.read(classBytes);
??????????????????? break;
??????????????? }
??????????? }
??????????? catch (IOException ex) {
??????????????? System.out.println("IOException raised while reading class file data");
??????????????? ex.printStackTrace();
??????????????? return null;
??????????? }
??????? }
??????? return this.defineClass(className, classBytes, 0, classBytes.length);
?
我們知道,JVM中class和Meta信息存放在PermGen space區(qū)域。在上面的代碼中,如果加載的class文件很多,那么可能導(dǎo)致PermGen space區(qū)域空間溢出,即java.lang.OutOfMemoryErrorPermGen space.?異常。
‘Java虛擬機(jī)類型卸載和類型更新解析(http://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html)’中作了一些說明,摘錄部分內(nèi)容如下:
首先看一下,關(guān)于java虛擬機(jī)規(guī)范中時(shí)如何闡述類型卸載(unloading)的:
????A class or interface may be unloaded if and only if its class loader is unreachable. The bootstrap class loader is always reachable; as a result, system classes may never be unloaded.
Java虛擬機(jī)規(guī)范中關(guān)于類型卸載的內(nèi)容就這么簡單兩句話,大致意思就是:只有當(dāng)加載該類型的類加載器實(shí)例(非類加載器類型)為unreachable狀態(tài)時(shí),當(dāng)前被加載的類型才被卸載.啟動(dòng)類加載器實(shí)例永遠(yuǎn)為reachable狀態(tài),由啟動(dòng)類加載器加載的類型可能永遠(yuǎn)不會(huì)被卸載.
我們再看一下Java語言規(guī)范提供的關(guān)于類型卸載的更詳細(xì)的信息(部分摘錄):
????//摘自JLS 12.7 Unloading of Classes and Interfaces
????1、An implementation of the Java programming language may unload classes.
????2、Class unloading is an optimization that helps reduce memory use. Obviously,the semantics of a program should not depend? on whether and how a system chooses to implement an optimization such as class unloading.
????3、Consequently,whether a class or interface has been unloaded or not should be transparent to a program
通過以上我們可以得出結(jié)論: 類型卸載(unloading)僅僅是作為一種減少內(nèi)存使用的性能優(yōu)化措施存在的,具體和虛擬機(jī)實(shí)現(xiàn)有關(guān),對開發(fā)者來說是透明的.
縱觀java語言規(guī)范及其相關(guān)的API規(guī)范,找不到顯示類型卸載(unloading)的接口, 換句話說:?
????1、一個(gè)已經(jīng)加載的類型被卸載的幾率很小至少被卸載的時(shí)間是不確定的
????2、一個(gè)被特定類加載器實(shí)例加載的類型運(yùn)行時(shí)可以認(rèn)為是無法被更新的
有 關(guān)unreachable狀態(tài)的解釋:
????1、A reachable object is any object that can be accessed in any potential continuing computation from any live thread.
????2、finalizer-reachable: A finalizer-reachable object can be reached from some finalizable object through some chain of references, but not from any live thread. An unreachable object cannot be reached by either means.
某種程度上講,在一個(gè)稍微復(fù)雜的java應(yīng)用中,我們很難準(zhǔn)確判斷出一個(gè)實(shí)例是否處于unreachable狀態(tài).
關(guān)于類型卸載大致概括為:
??? 1、有啟動(dòng)類加載器加載的類型在整個(gè)運(yùn)行期間是不可能被卸載的(jvm和jls規(guī)范).
??? 2、被系統(tǒng)類加載器和標(biāo)準(zhǔn)擴(kuò)展類加載器加載的類型在運(yùn)行期間不太可能被卸載,因?yàn)橄到y(tǒng)類加載器實(shí)例或者標(biāo)準(zhǔn)擴(kuò)展類的實(shí)例基本上在整個(gè)運(yùn)行期間總能直接或者間接的訪問的到,其達(dá)到unreachable的可能性極小.
??? 3、被開發(fā)者自定義的類加載器實(shí)例加載的類型只有在很簡單的上下文環(huán)境中才能被卸載,而且一般還要借助于強(qiáng)制調(diào)用虛擬機(jī)的垃圾收集功能才可以做到.
綜合以上三點(diǎn),我們可以默認(rèn)前面的結(jié)論 一個(gè)已經(jīng)加載的類型被卸載的幾率很小至少被卸載的時(shí)間是不確定的..
關(guān)于java.lang.OutOfMemoryErrorPermGen space異常,網(wǎng)上曾有過相關(guān)的討論,可通過搜索找到更多的內(nèi)容。
通過上面的說明,我們在自定義ClassLoader中實(shí)現(xiàn)加載過程時(shí),就可以將先將原加載器設(shè)置為null,相關(guān)的加載信息也都設(shè)置為null后再調(diào)用System.gc();作一次GC.
?
關(guān)于類加載的說明,有幾篇可作參考:
Java類加載原理解析 http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html
入探討 Java 類加載器 http://www.ibm.com/developerworks/cn/java/j-lo-classloader/
深入研究Java類加載機(jī)制? http://lavasoft.blog.51cto.com/62575/184547/
http://nonopo.iteye.com/blog/208007
http://nonopo.iteye.com/blog/208012
?
轉(zhuǎn)載于:https://www.cnblogs.com/jevo/archive/2013/04/22/3019573.html
總結(jié)
- 上一篇: C#下的Windows服务通用壳程序(二
- 下一篇: java美元兑换,(Java实现) 美元