類加載器Java源碼解析

Java 虛擬機(jī)把描述類的數(shù)據(jù)從 Class 文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗、轉(zhuǎn)換解析和初始化,最 終形成可以被虛擬機(jī)直接使用的Java 類型,這個過程被稱作虛擬機(jī)的類加載機(jī)制。與那些在編譯時需 要進(jìn)行連接的語言不同,在Java 語言里面,類型的加載、連接和初始化過程都是在程序運(yùn)行期間完成 的,這種策略讓Java 語言進(jìn)行提前編譯會面臨額外的困難,也會讓類加載時稍微增加一些性能開銷, 但是卻為Java 應(yīng)用提供了極高的擴(kuò)展性和靈活性, Java 天生可以動態(tài)擴(kuò)展的語言特性就是依賴運(yùn)行期動 態(tài)加載和動態(tài)連接這個特點實現(xiàn)的

內(nèi)存結(jié)構(gòu)概述

image.png

詳細(xì)圖

image.png

pc寄存器:每一個線程一份

棧:虛擬機(jī)棧,每個線程一份

堆:對象分配空間(Gc重點)

方法區(qū):存放常量,類信息

類加載器子系統(tǒng)

類加載器子系統(tǒng)作用:

類加載器子系統(tǒng)負(fù)責(zé)從文件系統(tǒng)或者網(wǎng)絡(luò)中加載Class文件,class文件在文件開頭有特定的文件標(biāo)識。

ClassLoader只負(fù)責(zé)class文件的加載,至于它是否可以運(yùn)行,則由Execution Engine決定。

加載的類信息存放于一塊稱為方法區(qū)的內(nèi)存空間。除了類的信息外,方法區(qū)中還會存放運(yùn)行時常量池信息,可能還包括字符串字面量和數(shù)字常量(這部分常量信息是Class文件中常量池部分的內(nèi)存映射)

image.png

類加載器ClassLoader角色

class file(在下圖中就是Car.class文件)存在于本地硬盤上,可以理解為設(shè)計師畫在紙上的模板,而最終這個模板在執(zhí)行的時候是要加載到JVM當(dāng)中來根據(jù)這個文件實例化出n個一模一樣的實例。
class file加載到JVM中,被稱為DNA元數(shù)據(jù)模板(在下圖中就是內(nèi)存中的Car Class),放在方法區(qū)。
在.class文件–>JVM–>最終成為元數(shù)據(jù)模板,此過程就要一個運(yùn)輸工具(類裝載器Class Loader),扮演一個快遞員的角色。

image.png

流程

完整的流程圖如下所示:

##

加載階段

加載:

  • 通過一個類的全限定名獲取定義此類的二進(jìn)制字節(jié)流
  • 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)
  • 在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口

加載class文件的方式:

  • 從本地系統(tǒng)中直接加載
  • 通過網(wǎng)絡(luò)獲取,典型場景:Web Applet
  • 從zip壓縮包中讀取,成為日后jar、war格式的基礎(chǔ)
  • 運(yùn)行時計算生成,使用最多的是:動態(tài)代理技術(shù)
  • 由其他文件生成,典型場景:JSP應(yīng)用從專有數(shù)據(jù)庫中提取.class文件,比較少見
  • 從加密文件中獲取,典型的防Class文件被反編譯的保護(hù)措施

鏈接階段

鏈接分為三個子階段:驗證 -> 準(zhǔn)備 -> 解析

驗證(Verify)

目的在于確保Class文件的字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)要求,保證被加載類的正確性,不會危害虛擬機(jī)自身安全
主要包括四種驗證,文件格式驗證,元數(shù)據(jù)驗證,字節(jié)碼驗證,符號引用驗證。

準(zhǔn)備(Prepare)

為類變量(static變量)分配內(nèi)存并且設(shè)置該類變量的默認(rèn)初始值,即零值
這里不包含用final修飾的static,因為final在編譯的時候就會分配好了默認(rèn)值,準(zhǔn)備階段會顯式初始化
注意:這里不會為實例變量分配初始化,類變量會分配在方法區(qū)中,而實例變量是會隨著對象一起分配到Java堆中

初始化階段

類的初始化時機(jī)

  • 創(chuàng)建類的實例
  • 訪問某個類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值
  • 調(diào)用類的靜態(tài)方法
  • 反射(比如:Class.forName(“com.atguigu.Test”))
  • 初始化一個類的子類
  • Java虛擬機(jī)啟動時被標(biāo)明為啟動類的類
  • JDK7開始提供的動態(tài)語言支持:java.lang.invoke.MethodHandle實例的解析結(jié)果REF_getStatic、REF putStatic、REF_invokeStatic句柄對應(yīng)的類沒有初始化,則初始化

除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導(dǎo)致類的初始化,即不會執(zhí)行初始化階段(不會調(diào)用 clinit() 方法和 init() 方法)

clinit()

  • 初始化階段就是執(zhí)行類構(gòu)造器方法<clinit>()的過程

  • 此方法不需定義,是javac編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)代碼塊中的語句合并而來。也就是說,當(dāng)我們代碼中包含static變量的時候,就會有clinit方法

  • <clinit>()方法中的指令按語句在源文件中出現(xiàn)的順序執(zhí)行

  • <clinit>()不同于類的構(gòu)造器。(關(guān)聯(lián):構(gòu)造器是虛擬機(jī)視角下的<init>())

  • 若該類具有父類,JVM會保證子類的<clinit>()執(zhí)行前,父類的<clinit>()已經(jīng)執(zhí)行完畢

  • 虛擬機(jī)必須保證一個類的<clinit>()方法在多線程下被同步加鎖

