前面有提到過Tomcat的熱部署,所謂熱部署就是在應用運行時更新Java類文件以升級軟件功能,升級過程不需要關停和重啟應用。要進行熱部署需要做class熱替換。Class熱替換實現了將修改的class再次加載到JVM中,以動態替換內存中原有的class字節碼。
實現class的熱替換就與Java類加載過程相關,關於Java類加載過程的文章或書籍早些年就已經很多了,這里從” 深入探討 Java 類加載器(http://www.ibm.com/developerworks/cn/java/j-lo-classloader/)’’一文中摘錄了部分內容說明Java類的加載過程。
類加載器基本概念
基本上所有的類加載器都是 java.lang.ClassLoader類的一個實例。java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,然后從這些字節代碼中定義出一個 Java 類,即 java.lang.Class類的一個實例。除此之外,ClassLoader還負責加載 Java 應用所需的資源,如圖像文件和配置文件等。
Java 中的類加載器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。系統提供的類加載器主要有下面三個:
• 引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader。
• 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄里面查找並加載 Java 類。
• 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。
除了系統提供的類加載器以外,開發人員可以通過繼承 java.lang.ClassLoader類的方式實現自己的類加載器,以滿足一些特殊的需求。
除了引導類加載器之外,所有的類加載器都有一個父類加載器。通過getParent()方法可以得到。對於系統提供的類加載器來說,系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器;對於開發人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。因為類加載器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類加載器。
1、Bootstrap Loader(啟動類加載器):加載System.getProperty("sun.boot.class.path")所指定的路徑或jar。
2、Extended Loader(標准擴展類加載器ExtClassLoader):加載System.getProperty("java.ext.dirs")所指定的路徑或jar。在使用Java運行程序時,也可以指定其搜索路徑.
3、AppClass Loader(系統類加載器AppClassLoader):加載System.getProperty("java.class.path")所指定的路徑或jar。在使用Java運行程序時,也可以加上-cp來覆蓋原有的Classpath設置.
類加載器的代理模式
類加載器在嘗試自己去查找某個類的字節代碼並定義它時,會先代理給其父類加載器,由父類加載器先去嘗試加載這個類,依次類推。代理模式是為了保證 Java 核心庫的類型安全。通過代理模式,對於 Java 核心庫的類的加載工作由引導類加載器來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的。
不同的類加載器為相同名稱的類創建了額外的名稱空間。相同名稱的類可以並存在 Java 虛擬機中,只需要用不同的類加載器來加載它們即可。不同類加載器加載的類之間是不兼容的,這就相當於在 Java 虛擬機內部創建了一個個相互隔離的 Java 類空間。
加載類的過程
真正完成類的加載工作是通過調用 defineClass來實現的;而啟動類的加載過程是通過調用 loadClass來實現的。前者稱為一個類的定義加載器(defining loader),后者稱為初始加載器(initiating loader)。在 Java 虛擬機判斷兩個類是否相同的時候,使用的是類的定義加載器。也就是說,哪個類加載器啟動類的加載過程並不重要,重要的是最終定義這個類的加載器。兩種類加載器的關聯之處在於:一個類的定義加載器是它引用的其它類的初始加載器。如類 com.example.Outer引用了類 com.example.Inner,則由類 com.example.Outer的定義加載器負責啟動類 com.example.Inner的加載過程。
方法 loadClass()拋出的是 java.lang.ClassNotFoundException異常;方法 defineClass()拋出的是 java.lang.NoClassDefFoundError異常。
類加載器在成功加載某個類之后,會把得到的 java.lang.Class類的實例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,對於一個類加載器實例來說,相同全名的類只加載一次,即 loadClass方法不會被重復調用。
類加載過程:
1、尋找jre目錄,尋找jvm.dll,並初始化JVM;
2、產生一個Bootstrap Loader(啟動類加載器);
3、Bootstrap Loader自動加載Extended Loader(標准擴展類加載器),並將其父Loader設為Bootstrap Loader。
4、Bootstrap Loader自動加載AppClass Loader(系統類加載器),並將其父Loader設為Extended Loader。
5、最后由AppClass Loader加載應用程序類。
我們再來回顧一下類的加載過程:前面提到,某一特定類加載器在加載類時,首先將加載任務委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;如果父類加載器無法完成加載,則由該特定類加載器完成加載。這個機制就是所謂的雙親委派機制。
我們再來看一下Java 虛擬機是如何判定兩個 Java 類是相同的。Java 虛擬機不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。即便是同樣的字節代碼,被不同的類加載器加載之后所得到的類,也是不同的。具體的示例可以參見”深入探討 Java 類加載器”一文。
了解了基本概念,我們知道要實現Java類的熱替換,首先就需要繼承java.lang.ClassLoader類實現自己的類加載器。
定義一個類加載器,如下:
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;
//首先檢查請求的類型是否已經被這個類裝載器裝載到命名空間中了,如果已經裝載,直接返回;否則繼續。
clazz = findLoadedClass(name); //查找名稱為 name
的已經被加載過的類
if (clazz != null) {
if (resolve) {
resolveClass(clazz); //鏈接指定的 Java 類
}
//如果class類被修改過,則重新加載
if (isModify(name)) {
MyClassLoader hcl = new MyClassLoader(url);
clazz = customLoad(name, hcl);
}
return (clazz);
}
//如果類的包名為"java."開始,則有系統默認加載器加載
if (!name.startsWith("org.jevo.")) {
try {
//得到系統默認的加載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 {
// //調用本類加載器的findClass(…)方法,試圖獲取對應的字節碼,如果獲取的到,則調用defineClass(…)導入類型到方法區;否則拋出異常
Class clazz = ((MyClassLoader) cl).findClass(name); //查找名稱為 name的類
if (resolve)
((MyClassLoader) cl).resolveClass(clazz);
//緩存加載class文件的最后修改時間
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)); //這里使用反射機制來執行事務。
} else if (cmd.equals("RELOAD")) {
loadNewVersionOfServer();
}
}
}
public static void main(String[] args) {
try {
test();
} catch (Exception e) {
e.printStackTrace();
}
}
}
在上面的代碼中,使用了反射機制來執行事務。這里不能將hotClazz.newInstance()得到的實例強制轉換成DyService對象實例來使用doBussiness方法,否則會拋出java.lang.ClassCastException異常。拋出ClassCastException異常的原因就在於前面提到的“Java 虛擬機如何判定兩個 Java 類是相同的”,這里例子中的hotClazz 是由MyClassLoader加載的,而server變量類型聲明和類是由loadNewVersionOfServer方法所屬的類的加載器加載的,因此屬於不同的兩個類型,轉換時並不兼容,所以會拋出ClassCastException異常。
同樣,在上面例子中,當class類被修改過后重新加載時,我們是通過重新new一個MyClassLoader來加載被修改類的,MyClassLoader hcl = new MyClassLoader(url);
在前面我們提到:對於一個類加載器實例來說,相同全名的類只加載一次。所以同一個ClassLoader實例只能加載Class一次,一個class被一個ClassLoader實例加載過的話,就不能再被這個ClassLoader實例再次加載,即不再重復調defineClass()方法關聯字節碼,重復裝載將拋出重復類定義異常。同樣,系統默認的ClassLoader加載器內部會緩存加載過的class,重新加載的話,就直接取緩存。因此這里只能重新創建一個ClassLoader,然后再去加載已經被加載過的class文件。
AppClass Loader(系統類加載器AppClassLoader):加載System.getProperty("java.class.path")所指定的路徑或jar
前面我們通過模擬loadClass方法的過程來加載Java類,我們也提到” 真正完成類的加載工作是通過調用 defineClass來實現的;而啟動類的加載過程是通過調用 loadClass來實現的。”同時我們也知道系統類加載器一般加載ClassPath所指定的路徑下的class,所以我們可以使用類定義加載器來加載ClassPath指定路徑下的class來實現熱替換,代碼如下:
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區域。在上面的代碼中,如果加載的class文件很多,那么可能導致PermGen space區域空間溢出,即java.lang.OutOfMemoryErrorPermGen space. 異常。
‘Java虛擬機類型卸載和類型更新解析(http://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html)’中作了一些說明,摘錄部分內容如下:
首先看一下,關於java虛擬機規范中時如何闡述類型卸載(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虛擬機規范中關於類型卸載的內容就這么簡單兩句話,大致意思就是:只有當加載該類型的類加載器實例(非類加載器類型)為unreachable狀態時,當前被加載的類型才被卸載.啟動類加載器實例永遠為reachable狀態,由啟動類加載器加載的類型可能永遠不會被卸載.
我們再看一下Java語言規范提供的關於類型卸載的更詳細的信息(部分摘錄):
//摘自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
通過以上我們可以得出結論: 類型卸載(unloading)僅僅是作為一種減少內存使用的性能優化措施存在的,具體和虛擬機實現有關,對開發者來說是透明的.
縱觀java語言規范及其相關的API規范,找不到顯示類型卸載(unloading)的接口, 換句話說:
1、一個已經加載的類型被卸載的幾率很小至少被卸載的時間是不確定的
2、一個被特定類加載器實例加載的類型運行時可以認為是無法被更新的
有 關unreachable狀態的解釋:
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.
某種程度上講,在一個稍微復雜的java應用中,我們很難准確判斷出一個實例是否處於unreachable狀態.
關於類型卸載大致概括為:
1、有啟動類加載器加載的類型在整個運行期間是不可能被卸載的(jvm和jls規范).
2、被系統類加載器和標准擴展類加載器加載的類型在運行期間不太可能被卸載,因為系統類加載器實例或者標准擴展類的實例基本上在整個運行期間總能直接或者間接的訪問的到,其達到unreachable的可能性極小.
3、被開發者自定義的類加載器實例加載的類型只有在很簡單的上下文環境中才能被卸載,而且一般還要借助於強制調用虛擬機的垃圾收集功能才可以做到.
綜合以上三點,我們可以默認前面的結論 一個已經加載的類型被卸載的幾率很小至少被卸載的時間是不確定的..
關於java.lang.OutOfMemoryErrorPermGen space異常,網上曾有過相關的討論,可通過搜索找到更多的內容。
通過上面的說明,我們在自定義ClassLoader中實現加載過程時,就可以將先將原加載器設置為null,相關的加載信息也都設置為null后再調用System.gc();作一次GC.
關於類加載的說明,有幾篇可作參考:
Java類加載原理解析 http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html
入探討 Java 類加載器 http://www.ibm.com/developerworks/cn/java/j-lo-classloader/
深入研究Java類加載機制 http://lavasoft.blog.51cto.com/62575/184547/
http://nonopo.iteye.com/blog/208007
http://nonopo.iteye.com/blog/208012