類載入器是沙箱的第一道防線,畢竟代碼都是由它裝入jvm中的,其中也包括有危險的代碼。它的安全作用有三點:
一 保護善意代碼不受惡意代碼的干擾
二 保護已驗證的類庫
三 代碼放入有不同的行為限制的各個保護域中
類載入體系通過使用不同的類載入器把類放入不同的名字空間中從而保護善意代碼不受惡意代碼的干擾。
JVM為每個類載入器維護一個名字空間。例如,如果jvm在某個名字空間中載入了一個稱為volcano的類,就不能再在這個名字空間中載入另一個也稱為volcano的類,除非你再創建另一個名字空間。也就是說,如果jvm有三個名字空間,你就可以載入三個叫做volcano的類,一個名字空間一個。
在jvm中,同一個名字空間中的類是可以直接互動的,但在不同名字空間中的類就不行,除非提供另外的機制。這樣,名字空間就起到了一個屏障的作用。
在圖3-1中,顯示了兩個名字空間,各有一個類載入器,各載入了兩個類型,兩個名字空間都有一個叫做volcano的類型。左邊顏色較深的類載入器載入了類型climber和volcano,右邊顏色較淺的類載入器載入了類型bakingsoda和volcano。圖中的箭頭表示名字空間中的名字對在方法區(method area)中對應類型定義數據的引用。因為名字空間的屏障作用,當climber引用volcano時,是指的同一個名字空間中的volcano。儘管都在同一個jvm,它也無法知道另一個名字空間中的volcano,。如果想知道如何達到名字空間的分隔的,可以看第八章“連結模式”
類載入器體系可以通過使用不同的類載入器載入可信包(trusted packages)和不可信包(untrusted packages)從而保護可信包的安全。儘管你可以對同一包中的類型制定訪問控制,但這種控制只有在被同一個載入器載入的前提下才起作用。
通常,一個用戶定義的類載入器需要依賴其他類載入器來完成任務,至少需要一個在jvm啟動時創建的類載入器,這個類載入器稱為啟動類載入器。在1.2版本以前,類載入器必須顯示的調用其他類載入器,如調用其他用戶定義的類載入器的loadClass方法,或者調用啟動類載入器(bootstrap class loader)的靜態函式findSystemClass()。
在1.2版本中,一個類載入器要求另一個類載入器載入某個類型的過程被規範化為代理模式(parent-delegation model 譯者:就是Chain of Responsibility模式)
在某個類載入器試圖以自己的方式載入一個類時,它首先預設把這個工作交給自己的父對象。而這個父對象又會首先把這個任務交給自己的父對象處理,這樣這個任務會一直傳到啟動類載入器,因為啟動類載入器通常是代理鏈的最後一個類。如果父類載入器能夠載入這個類型,就會返回此類型,否則由子類載入器處理。
在1.2版本以前的多數jvm的實現中,內建類載入器負責載入本地可用的類檔案,通常包括java套用的類檔案和所有這個套用需要的庫,儘管載入所需類檔案的方式根據套用不同而不同,但許多套用都以class path定義的路徑來搜尋所需類檔案。
在1.2版本中,載入本地可用類檔案的任務被分解給了多個類載入器。以前稱為原始類載入器(primordial class loader)的類載入器被改稱為啟動類載入器,用來表示它只用來載入核心java api的類檔案,因為核心java api類檔案是用來啟動jvm的。
而負責載入其他類檔案的任務都給了用戶定義的類載入器(譯者:這裡指廣義用戶,包括虛擬機的實現者),這些類檔案包括套用的類檔案,用來進行安裝和下載的標準擴展類檔案,用來在class path中查找庫的類檔案等等。
因此當1.2的JVM開始運行時,它會創建至少一個用戶定義的類載入器,所有的這些類載入器串成一個鏈,在鏈的頭部是啟動類載入器,在鏈的尾部是系統類載入器(system class loader)。在1.2之前,有時稱內建類載入器為系統類載入器,在1.2,這個名字被更正式的用於稱呼java套用所創建的新的類載入器的父親。
這個預設父代理通常來載入套用的初始類,但任何用戶定義的類載入器都可能被java平台的設計者所改變。
例如,假如你寫了一個套用,此套用需要安裝一個類載入器,用來載入從網路上下載的類檔案。這個套用運行在一個jvm上,而這個jvm有兩個用戶定義的類載入器,一個是安裝擴展類載入器,另一個是類路徑類載入器。它們和啟動類載入器串成一個鏈,依次為:啟動類載入器,安裝擴展類載入器,類路徑載入器。
如圖3-2,類路徑載入器被設計成了系統類載入器,它將是java套用新的類載入器的父親。當你的套用的網路類載入器被安裝時,它將這個系統類載入器設為它的父親。
假如在java套用運行中需要載入一個稱為volcano的類,你的類載入器會首先把這一任務交給它的父親,類路徑類載入器,去查找和載入這個類檔案。而類路徑類載入器同樣首先交給自己的父親,安裝擴展類載入器,去完成任
務。這樣,這個任務最後交給啟動類載入器去首先嘗試處理。
假設類volcano不是java api的一部分,也不是安裝擴展和類路徑的一部分,所有對應的類載入器都沒有返回這個類型,這樣就輪到你自己的類載入器了。它將會從網路上下載此類檔案,這樣這個類就稱為你套用中的一部分了。
我們繼續這個例子,假如以後某個時候第一次調用了類volcano的一個方法,這個方法中引用了java api中的類java.util.HashMap,而這個類是這個套用第一次引用,這樣jvm就要求你的類載入器去載入這個類。象以前一樣,這個請求最終到達了啟動類載入器。
但這次不同,啟動類載入器能夠載入java.util.Hashmap並把它返回給了你的類載入器。這樣安裝擴展類載入器和類路徑類載入器只起到了一個傳遞的作用,而你的類載入器也不用從網路下載這個類檔案了。從此,在類volcano中,所有對java.util.Hashmap的引用都會使用這個類。
有了這個背景知識,我們就可以看看類載入器是如何被用來保護可信庫(trusted libraries)的了。類載入器體系通過防止不可信類冒充可信類保護了可信類的邊界,防止了對java runtime安全的潛在威脅。
有了這個鏈狀的代理關係,我們知道,要載入一個類,需要鏈上的類載入器按特定順序逐次檢查,這樣自己定義的類載入器始終處於一個較低優先權的狀態,如果你自己的類載入器想要從網路上下載一個叫做java.lang.Integer的類是不可能的。它只能使用從父類載入器傳來的類型。通過這種方法,防止了用不可信代碼替換可信代碼的發生。
但假如代碼不準備去替換一個可信類型,而只想在可信包中插入一個新類型呢?假如在前面例子中,你的網路類載入器想載入一個叫做java.lang.Virus的類。象以前一樣,載入類的要求在鏈內傳遞,直到啟動類載入器,儘管啟動類載入器負責載入核心java api的類,其中也包括一個叫java.lang的包名,但找不到Virus,我們假設這個類同樣在安裝擴展類載入器和類路徑類載入器中也沒有找到。這樣你的網路類載入器就從網路上下載了這個類。
假設你成功下載了類java.lang.Virus,Java對在同一個包中的類的相互訪問有一定的特權。因此,因為你的類載入器載入了一個無恥的宣稱自己是java api的一部分的類java.lang.Virus,你肯定希望能夠享受到某種特權,從而乾一些罪惡的勾當。但類載入機制制止了這種事情的發生,因為類載入機制限制這種特權只有在同一個類載入器載入的前提下。
因為java api的java.lang包中的可信類都由啟動類載入器載入,而邪惡的java.lang.Virus由你的網路類載入器載入,他們並不屬於同一個運行包(runtime package)。
運行包這個術語首次在jvm第二版的規範中引入,指由同一個類載入器載入的同一個包中的所以類型。
在允許同一個包中的兩個類型訪問之前,jvm還有確信此兩個類型是由同一個類載入器載入的。
因此,jvm不允許java.lang.Virus去訪問java api中java.lang 包中的其他類型,因為他們不是由同一個類載入器載入的。
引入運行包的目的之一就是使用不同的類載入器載入不同類型的類檔案。啟動類載入器用來載入最值得信賴的核心java api。安裝擴展類載入器用來載入安裝擴展的任何類檔案。雖然安裝擴展也是可以信賴的,但還沒有到可以向java api添加新類型的程度。同樣,類路徑類載入器載入的類也不能訪問安裝擴展和java api中的類型。
類載入器也可以簡單的禁止載入某些類型來保護可信代碼。
例如,你可能安裝了某些包,其中有一些類你想由類路徑類載入器來載入,而不是你的網路類載入器。
假設你創建一個叫做absolutepower的包,並把它安裝在了類路徑類載入器的訪問範圍內。同時你希望由你的類載入器載入的類不能載入absolutepower包中任何類。這樣在你的類載入器中的第一件事就是檢查需要載入的類是不是把自己聲明為absolutepower中的類,如果是,則拋出一個異常,並不是交給父類載入器來處理。
類載入器機制除了禁止不同名字空間,保護可信類庫外,還把每個載入的類放到了一個保護域(protection domain)中,保護域對類的活動範圍也有一個定義。