類加載器的分類

類加載器負(fù)責(zé)加載所有的類,其為所有被載入內(nèi)存中的類生成一個java.lang.Class實例對象。一旦一個類被加載入JVM中,同一個類就不會被再次載入了。正如一個對象有一個唯一的標(biāo)識一樣,一個載入JVM的類也有一個唯一的標(biāo)識。

關(guān)于唯一標(biāo)識符:

在Java中,一個類用其全限定類名(包括包名和類名)作為標(biāo)識;

但在JVM中,一個類用其全限定類名和其類加載器作為其唯一標(biāo)識。

類加載器的任務(wù)是根據(jù)一個類的全限定名來讀取此類的二進(jìn)制字節(jié)流到JVM中,然后轉(zhuǎn)換為一個與目標(biāo)類對應(yīng)的java.lang.Class對象實例,在虛擬機(jī)提供了3種類加載器,啟動(Bootstrap)類加載器、擴(kuò)展(Extension)類加載器、系統(tǒng)(System)類加載器(也稱應(yīng)用類加載器),如

image.png

類加載器可以大致劃分為以下三類:

  • 啟動類加載器BootstrapClassLoader,啟動類加載器主要加載的是JVM自身需要的類,這個類加載使用C++語言實現(xiàn)的,是虛擬機(jī)自身的一部分,負(fù)責(zé)加載存放在 JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被 -Xbootclasspath參數(shù)指定的路徑中的,并且能被虛擬機(jī)識別的類庫(如rt.jar,所有的java.開頭的類均被 BootstrapClassLoader加載)。啟動類加載器是無法被Java程序直接引用的。<mark style="box-sizing: border-box; outline: 0px; background-color: rgb(248, 248, 64); color: rgb(0, 0, 0); overflow-wrap: break-word;">總結(jié)一句話:啟動類加載器加載java運(yùn)行過程中的核心類庫JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放在JRE\classes里的類,也就是JDK提供的類等常見的比如:Object、Stirng、List…</mark>

  • 擴(kuò)展類加載器: ExtensionClassLoader,該加載器由 sun.misc.Launcher$ExtClassLoader實現(xiàn),它負(fù)責(zé)加載 JDK\jre\lib\ext目錄中,或者由 java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫(如javax.開頭的類),開發(fā)者可以直接使用擴(kuò)展類加載器。

  • 應(yīng)用程序類加載器: ApplicationClassLoader,該類加載器由 sun.misc.Launcher$AppClassLoader來實現(xiàn),它負(fù)責(zé)加載用戶類路徑(ClassPath)所指定的類,開發(fā)者可以直接使用該類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。<mark style="box-sizing: border-box; outline: 0px; background-color: rgb(248, 248, 64); color: rgb(0, 0, 0); overflow-wrap: break-word;">總結(jié)一句話:應(yīng)用程序類加載器加載CLASSPATH變量指定路徑下的類 即指你自已在項目工程中編寫的類</mark>

  • 線程上下文類加載器:除了以上列舉的三種類加載器,其實還有一種比較特殊的類型就是線程上下文類加載器。類似Thread.currentThread().getContextClassLoader()獲取線程上下文類加載器,線程上下文加載器其實很重要,它違背(破壞)雙親委派模型,很好地打破了雙親委派模型的局限性,盡管我們在開發(fā)中很少用到,但是框架組件開發(fā)絕對要頻繁使用到線程上下文類加載器,如Tomcat等等…

在Java的日常應(yīng)用程序開發(fā)中,類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的,在必要時,我們還可以自定義類加載器,因為JVM自帶的類加載器(ClassLoader)只是懂得從本地文件系統(tǒng)加載標(biāo)準(zhǔn)的java class文件,因此如果編寫了自己的ClassLoader,便可以做到如下幾點:

1、在執(zhí)行非置信代碼之前,自動驗證數(shù)字簽名。

2、動態(tài)地創(chuàng)建符合用戶特定需要的定制化構(gòu)建類。

3、從特定的場所取得java class,例如數(shù)據(jù)庫中和網(wǎng)絡(luò)中。


image.png

需要注意的是,Java虛擬機(jī)對class文件采用的是按需加載的方式,也就是說當(dāng)需要使用該類時才會將它的class文件加載到內(nèi)存生成class對象,而且加載某個類的class文件時,Java虛擬機(jī)默認(rèn)采用的是雙親委派模式即把請求交由父類處理,它一種任務(wù)委派模式,下面將會詳細(xì)講到!

虛擬機(jī)自帶的加載器

啟動類加載器(引導(dǎo)類加載器,Bootstrap ClassLoader)

  • 這個類加載使用C/C++語言實現(xiàn)的,嵌套在JVM內(nèi)部
  • 它用來加載Java的核心庫(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路徑下的內(nèi)容),用于提供JVM自身需要的類
  • 并不繼承自java.lang.ClassLoader,沒有父加載器
  • 加載擴(kuò)展類和應(yīng)用程序類加載器,并作為他們的父類加載器
  • 出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類

擴(kuò)展類加載器(Extension ClassLoader)

  • Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現(xiàn)
  • 派生于ClassLoader類
  • 父類加載器為啟動類加載器
  • 從java.ext.dirs系統(tǒng)屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴(kuò)展目錄)下加載類庫。如果用戶創(chuàng)建的JAR放在此目錄下,也會自動由擴(kuò)展類加載器加載

