靜態庫、動態連接庫
程序編制一般需經編輯、編譯、連接、加載和運行幾個步驟。在我們的應用中,有一些公共代碼是需要反復使用,就把這些代碼編譯為“庫”文件;在連接步驟中,連接器將從庫文件取得所需的代碼,復制到生成的可執行文件中。這種庫稱為靜態庫,其特點是可執行文件中包含了庫代碼的一份完整拷貝;缺點就是被多次使用就會有多份冗余拷貝。
為了克服這個缺點可以采用動態連接庫。這個時候連接器僅僅是在可執行文件中打上標志,說明需要使用哪些動態連接庫;當運行程序時,加載器根據這些標志把所需的動態連接庫加載到內存。
另外在當前的編程環境中,一般都提供方法讓程序在運行的時候把某個特定的動態連接庫加載并運行,也可以將其卸載(例如Win32的LoadLibrary()&FreeLibrary()和Posix的dlopen()&dlclose())。這個功能被廣泛地用于在程序運行時刻更新某些功能模塊或者是程序外觀。
What is ClassLoader?
與普通程序不同的是,Java程序(class文件)并不是本地的可執行程序。當運行Java程序時,首先運行JVM(Java虛擬機),然后再把Java class加載到JVM里頭運行,負責加載Java class的這部分就叫做Class Loader。
類加載器是負責加載類的對象。ClassLoader 類是一個抽象類。如果給定類的二進制名稱,那么類加載器會試圖查找或生成構成類定義的數據。一般策略是將名稱轉換為某個文件名,然后從文件系統讀取該名稱的“類文件”。
ClassLoader 的基本目標是對類的請求提供服務。當 JVM 需要使用類時,它根據名稱向 ClassLoader 請求這個類,然后 ClassLoader 試圖返回一個表示這個類的 Class 對象。 通過覆蓋對應于這個過程不同階段的方法,可以創建定制的 ClassLoader。
Thread.currentThread().getContextClassLoader()指的是取得當前線程的ClassLoader,而Class.forName()其實也是使用當前線程的ClassLoader裝入一個類,二者并沒有本質的區別。
Class.forName()只是為了方便裝入類而寫的一個靜態方法,不知為何Banq總是拿來和ClassLoader作比較,二者不是同一個級別的東西。
通過調用Thread.currentThread().setContextClassLoader(ClassLaoder load)方法,Class.forName()也可以裝入其他ClassLoader范圍內的類。
JVM本身包含了一個ClassLoader稱為Bootstrap ClassLoader,和JVM一樣,Bootstrap ClassLoader是用本地代碼實現的,它負責加載核心Java Class(即所有java.*開頭的類)。另外JVM還會提供兩個ClassLoader,它們都是用Java語言編寫的,由Bootstrap ClassLoader加載;其中Extension ClassLoader負責加載擴展的Java class(例如所有javax.*開頭的類和存放在JRE的ext目錄下的類),Application ClassLoader負責加載應用程序自身的類。
Classloader存在下面問題:
在一個JVM中可能存在多個ClassLoader,每個ClassLoader擁有自己的NameSpace。一個ClassLoader只能擁有一個class對象類型的實例,但是不同的ClassLoader可能擁有相同的class對象實例,這時可能產生致命的問題。如ClassLoaderA,裝載了類A的類型實例A1,而ClassLoaderB,也裝載了類A的對象實例A2。邏輯上講A1=A2,但是由于A1和A2來自于不同的ClassLoader,它們實際上是完全不同的,如果A中定義了一個靜態變量c,則c在不同的ClassLoader中的值是不同的。

When to load the class?
什么時候JVM會使用ClassLoader加載一個類呢?當你使用java去執行一個類,JVM使用Application ClassLoader加載這個類;然后如果類A引用了類B,不管是直接引用還是用Class.forName()引用,JVM就會找到加載類A的ClassLoader,并用這個ClassLoader來加載類B。
2, 一些重要的方法
A) 方法 loadClass
ClassLoader.loadClass() 是 ClassLoader 的入口點。該方法的定義如下:
Class loadClass( String name, boolean resolve );
name JVM 需要的類的名稱,如 Foo 或 java.lang.Object。
resolve 參數告訴方法是否需要解析類。在準備執行類之前,應考慮類解析。并不總是需要解析。如果 JVM 只需要知道該類是否存在或找出該類的超類,那么就不需要解析。
B) 方法 defineClass
defineClass 方法是 ClassLoader 的主要訣竅。該方法接受由原始字節組成的數組并把它轉換成 Class 對象。原始數組包含如從文件系統或網絡裝入的數據。defineClass 管理 JVM 的許多復雜、神秘和倚賴于實現的方面 -- 它把字節碼分析成運行時數據結構、校驗有效性等等。不必擔心,您無需親自編寫它。事實上,即使您想要這么做也不能覆蓋它,因為它已被標記成final的。

