❶ java靜態資源(靜態方法,靜態屬性)是程序一運行就載入到jvm中,還是當被調用的時候才進行載入呢
java靜態資源(靜態方法,靜態屬性)是程序一運行就載入到jvm中的。
1、類中的靜態屬性會被加入到類對象(也可以叫做類的模板,是類的描述) 的構造器中,靜態方法也會被加入到類對象中。
2、當第一次使用類時,JVM會通過類載入器,載入類對象,從而初始化靜態屬性,並裝入類的方法,包括靜態方法和實例方法(方法不會被調用,只是載入,從這個意義上來說,靜態方法和實例方法是類似的)。
3、當創建類的實例對象時,JVM會調用類的構造器,從而初始化類的屬性。
(1)java類載入過程擴展閱讀:
JVM 類載入機制
JVM類載入機制分為五個部分:載入,驗證,准備,解析,初始化,下面我們就分別來看一下這五個過程。
1、載入
載入是類載入過程中的一個階段,這個階段會在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的入口。注意這里不一定非得要從一個Class文件獲取,這里既可以從ZIP包中讀取(比如從jar包和war包中讀取),也可以在運行時計算生成(動態代理),也可以由其它文件生成(比如將jsP文件轉換成對應的Class類)。
2、驗證
這一階段的主要目的是為了確保Class文件的位元組流中包含的信息是否符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
3、准備
准備階段是正式為類變數分配內存並設置類變數的初始值階段,即在方法區中分配這些變數所使用的內存空間。
4、解析
解析階段是指虛擬機將常量池中的符號引用替換為直接引用的過程。符號引用就是class文件中的:
CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
等類型的常量。
5、初始化
初始化階段是類載入最後一個階段,前面的類載入階段之後,除了在載入階段可以自定義類載入器以外,其它操作都由JVM主導。到了初始階段,才開始真正執行類中定義的Java程序代碼。初始化階段是執行類構造器<client>方法的過程。
❷ java類載入順序
記住 3 條原則:
1、父類優先於子類
2、屬性優先於代碼塊優先於構造方法
3、靜態優先於非靜態
因此,類載入順序為:
父類靜態變數->父類靜態語句塊->子類靜態變數->子類靜態語句塊->父類普通成員變數->父類動態語句塊->父類構造器->子類普通成員變數->子類動態語句塊->子類構造器
❸ Java類的熱替換——概念、設計與實現
對於許多關鍵性業務或者龐大的 Java 系統來說 如果必須暫停系統服務才能進行系統升級 既會大大影響到系統的可用性 同時也增加了系統的管理和維護成本 因此 如果能夠方便地在不停止系統業務的情況下進行系統升級 則可以很好地解決上述問題 在本文中 我們將基於實例 對構建在線升級 Java 系統的基礎技術和設計原則進行了深入的講解 相信讀者能夠根據文中的技術構建出自己的在線升級系統來
Java ClassLoader 技術剖析
在本文中 我們將不對 Java ClassLoader 的細節進行過於詳細的講解 而是關注於和構建在線升級系統相關的基礎概念 關於 ClassLoader 的詳細細節許多資料可以參考 有興趣的讀者可以自行研讀
要構建在線升級系統 一個重要的技術就判如是能夠實現 Java 類的熱替換 —— 也就是在不停止正在運行的系統的情況下進行類(對象)的升級替換 而 Java 的 ClassLoader 正是實現這項技術的基礎
在 Java 中 類銷數的實例化流程分為兩個部分 類的載入和類的實例化 類的載入又分為顯式載入和隱式載入 大家使用 new 關鍵字創建類實例時 其實就隱式地包含了類的載入過程 對於類的顯式載入來說 比較常用的是 Class forName 其實 它們都是通過調用 ClassLoader 類的 loadClass 方法來完成類的實際載入工作的 直接調用 ClassLoader 的 loadClass 方法是另外一種不常用的顯式載入類的技術
圖 Java 類載入器層次結構圖
ClassLoader 在載入類時有一定的層次關系和規則 在 Java 中 有四種類型的類載入器 分別為 BootStrapClassLoader ExtClassLoader AppClassLoader 以及用戶自定義的 ClassLoader 這四種類載入器分別負責不同路徑的類的載入 並形成了一個類載入的層次結構
BootStrapClassLoader 處於類載入器層次結構的最高層 負責 sun boot class path 路徑下類的載入 默認為 jre/lib 目錄下的核心 API 或 Xbootclasspath 選項指定的 jar 包 ExtClassLoader 的載入路徑為 java ext dirs 默認為 jre/lib/ext 目錄或者 Djava ext dirs 指定目錄下的 jar 包載入 AppClassLoader 的載入路徑為 java class path 默認為環境變數 CLASSPATH 中設定的值 也可以通過 classpath 選型進行指定 用戶自定義 ClassLoader 可以根據用戶的需要定製自己的類載入過程 在運行期進行指定類的動態實時載入
這四種類載入器的層次關系圖如 圖 所示 一般來說 這四種類載入器會形成一種父子關系 高層為低層的父載入器 在進行類載入時 首先會自底向上挨個檢查是否已經載入了指定類 如果已經載入則直接返回該類的引用 如果到最高層也沒有載入過指定類 那麼會自頂向下挨個嘗試載入 直到用戶自定義類載入器 如果還不能成功 就會拋出異常 Java 類的載入過程如 圖 所示
圖 Java 類的載入過程
每個類載入器有自己的名字空間 對於同一個類載入器實例來說 名字相同的類只能存在一個 並且僅載入一次 不管該類有沒有變化 下次再需要載入時 它只是從自己的緩存中直接返回已經載入過的類引用
我們編寫的應用類默認情況下都是通過 AppClassLoader 進行載入的 當虧沖首我們使用 new 關鍵字或者 Class forName 來載入類時 所要載入的類都是由調用 new 或者 Class forName 的類的類載入器(也是 AppClassLoader)進行載入的 要想實現 Java 類的熱替換 首先必須要實現系統中同名類的不同版本實例的共存 通過上面的介紹我們知道 要想實現同一個類的不同版本的共存 我們必須要通過不同的類載入器來載入該類的不同版本 另外 為了能夠繞過 Java 類的既定載入過程 我們需要實現自己的類載入器 並在其中對類的載入過程進行完全的控制和管理
編寫自定義的 ClassLoader
為了能夠完全掌控類的載入過程 我們的定製類載入器需要直接從 ClassLoader 繼承 首先我們來介紹一下 ClassLoader 類中和熱替換有關的的一些重要方法
findLoadedClass 每個類載入器都維護有自己的一份已載入類名字空間 其中不能出現兩個同名的類 凡是通過該類載入器載入的類 無論是直接的還是間接的 都保存在自己的名字空間中 該方法就是在該名字空間中尋找指定的類是否已存在 如果存在就返回給類的引用 否則就返回 null 這里的直接是指 存在於該類載入器的載入路徑上並由該載入器完成載入 間接是指 由該類載入器把類的載入工作委託給其他類載入器完成類的實際載入
getSystemClassLoader Java 中新增的方法 該方法返回系統使用的 ClassLoader 可以在自己定製的類載入器中通過該方法把一部分工作轉交給系統類載入器去處理
defineClass 該方法是 ClassLoader 中非常重要的一個方法 它接收以位元組數組表示的類位元組碼 並把它轉換成 Class 實例 該方法轉換一個類的同時 會先要求裝載該類的父類以及實現的介面類
loadClass 載入類的入口方法 調用該方法完成類的顯式載入 通過對該方法的重新實現 我們可以完全控制和管理類的載入過程
resolveClass 鏈接一個指定的類 這是一個在某些情況下確保類可用的必要方法 詳見 Java 語言規范中 執行 一章對該方法的描述
了解了上面的這些方法 下面我們來實現一個定製的類載入器來完成這樣的載入流程 我們為該類載入器指定一些必須由該類載入器直接載入的類集合 在該類載入器進行類的載入時 如果要載入的類屬於必須由該類載入器載入的集合 那麼就由它直接來完成類的載入 否則就把類載入的工作委託給系統的類載入器完成
在給出示例代碼前 有兩點內容需要說明一下 要想實現同一個類的不同版本的共存 那麼這些不同版本必須由不同的類載入器進行載入 因此就不能把這些類的載入工作委託給系統載入器來完成 因為它們只有一份 為了做到這一點 就不能採用系統默認的類載入器委託規則 也就是說我們定製的類載入器的父載入器必須設置為 null 該定製的類載入器的實現代碼如下
class CustomCL extends ClassLoader {
private String basedir; // 需要該類載入器直接載入的類文件的基目錄
private HashSet dynaclazns; // 需要由該類載入器直接載入的類名
public CustomCL(String basedir String[] clazns) {
super(null); // 指定父類載入器為 null
this basedir = basedir;
dynaclazns = new HashSet();
loadClassByMe(clazns);
}
private void loadClassByMe(String[] clazns) {
for (int i = ; i < clazns length; i++) {
loadDirectly(clazns[i]);
dynaclazns add(clazns[i]);
}
}
private Class loadDirectly(String name) {
Class cls = null;
StringBuffer *** = new StringBuffer(basedir);
String classname = name replace( File separatorChar) + class ;
*** append(File separator + classname);
File classF = new File( *** toString());
cls = instantiateClass(name new FileInputStream(classF)
classF length());
return cls;
}
private Class instantiateClass(String name InputStream fin long len){
byte[] raw = new byte[(int) len];
fin read(raw);
fin close();
return defineClass(name raw raw length);
}
protected Class loadClass(String name boolean resolve)
throws ClassNotFoundException {
Class cls = null;
cls = findLoadedClass(name);
if(!ntains(name) && cls == null)
cls = getSystemClassLoader() loadClass(name);
if (cls == null)
throw new ClassNotFoundException(name);
if (resolve)
resolveClass(cls);
return cls;
}
}
在該類載入器的實現中 所有指定必須由它直接載入的類都在該載入器實例化時進行了載入 當通過 loadClass 進行類的載入時 如果該類沒有載入過 並且不屬於必須由該類載入器載入之列都委託給系統載入器進行載入 理解了這個實現 距離實現類的熱替換就只有一步之遙了 我們在下一小節對此進行詳細的講解
實現 Java 類的熱替換
在本小節中 我們將結合前面講述的類載入器的特性 並在上小節實現的自定義類載入器的基礎上實現 Java 類的熱替換 首先我們把上小節中實現的類載入器的類名 CustomCL 更改為 HotswapCL 以明確表達我們的意圖
現在來介紹一下我們的實驗方法 為了簡單起見 我們的包為默認包 沒有層次 並且省去了所有錯誤處理 要替換的類為 Foo 實現很簡單 僅包含一個方法 sayHello
清單 待替換的示例類
public class Foo{
public void sayHello() {
System out println( hello world! (version one) );
}
}
在當前工作目錄下建立一個新的目錄 swap 把編譯好的 Foo class 文件放在該目錄中 接下來要使用我們前面編寫的 HotswapCL 來實現該類的熱替換 具體的做法為 我們編寫一個定時器任務 每隔 秒鍾執行一次 其中 我們會創建新的類載入器實例載入 Foo 類 生成實例 並調用 sayHello 方法 接下來 我們會修改 Foo 類中 sayHello 方法的列印內容 重新編譯 並在系統運行的情況下替換掉原來的 Foo class 我們會看到系統會列印出更改後的內容 定時任務的實現如下(其它代碼省略 請讀者自行補齊)
public void run(){
try {
// 每次都創建出一個新的類載入器
HowswapCL cl = new HowswapCL( /swap new String[]{ Foo });
Class cls = cl loadClass( Foo );
Object foo = cls newInstance();
Method m = foo getClass() getMethod( sayHello new Class[]{});
m invoke(foo new Object[]{});
} catch(Exception ex) {
ex printStackTrace();
}
}
編譯 運行我們的系統 會出現如下的列印
圖 熱替換前的運行結果
好 現在我們把 Foo 類的 sayHello 方法更改為
public void sayHello() {
System out println( hello world! (version o) );
}
在系統仍在運行的情況下 編譯 並替換掉 swap 目錄下原來的 Foo class 文件 我們再看看屏幕的列印 奇妙的事情發生了 新更改的類在線即時生效了 我們已經實現了 Foo 類的熱替換 屏幕列印如下
圖 熱替換後的運行結果
敏銳的讀者可能會問 為何不用把 foo 轉型為 Foo 直接調用其 sayHello 方法呢?這樣不是更清晰明了嗎?下面我們來解釋一下原因 並給出一種更好的方法
如果我們採用轉型的方法 代碼會變成這樣 Foo foo = (Foo)cls newInstance(); 讀者如果跟隨本文進行試驗的話 會發現這句話會拋出 ClassCastException 異常 為什麼嗎?因為在 Java 中 即使是同一個類文件 如果是由不同的類載入器實例載入的 那麼它們的類型是不相同的 在上面的例子中 cls 是由 HowswapCL 載入的 而 foo 變數類型聲名和轉型里的 Foo 類卻是由 run 方法所屬的類的載入器(默認為 AppClassLoader)載入的 因此是完全不同的類型 所以會拋出轉型異常
那麼通過介面調用是不是就行了呢?我們可以定義一個 IFoo 介面 其中聲名 sayHello 方法 Foo 實現該介面 也就是這樣 IFoo foo = (IFoo)cls newInstance(); 本來該方法也會有同樣的問題的 因為外部聲名和轉型部分的 IFoo 是由 run 方法所屬的類載入器載入的 而 Foo 類定義中 implements IFoo 中的 IFoo 是由 HotswapCL 載入的 因此屬於不同的類型轉型還是會拋出異常的 但是由於我們在實例化 HotswapCL 時是這樣的
HowswapCL cl = new HowswapCL( /swap new String[]{ Foo });
其中僅僅指定 Foo 類由 HotswapCL 載入 而其實現的 IFoo 介面文件會委託給系統類載入器載入 因此轉型成功 採用介面調用的代碼如下
清單 採用介面調用的代碼
public void run(){
try {
HowswapCL cl = new HowswapCL( /swap new String[]{ Foo });
Class cls = cl loadClass( Foo );
IFoo foo = (IFoo)cls newInstance();
foo sayHello();
} catch(Exception ex) {
ex printStackTrace();
}
}
確實 簡潔明了了很多 在我們的實驗中 每當定時器調度到 run 方法時 我們都會創建一個新的 HotswapCL 實例 在產品代碼中 無需如此 僅當需要升級替換時才去創建一個新的類載入器實例
在線升級系統的設計原則
在上小節中 我們給出了一個 Java 類熱替換的實例 掌握了這項技術 就具備了實現在線升級系統的基礎 但是 對於一個真正的產品系統來說 升級本省就是一項非常復雜的工程 如果要在線升級 就會更加復雜 其中 實現類的熱替換只是最後一步操作 在線升級的要求會對系統的整體設計帶來深遠的影響 下面我們來談談在線升級系統設計方面的一些原則
在系統設計一開始 就要考慮系統的哪些部分是需要以後在線升級的 哪些部分是穩定的
雖然我們可以把系統設計成任何一部分都是可以在線升級的 但是其成本是非常高昂的 也沒有必要 因此 明確地界定出系統以後需要在線升級的部分是明智之舉 這些部分常常是系統業務邏輯規則 演算法等等
設計出規范一致的系統狀態轉換方法
替換一個類僅僅是在線升級系統所要做的工作中的一個步驟 為了使系統能夠在升級後正常運行 就必須保持升級前後系統狀態的一致性 因此 在設計時要考慮需要在線升級的部分所涉及的系統狀態有哪些 把這些狀態設計成便於獲取 設置和轉換的 並用一致的方式來進行
明確出系統的升級控制協議
這個原則是關於系統在線升級的時機和流程式控制制的 不考慮系統的當前運行狀態就貿然進行升級是一項非常危險的活動 因此在系統設計中 就要考慮並預留出系統在線升級的控制點 並定義清晰 明確的升級協議來協調 控制多個升級實體的升級次序 以確保系統在升級的任何時刻都處在一個確定的狀態下
考慮到升級失敗時的回退機制
即使我們做了非常縝密細致的設計 還是難以從根本上保證系統升級一定是成功的 對於大型分布式系統來說尤其如此 因此在系統設計時 要考慮升級失敗後的回退機制
好了 本小節我們簡單介紹了在線升級系統設計時的幾個重要的原則 下一小節我們將給出一個簡單的實例 來演示一下如何來實現一個在線升級系統
在線升級系統實例
首先 我們來簡單介紹一下這個實例的結構組成和要完成的工作 在我們的例子中 主要有三個實體 一個是升級控制實體 兩個是工作實體 都基於 ActiveObject 實現
升級控制實體以 RMI 的方式對外提供了一個管理命令介面 用以接收外部的在線升級命令 工作實體有兩個消息隊列 一個用以接收分配給它的任務(我們用定時器定時給它發送任務命令消息) 我們稱其為任務隊列 另一個用於和升級控制實體交互 協作完成升級過程 我們稱其為控制隊列 工作實體中的任務很簡單 就是使用我們前面介紹的 Foo 類簡單地列印出一個字元串 不過這次字元串作為狀態保存在工作實體中 動態設置給 Foo 類的實例的 升級的協議流程如下
當升級控制實體接收到來自 RMI 的在線升級命令時 它會向兩個工作實體的任務隊列中發送一條准備升級消息 然後等待回應 當工作實體在任務隊列中收到准備升級消息時 會立即給升級控制實體發送一條准備就緒消息 然後切換到控制隊列等待進一步的升級指令 升級控制實體收齊這兩個工作實體發來的准備就緒消息後 就給這兩個工作實體的控制隊列各發送一條開始升級消息 然後等待結果 工作實體收到開始升級消息後 進行實際的升級工作 也就是我們前面講述的熱替換類 然後 給升級控制實體發送升級完畢消息 升級控制實體收到來自兩個工作實體的升級完畢消息後 會給這兩個工作實體的控制隊列各發送一條繼續工作消息 工作實體收到繼續工作消息後 切換到任務隊列繼續工作 升級過程結束
主要的代碼片段如下(略去命令消息的定義和執行細節)
清單 主要的代碼片段
// 升級控制實體關鍵代碼
class UpgradeController extends ActiveObject{
int nready = ;
int nfinished = ;
Worker[] workers;
// 收到外部升級命令消息時 會觸發該方法被調用
public void askForUpgrade() {
for(int i= ; i<workers length; i++)
workers[i] getTaskQueue() enqueue(new PrepareUpgradeCmd(workers[i]));
}
// 收到工作實體回應的准備就緒命令消息時 會觸發該方法被調用
public void readyForUpgrade(String worker_name) {
nready++;
if(nready == workers length){
for(int i= ; i<workers length; i++)
workers[i] getControlQueue() enqueue(new
StartUpgradeCmd(workers[i]));
}
}
// 收到工作實體回應的升級完畢命令消息時 會觸發該方法被調用
public void finishUpgrade(String worker_name) {
nfinished++;
if(nfinished == workers length){
for(int i= ; i<workers length; i++)
workers[i] getControlQueue() enqueue(new
ContineWorkCmd(workers[i]));
}
}
}
// 工作實體關鍵代碼
class Worker extends ActiveObject{
UpgradeController ugc;
HotswapCL hscl;
IFoo foo;
String state = hello world! ;
// 收到升級控制實體的准備升級命令消息時 會觸發該方法被調用
public void prepareUpgrade() {
switchToControlQueue();
ugc getMsgQueue() enqueue(new ReadyForUpdateCMD(ugc this));
}
// 收到升級控制實體的開始升級命令消息時 會觸發該方法被調用
public void startUpgrade(String worker_name) {
doUpgrade();
ugc getMsgQueue() enqueue(new FinishUpgradeCMD(ugc this));
}
// 收到升級控制實體的繼續工作命令消息時 會觸發該方法被調用
public void continueWork(String worker_name) {
switchToTaskQueue();
}
// 收到定時命令消息時 會觸發該方法被調用
public void doWork() {
foo sayHello();
}
// 實際升級動作
private void doUpgrade() {
hscl = new HowswapCL( /swap new String[]{ Foo });
Class cls = hscl loadClass( Foo );
foo = (IFoo)cls newInstance();
foo SetState(state);
}
}
//IFoo 介面定義
interface IFoo {
void SetState(String);
void sayHello();
}
在Foo 類第一個版本的實現中 只是把設置進來的字元串直接列印出來 在第二個版本中 會先把設置進來的字元串變為大寫 然後列印出來 例子很簡單 旨在表達規則或者演算法方面的升級變化 另外 我們並沒有提及諸如 消息超時 升級失敗等方面的異常情況 這在實際產品開發中是必須要考慮的
lishixin/Article/program/Java/hx/201311/26326
❹ java解釋器如何載入類
類載入次序:1、靜態代碼塊或者靜態方法->2、main方法調用到的方法
對象載入次序:1、靜態代碼塊或者靜態方法->2、非靜態代碼塊或者非靜態方法->3、對象的構造方法。
但是有一段代碼沒有辦法解釋。代碼忘了,過段時間丟上來
個人感覺應該好像不大對勁,我覺得應該是:
類裝載時,1、靜態代碼塊或者靜態方法被調用
然後是程序的運行,main調用到的方法會被執行,如果是新建一個對象,則
2、非靜態代碼塊或者非靜態方法->3、對象的構造方法順序執行。
===============================================
首先我們要分析類載入原理,java中默認有三種類載入器:引導類載入器,擴展類載入器,系統類載入器(也叫應用類載入器)引導類載入器負責載入jdk中的系統類,這種類載入器都是用c語言實現的,在java程序中沒有辦法獲得這個類載入器,對於java程序是一個概念而已,基本上不用考慮它的存在,像String,Integer這樣的類都是由引導類載入器載入器的.
擴展類載入器負責載入標准擴展類,一般使用java實現,這是一個真正的java類載入器,負責載入jre/lib/ext中的類,和普通的類載入器一樣,其實這個類載入器對我們來說也不是很重要,我們可以通過java程序獲得這個類載入器。
系統類載入器,載入第一個應用類的載入器(其實這個定義並不準確,下面你將會看到),也就是執行java MainClass 時載入MainClass的載入器,這個載入器使用java實現,使用的很廣泛,負責載入classpath中指定的類。
類載入器之間有一定的關系(父子關系),我們可以認為擴展類載入器的父載入器是引導類載入器(當然不這樣認為也是可以的,因為引導類載入器表現在java中就是一個null),不過系統類載入器的父載入器一定是擴展類載入器,類載入器在載入類的時候會先給父載入器一個機會,只有父載入器無法載入時才會自己去載入。
我們無法獲得引導類載入器,因為它是使用c實現的,而且使用引導類載入器載入的類通過getClassLoader方法返回的是null.所以無法直接操作引導類載入器,但是我們可以根據Class.getClassLoader方法是否為null判斷這個類是不是引導類載入器載入的,可以通過下面的方法獲得引導類載入器載入的類路徑(每個jar包或者文件夾對應了一個URL);
sun.misc.Launcher.getBootstrapClassPath().getURLs()
你可以直接在你的main函數中輸出就可以了
System.out.println(java.util.Arrays.asList(sun.misc.Launcher.getBootstrapClassPath().getURLs()).toString());
得到的結果是:
[file:/C:/Program%20Files/Java/j2re1.4.2_10/lib/rt.jar,
file:/C:/Program%20Files/Java/j2re1.4.2_10/lib/i18n.jar,
file:/C:/Program%20Files/Java/j2re1.4.2_10/lib/sunrsasign.jar,
file:/C:/Program%20Files/Java/j2re1.4.2_10/lib/jsse.jar,
file:/C:/Program%20Files/Java/j2re1.4.2_10/lib/jce.jar,
file:/C:/Program%20Files/Java/j2re1.4.2_10/lib/charsets.jar,
file:/C:/Program%20Files/Java/j2re1.4.2_10/classes]
其實我們是可以指定引導類載入器的類路徑的,java提供了一個-Xbootclasspath參數,不過這個參數不是標准參數。
java -Xbootclasspath: 運行時指定引導類載入器的載入路徑(jar文件或者目錄)
java -Xbootclasspath/p:和上面的相同,不過把這個路徑放到原來的路徑前面
java -Xbootclasspath/a:這個就是在原引導類路徑後面添加類路徑。
上面我們有提過載入第一個應用類未必就是系統載入器。
如果我把這個應用類的路徑放到引導類路徑中,它將會被引導類載入器載入,大致這樣
java -Xbootclasspath/a:myjar.jar MainClass
如果MainClass在myjar.jar中,那麼這個類將會被引導類載入器載入。
如果希望看詳情,使用-verbose參數,為了看的更清楚,使用重定向,大致為(windows下):
java -verbose -Xbootclasspath/a:myjar.jar MainClass -> C:\out.txt
通過這個參數我們可以實現自己的系統類,比如替換掉java.lang.Object的實現,自己可以擴展
一些方法,不過這樣做似乎沒有好處,因為那就不是標准了。
我們最關心的還是系統類載入器,一般都認為系統類載入器是載入應用程序第一個類的載入器,
也就是java MainClass命令中載入MainClass的類載入器,這種說法雖然不是很嚴謹,但基本上還是可以這樣認為的,因為我們很少會改變引導類載入器和擴展類載入器的默認行為。應該說系統類載入器負責載入classpath路徑中的而且沒有被擴展類載入器載入的類(當然也包括引導類載入器載入的)。如果classpath中有這個類,但是這個類也在擴展類載入器的類路徑,那麼系統類載入器將沒有機會載入它。
我們很少改變擴展類載入器的行為,所以一般你自己定義的類都是系統類載入器載入器的。
獲得系統類載入器非常簡單,假設MyClass是你定義的一個類
MyClass.class.getClassLoader()返回的就是系統類載入器,當然這種方法無法保證絕對正確,我們可以使用更簡單而且一定正確的方式:
ClassLoader.getSystemClassLoader()獲得系統類載入器。我們知道ClassLoader是一個抽象類,所以系統類載入器肯定是ClassLoader的一個子類實現。我們來看看它是什麼
ClassLoader.getSystemClassLoader().getClass();
結果是class sun.misc.Lancher$AppClassLoader
可以看出這是sun的一個實現,從名字可以看出是一個內部類,目前我也沒有看到這個源代碼,似乎還不是很清晰:
我們在看看它的父類是什麼:
ClassLoader.getSystemClassLoader().getClass().getSuperclass();
結果是:class java.net.URLClassLoader
這個是j2se的標准類,它的父類是SecureClassLoader,而SecureClassLoader是繼承ClassLoader的。
現在整個關系應該很清楚,我們會看到幾乎所有的ClassLoader實現都是繼承URLClassLoader的。
因為系統類載入器是非常重要的,而且是我們可以直接控制的,所以我們後面還會介紹,不過先來看一下擴展類
載入器以及它們之間的關系。
擴展類載入器似乎是一個不起眼的角色,它負責載入java的標准擴展(jre/lib/ext目錄下的所有jar),它其實就是一個普通的載入器,看得見摸得著的。
首先的問題是怎麼知道擴展類載入器在哪裡?
的確沒有直接途徑獲得擴展類載入器,但是我們知道它是系統類載入器的父載入器,我們已經很容易的獲得系統類載入器了,所以我們可以間接的獲得擴展類載入器:
ClassLoader.getSystemClassLoader().getParent().getClass();
其實是通過系統類載入器間接的獲得了擴展類載入器,看看是什麼東西:
結果是:class sun.misc.Launcher$ExtClassLoader
這個類和系統類載入器一樣是一個內部類,而且定義在同一個類中。
同樣看看它的父類是什麼:
ClassLoader.getSystemClassLoader().getParent().getClass().getSuperclass();
可以看出結果也是class java.net.URLClassLoader
擴展類載入jre/lib/ext目錄下的所有類,包括jar,目錄下的所有類(目錄名不一定要classes).
現在可以回答上面的問題了,你寫一個HelloWorld,放到jre/lib/ext/下的某個目錄
比如 jre/lib/ext/myclass/HelloWorld.class
然後在你classpath也設置一份到這個類的路徑,結果執行java HelloWorld時,這個類是被擴展類載入器載入器的,可以這樣證明
public static void main(String[] args){
System.out.println("loaded by"+HelloWorld.class.getClassLoader().getClass());
System.out.println("Hello World");
}
結果可以得到class sun.misc.Launcher$ExtClassLoader
當然如果你把jre/lib/ext下myclass這個目錄刪除,仍然可以運行,但是這樣結果是
class sun.misc.Lancher$AppClassLoader
如果你不知道這個過程的話,假設在你擴展類路徑下有一份classpath中的拷貝,或者是比較低的版本,當你使用新的版本時會發現沒有起作用,知道這個過程你就不會覺得奇怪了。另外就是兩個不同的類載入器是可以載入一個同名的類的,也就是說雖然擴展類載入器載入了某個類,系統類載入器是可以載入自己的版本的,
但是現有的實現都沒有這樣做,ClassLoader中的方法是會請求父類載入器先載入的,如果你自己定義類載入器完全可以修改這種默認行為,甚至可以讓他沒有父載入器。
這里給出一個方法如何獲得擴展類載入器載入的路徑:
String path=System.getProperty("java.ext.dirs");
File dir=new File(path);
if(!dir.exists()||!dir.isDirectory()){
return Collections.EMPTY_LIST;
}
File[] jars=dir.listFiles();
URL[] urls=new URL[jars.length];
for(int i=0;i<jars.length;i++){
urls[i]=sun.misc.URLClassPath.pathToURLs(jars[i].getAbsolutePath())[0];
}
return Arrays.asList(urls);
對於擴展類載入器我們基本上不會去關心,也很少把你自己的jar放到擴展路徑,大部分情況下我們都感覺不到它的存在,當然如果你一定要放到這個目錄下,一定要知道這個過程,它會優先於classpath中的類。
現在我們應該很清楚知道某個類是哪個載入器載入的,並且知道為什麼是它載入的,如果要在運行時獲得某個類的類載入器,直接使用Class的getClassLoader()方法就可以了。
用戶定義的類一般都是系統類載入器載入的,我們很少直接使用類載入器載入類,我們甚至很少自己載入類。
因為類在使用時會被自動載入,我們用到某個類時該類會被自動載入,比如new A()會導致類A自動被載入,不過這種載入只發生一次。
我們也可以使用系統類載入器手動載入類,ClassLoader提供了這個介面
ClassLoader.getSystemClassLoader().loadClass("classFullName");
這就很明確的指定了使用系統類載入器載入指定的類,但是如果該類能夠被擴展類載入器載入,系統類載入器還是不會有機會的。
我們最常用的還是使用Class.forName載入使用的類,這種方式沒有指定某個特定的ClassLoader,會使用調用類的ClassLoader。
也就是說調用這個方法的類的類載入器將會用於載入這個類。比如在類A中使用Class.forName載入類B,那麼載入類A的類載入器將會用於載入類B,這樣兩個類的類載入器是同一個。
最後討論一下如何獲得某個類載入器載入了哪些類,這個似乎有一定的使用價值,可以看出哪些類被載入了。其實這個也不是很難,因為ClassLoader中有一個classes成員變數就是用來保存類載入器載入的類列表,而且有一個方法
void addClass(Class c) { classes.addElement(c);}
這個方法被JVM調用。
我們只要利用反射獲得classes這個值就可以了,不過classes聲明為private的,我們需要修改它的訪問許可權(沒有安全管理器時很容易做到)
classes = ClassLoader.class.getDeclaredField("classes");
classes.setAccessible(true);
List ret=(List) classes.get(cl); //classes是一個Vector
可惜的是對於引導類載入器沒有辦法獲得載入的類,因為它是c實現的,在java中很難控制