應(yīng)用程序類加載器(也稱為系統(tǒng)類加載器,AppClassLoader)

  • Java語言編寫,由sun.misc.LaunchersAppClassLoader實現(xiàn)
  • 派生于ClassLoader類
  • 父類加載器為擴(kuò)展類加載器
  • 它負(fù)責(zé)加載環(huán)境變量classpath或系統(tǒng)屬性java.class.path指定路徑下的類庫
  • 該類加載是程序中默認(rèn)的類加載器,一般來說,Java應(yīng)用的類都是由它來完成加載
  • 通過classLoader.getSystemclassLoader()方法可以獲取到該類加載器

用戶自定義類加載器

什么時候需要自定義類加載器?

在Java的日常應(yīng)用程序開發(fā)中,類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的,在必要時,我們還可以自定義類加載器,來定制類的加載方式。那為什么還需要自定義類加載器?

  • 隔離加載類(比如說我假設(shè)現(xiàn)在Spring框架,和RocketMQ有包名路徑完全一樣的類,類名也一樣,這個時候類就沖突了。不過一般的主流框架和中間件都會自定義類加載器,實現(xiàn)不同的框架,中間價之間是隔離的)
  • 修改類加載的方式
  • 擴(kuò)展加載源(還可以考慮從數(shù)據(jù)庫中加載類,路由器等等不同的地方)
  • 防止源碼泄漏(對字節(jié)碼文件進(jìn)行解密,自己用的時候通過自定義類加載器來對其進(jìn)行解密)

如何自定義類加載器?

  • 開發(fā)人員可以通過繼承抽象類java.lang.ClassLoader類的方式,實現(xiàn)自己的類加載器,以滿足一些特殊的需求
  • 在JDK1.2之前,在自定義類加載器時,總會去繼承ClassLoader類并重寫loadClass()方法,從而實現(xiàn)自定義的類加載類,但是在JDK1.2之后已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findclass()方法中
  • 在編寫自定義類加載器時,如果沒有太過于復(fù)雜的需求,可以直接繼承URIClassLoader類,這樣就可以避免自己去編寫findclass()方法及其獲取字節(jié)碼流的方式,使自定義類加載器編寫更加簡潔。

關(guān)于ClassLoader

ClassLoader類,它是一個抽象類,其后所有的類加載器都繼承自ClassLoader(不包括啟動類加載器)

sun.misc.Launcher 它是一個java虛擬機(jī)的入口應(yīng)用

image.png

獲取ClassLoader途徑

image.png

雙親委派機(jī)制

雙親委派機(jī)制原理

Java虛擬機(jī)對class文件采用的是按需加載的方式,也就是說當(dāng)需要使用該類時才會將它的class文件加載到內(nèi)存生成class對象。而且加載某個類的class文件時,Java虛擬機(jī)采用的是雙親委派模式,即把請求交由父類處理,它是一種任務(wù)委派模式

  • 如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執(zhí)行;
  • 如果父類加載器還存在其父類加載器,則進(jìn)一步向上委托,依次遞歸,請求最終將到達(dá)頂層的啟動類加載器;
  • 如果父類加載器可以完成類加載任務(wù),就成功返回,倘若父類加載器無法完成此加載任務(wù),子加載器才會嘗試自己去加載,這就是雙親委派模式。
  • 父類加載器一層一層往下分配任務(wù),如果子類加載器能加載,則加載此類,如果將加載任務(wù)分配至系統(tǒng)類加載器也無法加載此類,則拋出異常
image.png
image.png

雙親委派機(jī)制優(yōu)勢

通過上面的例子,我們可以知道,雙親機(jī)制可以

  • 避免類的重復(fù)加載

  • 保護(hù)程序安全,防止核心API被隨意篡改

  • 自定義類:自定義java.lang.String 沒有被加載。
  • 自定義類:java.lang.ShkStart(報錯:阻止創(chuàng)建 java.lang開頭的類) 引導(dǎo)類無法加載

沙箱安全機(jī)制

  • 自定義String類時:在加載自定義String類的時候會率先使用引導(dǎo)類加載器加載,而引導(dǎo)類加載器在加載的過程中會先加載jdk自帶的文件(rt.jar包中java.lang.String.class),報錯信息說沒有main方法,就是因為加載的是rt.jar包中的String類。
  • 這樣可以保證對java核心源代碼的保護(hù),這就是沙箱安全機(jī)制。

其他

如何判斷兩個class對象是否相同?

在JVM中表示兩個class對象是否為同一個類存在兩個必要條件:

  • 類的完整類名必須一致,包括包名
  • 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同
  • 換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機(jī)所加載,但只要加載它們的ClassLoader實例對象不同,那么這兩個類對象也是不相等的

對類加載器的引用

  • JVM必須知道一個類型是由啟動加載器加載的還是由用戶類加載器加載的
  • 如果一個類型是由用戶類加載器加載的,那么JVM會將這個類加載器的一個引用作為類型信息的一部分保存在方法區(qū)中
  • 當(dāng)解析一個類型到另一個類型的引用的時候,JVM需要保證這兩個類型的類加載器是相同的

例子

下面我們看一個程序:

package com.jvm.classloaderQi;

public class ClassloaderTest {
    public static void main(String[] args) {
        //獲取ClassloaderTest類的加載器
        ClassLoader classLoader= ClassloaderTest.class.getClassLoader(); 

        System.out.println(classLoader);
        System.out.println(classLoader.getParent()); //獲取ClassloaderTest類的父類加載器
        System.out.println(classLoader.getParent().getParent());
    }
}

運(yùn)行結(jié)果:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null

從上面的結(jié)果可以看出,并沒有獲取到ExtClassLoader的父加載器(Loader),原因是Bootstrap Loader(啟動類加載器)是用C++語言實現(xiàn)的(這里僅限于Hotspot,也就是JDK1.5之后默認(rèn)的虛擬機(jī),有很多其他的虛擬機(jī)是用Java語言實現(xiàn)的),找不到一個確定的返回父Loader的方式,于是就返回null。至于$符號就是內(nèi)部類的含義。