C) 方法 findSystemClass
findSystemClass 方法從本地文件系統裝入文件。它在本地文件系統中尋找類文件,如果存在,就使用 defineClass 將原始字節轉換成 Class 對象,以將該文件轉換成類。當運行 Java 應用程序時,這是 JVM 正常裝入類的缺省機制。(Java 2 中 ClassLoader 的變動提供了關于 Java 版本 1.2 這個過程變動的詳細信息。) 對于定制的 ClassLoader,只有在嘗試其它方法裝入類之后,再使用 findSystemClass。原因很簡單:ClassLoader 是負責執行裝入類的特殊步驟,不是負責所有類。例如,即使 ClassLoader 從遠程的 Web 站點裝入了某些類,仍然需要在本地機器上裝入大量的基本 Java 庫。而這些類不是我們所關心的,所以要 JVM 以缺省方式裝入它們:從本地文件系統。這就是 findSystemClass 的用途。

D) 方法 resolveClass
正如前面所提到的,可以不完全地(不帶解析)裝入類,也可以完全地(帶解析)裝入類。當編寫我們自己的 loadClass 時,可以調用 resolveClass,這取決于 loadClass 的 resolve 參數的值。


E) 方法 findLoadedClass
findLoadedClass 充當一個緩存:當請求 loadClass 裝入類時,它調用該方法來查看 ClassLoader 是否已裝入這個類,這樣可以避免重新裝入已存在類所造成的麻煩。應首先調用該方法。

怎么組裝這些方法
1) 調用 findLoadedClass 來查看是否存在已裝入的類。
2) 如果沒有,那么采用那種特殊的神奇方式來獲取原始字節。
3) 如果已有原始字節,調用 defineClass 將它們轉換成 Class 對象。
4) 如果沒有原始字節,然后調用 findSystemClass 查看是否從本地文件系統獲取類。
5) 如果 resolve 參數是 true,那么調用 resolveClass 解析 Class 對象。
6) 如果還沒有類,返回 ClassNotFoundException。
看看jdk中classLoader的代碼:
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException

{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);

if (c == null)
{

try
{

if (parent != null)
{
c = parent.loadClass(name, false);

} else
{
c = findBootstrapClass0(name);
}

} catch (ClassNotFoundException e)
{
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}

if (resolve)
{
resolveClass(c);
}
return c;
1. 調用findLoadedClass(String):Class 檢查一下這個class是否已經被加載過了,由于JVM 規范規定ClassLoader可以cache它所加載的Class,因此如果一個class已經被加載過的話,直接從cache中獲取即可。
2. 調用它的parent 的loadClass()方法,如果parent為空,這使用JVM內部的class loader(即著名的bootstrap classloader)。
3. 如果上面兩步都沒有找到,調用findClass(String)方法。
Why use your own ClassLoader?
似乎JVM自身的ClassLoader已經足夠了,為什么我們還需要創建自己的ClassLoader呢?
因為JVM自帶的ClassLoader只是懂得從本地文件系統加載標準的java class文件,如果編寫你自己的ClassLoader,你可以做到:
1)在執行非置信代碼之前,自動驗證數字簽名
2)動態地創建符合用戶特定需要的定制化構建類
3)從特定的場所取得java class,例如數據庫中
4) 等等
事實上當使用Applet的時候,就用到了特定的ClassLoader,因為這時需要從網絡上加載java class,并且要檢查相關的安全信息。
目前的應用服務器大都使用了ClassLoader技術,即使你不需要創建自己的ClassLoader,了解其原理也有助于更好地部署自己的應用。
ClassLoader Tree & Delegation Model
當你決定創建你自己的ClassLoader時,需要繼承java.lang.ClassLoader或者它的子類。在實例化每個ClassLoader對象時,需要指定一個父對象;如果沒有指定的話,系統自動指定ClassLoader.getSystemClassLoader()為父對象。如下圖:
在Java 1.2后,java class的加載采用所謂的委托模式(Delegation Modle),當調用一個ClassLoader.loadClass()加載一個類的時候,將遵循以下的步驟:
1)檢查這個類是否已經被加載進來了?
2)如果還沒有加載,調用父對象加載該類
3)如果父對象無法加載,調用本對象的findClass()取得這個類。
所以當創建自己的Class Loader時,只需要重載findClass()這個方法。