源碼解析

AppClassLoader的父類加載器是ExtClassLoader,ExtClassLoader的父類加載器為空,但邏輯上是啟動類加載器;ExtClassLoader類和AppClassLoader類兩個都是Java編寫的,位于rt.jar中,也就是由啟動類加載器負(fù)責(zé)加載這兩個類;這兩個類的源碼在Java的官方源碼包src.zip中不存在,只能反編譯或者參考OpenJDK8 jdk/share/classes/sun/misc/Launcher.java源碼,如下圖:

image.png
image.png

這兩個都繼承自URLClassLoader,彼此沒有繼承關(guān)系,默認(rèn)的包級訪問,所以無法在sun.misc包外訪問,JDK8下 URLClassLoader的類繼承關(guān)系如下圖,下面詳細(xì)介紹各類加載器。

image.png

ClassLoader

ClassLoader是一個抽象類,主要定義了加載類和資源的方法,并在方法實現(xiàn)中定義了委托模型,如下圖:


image.png

以loadClass和getResource方法的實現(xiàn)為例說明,如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}
 
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        //返回加載指定類的鎖,避免并發(fā)加載時的重復(fù)加載和死鎖
        synchronized (getClassLoadingLock(name)) {
            //首先檢查該類是否已加載,最終由本地方法findLoadedClass0實現(xiàn)
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //如果父類加載器不為空,委托父類加載器加載該類
                        c = parent.loadClass(name, false);
                    } else {//父類加載器為空
                        //從啟動類加載器中查找該類,最終由本地方法findBootstrapClass實現(xiàn)
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
 
                //未能讀取該類,則調(diào)用findClass查找該類,自定義類加載可覆寫該方法
                //注意ClassLoader中未提供具體實現(xiàn),只是拋出ClassNotFoundException
                if (c == null) {
                    long t1 = System.nanoTime();
                    c = findClass(name);
 
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                //完成類的解析和初始化,最終由本地方法resolveClass0實現(xiàn)
                resolveClass(c);
            }
            return c;
        }
    }
 
    public URL getResource(String name) {
        URL url;
        if (parent != null) {
           //如果父類加載器不為空,委托給父類加載器加載該資源
            url = parent.getResource(name);
        } else {
            //如果父類加載器為空,則從啟動類路徑加載該資源,最終實現(xiàn)在sun.misc.Launcher.getBootstrapClassPath()
            url = getBootstrapResource(name);
        }
        if (url == null) {
            //如果找不到,則調(diào)用findResource繼續(xù)查找,同上面的findClass,ClassLoader中未提供具體實現(xiàn),返回null
            url = findResource(name);
        }
        return url;
    }

注意ClassLoader對外的loadClass方法只有一個參數(shù)String name,調(diào)用同名的保護(hù)方法時傳遞的resolve屬性為false,并且委托父類加載器加載時傳遞的resolve屬性也是false,這是因為從JDK1.1之后,JVM不再依賴類加載器去鏈接,而是完全由JVM根據(jù)類的實際使用情況來鏈接。

其中讀取資源相關(guān)方法是讀取類路徑下文件,jar包等的最常用的方法,Spring中解析classpath開頭的資源路徑時底層也是調(diào)用這些方法,示例如下:

@Test
    public void test4() throws Exception {
        ClassLoader classLoader=MyTest.class.getClassLoader();
        //默認(rèn)從類路徑下查找
//        InputStream in=classLoader.getResourceAsStream("test.txt");
        //getSystemResourceAsStream是利用系統(tǒng)類加載器讀取資源
//        InputStream in=ClassLoader.getSystemResourceAsStream("test.txt");
        //默認(rèn)從MyTest所在的當(dāng)前目錄讀取,加上/表示從類路徑下讀取
        InputStream in=MyTest.class.getResourceAsStream("/test.txt");
        readTxt(in);
    }
 
    @Test
    public void test5() throws Exception {
        ClassLoader classLoader=MyTest.class.getClassLoader();
        //讀取多個同名文件,支持讀取jar包中的文件
        Enumeration<URL> resources=classLoader.getResources("META-INF/MANIFEST.MF");
        while (resources.hasMoreElements()){
            URL url=resources.nextElement();
            String path=url.getPath();
            System.out.println(path);
            if(path.contains("junit-4.12.jar")){
                readTxt(url.openStream());
            }
        }
    }
 
    private void readTxt(InputStream in) throws Exception{
        BufferedReader reader=new BufferedReader(new InputStreamReader(in));
        String s=reader.readLine();
        while (s!=null){
            System.out.println(s);
            s=reader.readLine();
        }
    }

ClassLoader還有一個重要的方法getSystemClassLoader(),該方法返回的系統(tǒng)類加載器通常作為新的類加載器實例的默認(rèn)父類加載器,并負(fù)責(zé)加載應(yīng)用程序自身的jar包,默認(rèn)的系統(tǒng)啟動器是ClassLoader的子類。這個方法會在JVM啟動創(chuàng)建java.lang.Thread對象時調(diào)用,并將返回結(jié)果作為Thread的contextClassLoade。如果設(shè)置了屬性java.system.class.loader,則getSystemClassLoader()方法返回該屬性指定的全限定名的類加載器,該類加載器由默認(rèn)的系統(tǒng)類加載器加載,要求其必須提供只有一個參數(shù)ClassLoader的公開構(gòu)造方法,然后會將默認(rèn)的系統(tǒng)加載器作為參數(shù)調(diào)用該構(gòu)造方法,創(chuàng)建一個指定類名的類加載器實例,將該實例作為getSystemClassLoader()的返回結(jié)果。系統(tǒng)類加載器的初始化邏輯在initSystemClassLoader()方法中實現(xiàn),如下:

private static synchronized void initSystemClassLoader() {
        //sclSet屬性為true,表示系統(tǒng)類加載器已初始化
        if (!sclSet) {
            //scl屬性就是系統(tǒng)類加載器,不為空說明已初始化完成,此調(diào)用非法
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            //獲取Launcher
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l != null) {
                Throwable oops = null;
                 //從Launcher獲取類加載器作為默認(rèn)的系統(tǒng)類加載器
                scl = l.getClassLoader();
                try {
                    //SystemClassLoaderAction實現(xiàn)了自定義系統(tǒng)類加載器的邏輯
                    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));
                } catch (PrivilegedActionException pae) {
                    oops = pae.getCause();
                    if (oops instanceof InvocationTargetException) {
                        oops = oops.getCause();
                    }
                }
                //異常處理
                if (oops != null) {
                    if (oops instanceof Error) {
                        throw (Error) oops;
                    } else {
                        // wrap the exception
                        throw new Error(oops);
                    }
                }
            }
            sclSet = true;
        }
    }
 
    class SystemClassLoaderAction
    implements PrivilegedExceptionAction<ClassLoader> {
    private ClassLoader parent;
 
    SystemClassLoaderAction(ClassLoader parent) {
        this.parent = parent;
    }
 
    public ClassLoader run() throws Exception {
        //讀取屬性
        String cls = System.getProperty("java.system.class.loader");
        //未設(shè)置該屬性返回默認(rèn)的系統(tǒng)類加載器
        if (cls == null) {
            return parent;
        }
        //利用默認(rèn)的系統(tǒng)類加載器加載特定類
        Constructor<?> ctor = Class.forName(cls, true, parent)
            .getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
        //調(diào)用特定類的構(gòu)造方法,默認(rèn)的系統(tǒng)類加載器作為其父類加載器
        ClassLoader sys = (ClassLoader) ctor.newInstance(
            new Object[] { parent });
        //修改當(dāng)前主線程的ContextClassLoader
        Thread.currentThread().setContextClassLoader(sys);
        return sys;
    }
}

再看下sun.misc.Launcher#getClassLoader()方法的實現(xiàn),如下:

private static Launcher launcher = new Launcher();
    //getLauncher返回的是Launcher靜態(tài)私有屬性launcher
    public static Launcher getLauncher() {
        return launcher;
    }
 
    private ClassLoader loader;
    //getClassLoader返回Launcher的私有屬性loader,在構(gòu)造方法中初始化
    public ClassLoader getClassLoader() {
        return loader;
    }
 
    public Launcher() {
        //創(chuàng)建擴(kuò)展類加載器
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }
 
        //創(chuàng)建應(yīng)用類加載器
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }
 
        //將AppClassLoader作為當(dāng)前線程的ContextClassLoader
        Thread.currentThread().setContextClassLoader(loader);
 
        //根據(jù)屬性java.security.manager創(chuàng)建系統(tǒng)安全管理器
        String s = System.getProperty("java.security.manager");
        if (s != null) {
            SecurityManager sm = null;
            if ("".equals(s) || "default".equals(s)) {
                sm = new java.lang.SecurityManager();
            } else {
                try {
                    sm = (SecurityManager)loader.loadClass(s).newInstance();
                } catch (IllegalAccessException e) {
                } catch (InstantiationException e) {
                } catch (ClassNotFoundException e) {
                } catch (ClassCastException e) {
                }
            }
            if (sm != null) {
                System.setSecurityManager(sm);
            } else {
                throw new InternalError(
                    "Could not create SecurityManager: " + s);
            }
        }
    }

即默認(rèn)的系統(tǒng)類加載器就是AppClassLoader,AppClassLoader的父類加載器就是ExtClassLoader。

最后看看Class#getClassLoader()方法的實現(xiàn),如下:

//Class類實例由JVM創(chuàng)建,所以沒有對外的公開構(gòu)造方法,也無法通過反射創(chuàng)建
    //通過反射修改已有Class類實例的classLoader屬性也會報錯NoSuchFieldException
     private Class(ClassLoader loader) {
        classLoader = loader;
    }
 
    @CallerSensitive
    public ClassLoader getClassLoader() {
        //實際返回的是Class實例的classLoader屬性,該屬性在實例創(chuàng)建時初始化
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
        }
        return cl;
    }
 
    ClassLoader getClassLoader0() { return classLoader; }
    
    private final ClassLoader classLoader;

即默認(rèn)情況下自定義類調(diào)用getClassLoader返回AppClassLoader,AppClassLoader是在JVM創(chuàng)建該類的Class類實例時設(shè)置的,JVM設(shè)置的ClassLoader實際是當(dāng)前線程的ContextClassLoader,具體邏輯下回分解。

SecureClassLoader

SecureClassLoader擴(kuò)展自ClassLoader,添加了從源碼文件獲取類定義的安全控制的支持,主要是新增了兩個子類可覆寫的方法,其中CodeSource用于保存源碼文件相關(guān)的屬性,如URL,代碼簽名,簽名證書等,即CodeSource表示一個唯一的源碼文件,ProtectionDomain表示一系列的安全控制的策略,如下:

 protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
                                         CodeSource cs)
    {
        return defineClass(name, b, getProtectionDomain(cs));
    }
 
protected final Class<?> defineClass(String name,
                                         byte[] b, int off, int len,
                                         CodeSource cs)
    {
        return defineClass(name, b, off, len, getProtectionDomain(cs));
    }
 