publicclass AnotherClassLoader extends ClassLoader
{

private String baseDir;
privatestaticfinal Logger LOG = Logger.getLogger(AnotherClassLoader.class);

public AnotherClassLoader (ClassLoader parent, String baseDir)
{
super(parent);
this.baseDir = baseDir;
}

protected Class findClass(String name)throws ClassNotFoundException
{
LOG.debug("findClass " + name);
byte[] bytes = loadClassBytes(name);
Class theClass = defineClass(name, bytes, 0, bytes.length);//A
if (theClass == null)
thrownew ClassFormatError();return theClass;
}

private byte[] loadClassBytes(String className) throws ClassNotFoundException
{

try
{
String classFile = getClassFile(className);
FileInputStream fis = new FileInputStream(classFile);
FileChannel fileC = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel outC = Channels.newChannel(baos);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

while (true)
{
int i = fileC.read(buffer);
if (i == 0 || i == -1)
break;
buffer.flip();
outC.write(buffer);
buffer.clear();
}
fis.close();
return baos.toByteArray(); }

catch (IOException fnfe)
{
thrownew ClassNotFoundException(className);
} }

private String getClassFile(String name)
{
StringBuffer sb = new StringBuffer(baseDir);
name = name.replace('.', File.separatorChar) + ".class";
sb.append(File.separator + name);
return sb.toString();
}
}

很簡單的代碼,關鍵的地方就在A處,我們使用了defineClass方法,目的在于把從class文件中得到的二進制數組轉換為相應的Class實例。defineClass是一個native的方法,它替我們識別class文件格式,分析讀取相應的數據結構,并生成一個class實例。
還沒完呢,我們只是找到了發布在某個目錄下的class,還有資源呢。我們有時會用Class.getResource():URL來獲取相應的資源文件。如果僅僅使用上面的ClassLoader是找不到這個資源的,相應的返回值為null。

public java.net.URL getResource(String name)
{
name = resolveName(name);
ClassLoader cl = getClassLoader0();//這里
if (cl==null)

{// Asystem class.
return ClassLoader.getSystemResource(name); }
return cl.getResource(name);
}
原來是使用加載這個class的那個classLoader獲取得資源。

public URL getResource(String name)
{
URL url;

if (parent != null)
{
url = parent.getResource(name); }

else
{
url = getBootstrapResource(name); }

if (url == null)
{
url = findResource(name);//這里 }return url;
}

這樣看來只要繼承findResource(String)方法就可以了。修改以下我們的代碼:
//新增的一個findResource方法

protected URL findResource(String name)
{
LOG.debug("findResource " + name);

try
{
URL url = super.findResource(name);
if (url != null)
return url;
url = new URL("file:///" + converName(name));//簡化處理,所有資源從文件系統中獲取
return url; }

catch (MalformedURLException mue)
{
LOG.error("findResource", mue);
return null; }
}

private String converName(String name)
{
StringBuffer sb = new StringBuffer(baseDir);
name = name.replace('.', File.separatorChar);
sb.append(File.separator + name);
return sb.toString();
}


Unloading? Reloading?
當一個java class被加載到JVM之后,它有沒有可能被卸載呢?我們知道Win32有FreeLibrary()函數,Posix有dlclose()函數可以被調用來卸載指定的動態連接庫,但是Java并沒有提供一個UnloadClass()的方法來卸載指定的類。
在Java中,java class的卸載僅僅是一種對系統的優化,有助于減少應用對內存的占用。既然是一種優化方法,那么就完全是JVM自行決定如何實現,對Java開發人員來說是完全透明的。
在什么時候一個java class/interface會被卸載呢?Sun公司的
事實上我們關心的不是如何卸載類的,我們關心的是如何更新已經被加載了的類從而更新應用的功能。JSP則是一個非常典型的例子,如果一個JSP文件被更改了,應用服務器則需要把更改后的JSP重新編譯,然后加載新生成的類來響應后繼的請求。
其實一個已經加載的類是無法被更新的,如果你試圖用同一個ClassLoader再次加載同一個類,就會得到異常(java.lang.LinkageError: duplicate class definition),我們只能夠重新創建一個新的ClassLoader實例來再次加載新類。至于原來已經加載的類,開發人員不必去管它,因為它可能還有實例正在被使用,只要相關的實例都被內存回收了,那么JVM就會在適當的時候把不會再使用的類卸載。