private ProtectionDomain getProtectionDomain(CodeSource cs) {
        if (cs == null)
            return null;
 
        ProtectionDomain pd = null;
        //添加了一個ProtectionDomain的緩存,即pdcache
        synchronized (pdcache) {
            pd = pdcache.get(cs);
            if (pd == null) {
                PermissionCollection perms = getPermissions(cs);
                pd = new ProtectionDomain(cs, perms, this, null);
                pdcache.put(cs, pd);
                if (debug != null) {
                    debug.println(" getPermissions "+ pd);
                    debug.println("");
                }
            }
        }
        return pd;
    }

URLClassLoader

URLClassLoader擴(kuò)展自SecureClassLoader,這個類加載器可以用指向jar包或者文件夾的URL來加載類或者其他資源,URL路徑以“/”結(jié)束表示該URL指向一個文件夾,否則指向一個jar包。創(chuàng)建此URLClassLoader實例的線程的AccessControlContext會被用做加載類或者資源的安全控制。

URLClassLoader提供了findResource(final String name)和findClass(final String name)的具體實現(xiàn),這兩方法在ClassLoader中前者返回null,后者拋出ClassNotFoundException異常,如下:

 public URL findResource(final String name) {
        //acc是從當(dāng)前線程獲取的AccessControlContext,在URLClassLoader實例化時指定
        URL url = AccessController.doPrivileged(
            new PrivilegedAction<URL>() {
                public URL run() {
                    //ucp就是sun.misc.URLClassPath,用來搜索類文件或者其他資源
                    return ucp.findResource(name, true);
                }
            }, acc);
 
        return url != null ? ucp.checkURL(url) : null;
    }
 
    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        //將路徑中的"."替換成"/",并在末尾加上.class,即將類名轉(zhuǎn)換成對應(yīng)的class文件名
                        String path = name.replace('.', '/').concat(".class");
                        //讀取對應(yīng)的class文件
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                //將class文件轉(zhuǎn)換成Class實例,此方法是URLClassLoader的私有方法,在加載類的時候會創(chuàng)建對應(yīng)的表示包結(jié)構(gòu)的Package對象
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }
除此之外,URLClassLoader改寫了原有的getResourceAsStream(String name)和Enumeration<URL> findResources(final String name)的實現(xiàn),如下:

public InputStream getResourceAsStream(String name) {
        URL url = getResource(name);
        try {
            if (url == null) {
                return null;
            }
            //這兩步的效果等價于url.openStream()
            URLConnection urlc = url.openConnection();
            InputStream is = urlc.getInputStream();
            //主要的改動在于添加資源的自動關(guān)閉,closeables是一個保存弱引用key的WeakHashMap
            //即當(dāng)內(nèi)存不足的時候,資源會自動關(guān)閉,無需等到該資源對象本身被回收掉
            if (urlc instanceof JarURLConnection) {
                JarURLConnection juc = (JarURLConnection)urlc;
                JarFile jar = juc.getJarFile();
                synchronized (closeables) {
                    if (!closeables.containsKey(jar)) {
                        closeables.put(jar, null);
                    }
                }
            } else if (urlc instanceof sun.net.www.protocol.file.FileURLConnection) {
                synchronized (closeables) {
                    closeables.put(is, null);
                }
            }
            return is;
        } catch (IOException e) {
            return null;
        }
    }
 
public Enumeration<URL> findResources(final String name)
        throws IOException
    {
        final Enumeration<URL> e = ucp.findResources(name, true);
        //因為需要進(jìn)行安全檢查,這里對findResources的結(jié)果做了一層包裝
        return new Enumeration<URL>() {
            private URL url = null;
 
            private boolean next() {
                //如果不為空則返回true,
                if (url != null) {
                    return true;
                }
                do {
                    //獲取下一個元素
                    URL u = AccessController.doPrivileged(
                        new PrivilegedAction<URL>() {
                            public URL run() {
                                if (!e.hasMoreElements())
                                    return null;
                                return e.nextElement();
                            }
                        }, acc);
                    //如果獲取的為空則終止循環(huán)
                    if (u == null)
                        break;
                    url = ucp.checkURL(u);
                } while (url == null);//最外層的url是否為空是對checkURL的結(jié)果做判斷
                //如果不為空說明存在下一個元素,且該元素保存在url屬性中,返回true
                return url != null;
            }
 
            public URL nextElement() {
                if (!next()) {
                    throw new NoSuchElementException();
                }
                //返回url屬性,將其置空,方便下一次遍歷
                URL u = url;
                url = null;
                return u;
            }
 
            public boolean hasMoreElements() {
                return next();
            }
        };
    }

即URLClassLoader中的類和其他資源的搜索和讀取都由sun.misc.URLClassPath實現(xiàn)。

最后關(guān)注下URLClassLoader的構(gòu)造方法,URLClassLoader的父類的構(gòu)造方法的基礎(chǔ)上增加了一個必填的參數(shù)URL[],如下圖:

image.png

可選的ClassLoader用于指定父類加載器,如果沒有默認(rèn)使用系統(tǒng)類加載器;另外兩個可選的URLStreamHandlerFactory和AccessControlContext都是用于構(gòu)造URLClassPath,AccessControlContext如果沒有則使用當(dāng)前線程的AccessControlContext,因為構(gòu)造URLClassPath時URL[]和AccessControlContext是必須的參數(shù)。

、ExtClassLoader

ExtClassLoader擴(kuò)展自URLClassLoader,未改寫父類的任何方法,重點關(guān)注其初始化方法:

private static ExtClassLoader createExtClassLoader() throws IOException {
            try {
 
                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() {
                        public ExtClassLoader run() throws IOException {
                            //獲取擴(kuò)展路徑的文件列表
                            final File[] dirs = getExtDirs();
                            int len = dirs.length;
                            for (int i = 0; i < len; i++) {
                                //將加載的文件注冊到MetaIndex中
                                MetaIndex.registerDirectory(dirs[i]);
                            }
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }
    public ExtClassLoader(File[] dirs) throws IOException {
            //getExtURLs將文件列表轉(zhuǎn)換成URL列表
            super(getExtURLs(dirs), null, factory);
            //此類加載器的URLClassPath實例初始化查找緩存
            SharedSecrets.getJavaNetAccess().
                getURLClassPath(this).initLookupCache(this);
        }
 
    private static File[] getExtDirs() {
            //讀取屬性值,要求指定路徑是一個文件夾
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            if (s != null) {
                //pathSeparator表示路徑分割字符,unix下為“:”,windows下為“;”
                StringTokenizer st =
                    new StringTokenizer(s, File.pathSeparator);
                int count = st.countTokens();
                dirs = new File[count];
                //將文件路徑轉(zhuǎn)換成File對象
                for (int i = 0; i < count; i++) {
                    dirs[i] = new File(st.nextToken());
                }
            } else {
                dirs = new File[0];
            }
            return dirs;
        }
 
    private static URL[] getExtURLs(File[] dirs) throws IOException {
            Vector<URL> urls = new Vector<URL>();
            //依次遍歷所有的路徑
            for (int i = 0; i < dirs.length; i++) {
                 //返回該路徑下的文件名列表,如果為空則返回null,忽略該路徑
                String[] files = dirs[i].list();
                if (files != null) {
                    for (int j = 0; j < files.length; j++) {
                        //如果文件名不包含meta-index,則將其轉(zhuǎn)換成File對象,獲取其URL
                        if (!files[j].equals("meta-index")) {
                            File f = new File(dirs[i], files[j]);
                            urls.add(getFileURL(f));
                        }
                    }
                }
            }
            //最后將Vector中的內(nèi)容復(fù)制到URL數(shù)組中
            URL[] ua = new URL[urls.size()];
            urls.copyInto(ua);
            return ua;
        }

java.ext.dirs在默認(rèn)情況下JAVA_HOME/jre/lib/ext,測試代碼如下:

@Test
    public void test6() throws Exception {
        //結(jié)果是C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext;C:\windows\Sun\Java\lib\ext
        System.out.println(System.getProperty("java.ext.dirs"));
    }
5、AppClassLoader
      AppClassLoader擴(kuò)展自URLClassLoader,同ExtClassLoader,未改寫父類的任何方法,重點關(guān)注其初始化方法,如下:

public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            //讀取系統(tǒng)屬性
            final String s = System.getProperty("java.class.path");
            //讀取類路徑下的文件,跟getExtDirs方法不同,getClassPath不要求路徑是一個文件夾
            final File[] path = (s == null) ? new File[0] : getClassPath(s);
 
            return AccessController.doPrivileged(
                new PrivilegedAction<AppClassLoader>() {
                    public AppClassLoader run() {
                    //將類路徑下的文件列表有File[]轉(zhuǎn)換成URL數(shù)組
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }
 
 private static File[] getClassPath(String cp) {
        File[] path;
        if (cp != null) {
            int count = 0, maxCount = 1;
            int pos = 0, lastPos = 0;
            //統(tǒng)計路徑分隔符的數(shù)量
            while ((pos = cp.indexOf(File.pathSeparator, lastPos)) != -1) {
                maxCount++;
                lastPos = pos + 1;
            }
            path = new File[maxCount];
            lastPos = pos = 0;
            //不斷遍歷讀取每個路徑
            while ((pos = cp.indexOf(File.pathSeparator, lastPos)) != -1) {
                //找到一個新的非空路徑
                if (pos - lastPos > 0) {
                    path[count++] = new File(cp.substring(lastPos, pos));
                } else {
                    //空路徑,如;;中間的部分,轉(zhuǎn)換成路徑為"."的File
                    path[count++] = new File(".");
                }
                lastPos = pos + 1;
            }
            // 確保包含最后一個路徑
            if (lastPos < cp.length()) {
                //最后一個路徑非空
                path[count++] = new File(cp.substring(lastPos));
            } else {
                 //最后一個路徑為空
                path[count++] = new File(".");
            }
            //數(shù)組復(fù)制
            if (count != maxCount) {
                File[] tmp = new File[count];
                System.arraycopy(path, 0, tmp, 0, count);
                path = tmp;
            }
        } else {
            path = new File[0];
        }
       
        return path;
    }

java.class.path默認(rèn)情況下就是啟動命令中的classpath選項的值,可比對Ideal的啟動命令選項同該屬性的值。

URLClassPath

URLClassPath提供了搜索和讀取指定類的功能,以findResource(String name, boolean check)為例說明,如下:

public URLClassPath(URL[] urls,
                        URLStreamHandlerFactory factory,
                        AccessControlContext acc) {
        for (int i = 0; i < urls.length; i++) {
            //path屬性包含原始的URL
            path.add(urls[i]);
        }
        //將原始的URL放入urls屬性中,該屬性保存未打開的URL
        push(urls);
        if (factory != null) {
            jarHandler = factory.createURLStreamHandler("jar");
        }
        if (DISABLE_ACC_CHECKING)
            this.acc = null;
        else
            this.acc = acc;
}
 
 
public URL findResource(String name, boolean check) {
        Loader loader;
        //根據(jù)資源名查找包含該資源的Loader對象,int數(shù)組目標(biāo)Loader對象在Loader列表中的索引
        int[] cache = getLookupCache(name);
        //依次從包含目標(biāo)資源的loader對象中讀取資源
        for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {
            URL url = loader.findResource(name, check);
            if (url != null) {
                return url;
            }
        }
        return null;
    }
 
 
private synchronized int[] getLookupCache(String name) {
        //lookupCacheURLs包含已經(jīng)加載并緩存的URL路徑
        //lookupCacheEnabled參數(shù)表示是否啟用查找緩存,通過屬性sun.cds.enableSharedLookupCache指定,默認(rèn)為true
        if (lookupCacheURLs == null || !lookupCacheEnabled) {
            return null;
        }
        //lookupCacheLoader在initLookupCache方法中完成初始化,該方法在URLClassPath實例化完成調(diào)用,通常是URLClassLoader的父類加載器
        //getLookupCacheForClassLoader是一個本地方法,該方法返回包含指定name的資源的路徑在URL[]中的索引
        //比如URL[]為{a.jar, b.jar, c.jar, d.jar},a.jar和c.jar都包含有foo/Bar.class,則該方法返回[0,2]
        int[] cache = getLookupCacheForClassLoader(lookupCacheLoader, name);
        if (cache != null && cache.length > 0) {
            int maxindex = cache[cache.length - 1]; // cache[] is strictly ascending.
            //確保對應(yīng)路徑的Loader對象已實例化
            if (!ensureLoaderOpened(maxindex)) {
                if (DEBUG_LOOKUP_CACHE) {
                    System.out.println("Expanded loaders FAILED " +
                                       loaders.size() + " for maxindex=" + maxindex);
                }
                return null;
            }
        }
 
        return cache;
    }

Loader是URLClassPath定義的私有靜態(tài)內(nèi)部類,Loader提供在指定路徑的資源包如jar包或者文件目錄下查找指定名稱的資源的能力即findResource和getResource兩個方法,JarLoader表示jar包資源加載器,F(xiàn)ileLoader表示文件目錄的資源加載器,兩者都是Loader的子類,如下圖:

image.png

以FileLoader的實現(xiàn)為例說明,如下:

URL findResource(final String name, boolean check) {
            //查找資源,如果存在則返回URL,否則返回null
            Resource rsc = getResource(name, check);
            if (rsc != null) {
                return rsc.getURL();
            }
            return null;
        }
 
        Resource getResource(final String name, boolean check) {
            final URL url;
            try {
                //getBaseURL()是該資源的起始地址,如最外層的文件夾名
                URL normalizedBase = new URL(getBaseURL(), ".");
                //構(gòu)造目標(biāo)資源的URL
                url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));
                //獲取目標(biāo)資源的文件名,校驗是否在基地址下,如果不是返回null
                if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
                    // requested resource had ../..'s in path
                    return null;
                }
 
                if (check)
                    URLClassPath.check(url);
 
                final File file;
                //資源名中包含..,這時獲取的資源可能在基地址外
                if (name.indexOf("..") != -1) {
                    //處理路徑中包含的..將其轉(zhuǎn)換成平臺相關(guān)的絕對路徑
                    file = (new File(dir, name.replace('/', File.separatorChar)))
                          .getCanonicalFile();
                     //判斷目標(biāo)資源是否在基地址下,dir即基地址
                    if ( !((file.getPath()).startsWith(dir.getPath())) ) {
                        /* outside of base dir */
                        return null;
                    }
                } else {
                    file = new File(dir, name.replace('/', File.separatorChar));
                }
 
                //如果文件存在則返回對應(yīng)的Resource
                if (file.exists()) {
                    return new Resource() {
                        public String getName() { return name; };
                        public URL getURL() { return url; };
                        public URL getCodeSourceURL() { return getBaseURL(); };
                        public InputStream getInputStream() throws IOException
                            { return new FileInputStream(file); };
                        public int getContentLength() throws IOException
                            { return (int)file.length(); };
                    };
                }
            } catch (Exception e) {
                return null;
            }
            return null;
        }

類加載委托機(jī)制

Java提供的應(yīng)用類加載器AppClassLoader只能加載位于classpath下的類,因此Java應(yīng)用的啟動命令通常會將應(yīng)用自身的jar包,依賴的第三方j(luò)ar包都追加到classpath后面,以ideal測試程序的啟動命令為例,如下圖:


image.png

當(dāng)前測試程序編譯結(jié)果的目錄target/test-classes,應(yīng)用依賴的第三方j(luò)ar包如junit-4.12.jar都加入到classpath中了,除此之外屬于ExtClassLoader加載的位于ext目錄下的jar包,屬于啟動類加載器加載的jar包如rt.jar也都包含在里面了。那么AppClassLoader會重復(fù)加載本屬于ExtClassLoader和啟動類加載器加載的類么?答案是不會,類加載的委托機(jī)制保證了子類加載器不會重復(fù)加載父類加載器已經(jīng)加載過的類,因為Java核心類都是由啟動類加載器加載的,所以類加載的委托機(jī)制還保證了子類加載器無法加載一個非法的核心類來替換官方的核心類,源碼參考ClassLoader的loadClass方法定義,其時序圖如下:

image.png

在某些特殊場景下如Servlet容器需要同時加載多個應(yīng)用,而多個應(yīng)用之間可能存在同名的類,為了保證多個應(yīng)用間的類的隔離,允許同一個類被加載多次,需要打破上述委托模型,即加載某個類時不再優(yōu)先委托父類類加載器加載,而是現(xiàn)用自定義的類加載加載,如果無法加載了再用父類加載器加載,參考下篇文章的分析。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容