深入理解Java類加載器

java類加載器

Java類加載器(英語:Java Classloader)是Java運行時環(huán)境(Java Runtime Environment)的一部分坐漏,負(fù)責(zé)動態(tài)加載Java類Java虛擬機(jī)的內(nèi)存空間中。類通常是按需加載恶复,即第一次使用該類時才加載亿絮。由于有了類加載器,Java運行時系統(tǒng)不需要知道文件與文件系統(tǒng)颁股。

JVM中的默認(rèn)類加載器

JVM中有3個默認(rèn)的類加載器:

  1. 引導(dǎo)(Bootstrap)類加載器盹廷。由原生代碼(C++語言)編寫征绸,不繼承自java.lang.ClassLoader。負(fù)責(zé)加載JVM自身需要的類俄占,負(fù)責(zé)將<JAVA_HOME>/jre/lib路徑下的核心類庫或-Xbootclasspath參數(shù)指定的路徑下的jar包加載到內(nèi)存中管怠。
  2. 擴(kuò)展(Extensions)類加載器。用來在<JAVA_HOME>/jre/lib/ext,或java.ext.dirs中指明的目錄中加載 Java的擴(kuò)展庫缸榄。Java 虛擬機(jī)的實現(xiàn)會提供一個擴(kuò)展庫目錄渤弛。該類加載器在此目錄里面查找并加載 Java 類。該類由sun.misc.Launcher$ExtClassLoader實現(xiàn)甚带。
//ExtClassLoader類中獲取路徑的代碼
    private static File[] getExtDirs() {
        //加載<JAVA_HOME>/lib/ext目錄中的類庫
        String var0 = System.getProperty("java.ext.dirs");
        File[] var1;
        if (var0 != null) {
            StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
            int var3 = var2.countTokens();
            var1 = new File[var3];

            for (int var4 = 0; var4 < var3; ++var4) {
                var1[var4] = new File(var2.nextToken());
            }
        } else {
            var1 = new File[0];
        }

        return var1;
    }
  1. Apps類加載器(也稱系統(tǒng)類加載器)暮芭。根據(jù) Java應(yīng)用程序的類路徑(java.class.path或CLASSPATH環(huán)境變量)來加載 Java 類。一般來說欲低,Java 應(yīng)用的類都是由它來完成加載的⌒笪可以通過 ClassLoader.getSystemClassLoader()來獲取它砾莱。該類由sun.misc.Launcher$AppClassLoader實現(xiàn),是程序中默認(rèn)的類加載器凄鼻。

在Java的日常應(yīng)用程序開發(fā)中腊瑟,類的加載幾乎都是由上述3種類加載器相互配合執(zhí)行的,在必要時块蚌,我們還可以自定義類加載器闰非,需要注意的是,Java虛擬機(jī)對class文件采用的是按需加載的方式峭范,也就是說當(dāng)需要使用該類時才會將它的class文件加載到內(nèi)存生成class對象财松,而且加載某個類的class文件時,Java虛擬機(jī)采用的是雙親委派模式,即把請求交由父類處理辆毡,它是一種任務(wù)委派模式菜秦。

雙親委派模式

雙親委派模式的工作原理

雙親委派模式要求除了頂層的引導(dǎo)類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器舶掖,類加載器之間的關(guān)系如下:


雙親委派模式原理

雙親委派模式的工作原理是球昨,如果一個類加載器收到了類加載的請求,它并不會自己先去加載眨攘,而是把這個請求委托給父類的加載器去執(zhí)行主慰,如果父類加載器還存在其父類加載器,則進(jìn)一步向上委托鲫售,依次遞歸共螺,請求最終到達(dá)頂層的啟動類加載器,如果父類加載器可以完成類加載任務(wù)龟虎,就成功返回璃谨,倘若父類加載器無法完成加載任務(wù),子加載器才嘗試去自己加載鲤妥,這就是雙親委派模式佳吞。

雙親委派模式的優(yōu)勢

采用雙親委派模式的好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,通過這種層次關(guān)系可以避免類的重復(fù)加載棉安,當(dāng)父ClassLoader已經(jīng)加載了該類時底扳,就沒有必要子ClassLoader再加載一次。

其次是考慮到安全因素贡耽,Java核心api中定義的類型不會被隨意替換衷模,假設(shè)通過網(wǎng)絡(luò)傳遞一個名為java.lang.Integer的類,通過雙親委派模式傳遞到引導(dǎo)類加載器蒲赂,而引導(dǎo)類加載器在核心Java API中發(fā)現(xiàn)這個名字的類阱冶,發(fā)現(xiàn)該類已經(jīng)被加載,并不會重新加載網(wǎng)絡(luò)傳遞過來的java.lang.Integer滥嘴,而是直接返回已加載過的Integer.class木蹬,這樣可以防止核心API庫被隨意篡改。

類與類加載器

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

  • 類的包名和類名必須一致
  • 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同
    也就是說若皱,在JVM中镊叁,即使這兩個類對象來源于同一個class文件,被同一個虛擬機(jī)所加載走触,但只要加載它們的ClassLoader實例對象不同晦譬,那么這兩個類對象也是不相等的,這是因為不同的ClassLoader實例對象都擁有不同的獨立的類名稱空間互广,所以加載的class對象存在不同的類名稱空間中敛腌。

class文件的顯示加載和隱式加載

所謂class文件的顯示加載與隱式加載是指JVM加載class文件到內(nèi)存的方式。
顯示加載指在代碼中通過調(diào)用ClassLoader加載class對象,如直接使用Class.forName(name)this.getClass().getClassLoader().loadClass()加載class對象
隱式加載則是不直接在代碼中調(diào)用ClassLoader的方法加載class對象迎瞧,而是通過虛擬機(jī)自動加載到內(nèi)存中夸溶,如在加載某個類的class文件時,該類的class文件中引用了另外一個類的對象凶硅,此時額外引用的類將通過JVM自動加載到內(nèi)存中缝裁。

編寫自己的類加載器

自定義類加載器的用途
  • 運行時裝載或卸載類。這常用于:
  • 改變Java字節(jié)碼的裝入,例如氢妈,可用于Java類字節(jié)碼的加密裝入粹污。當(dāng)一個class文件是通過網(wǎng)絡(luò)傳輸并且可能會進(jìn)行相應(yīng)的加密操作時,需要先對class文件進(jìn)行相應(yīng)的解密后再加載到JVM內(nèi)存中首量。
  • 修改已裝入的字節(jié)碼
  • 熱部署
  • Tomcat容器壮吩,每個WebApp有自己的ClassLoader,加載每個WebApp的ClassPath路徑上的類,一旦遇到Tomcat自帶的Jar包就委托給CommonClassLoader加載加缘。
  • 隔離鸭叙,比如早些年比較火的Java模塊化框架OSGI,把每個Jar包以Bundle的形式運行,每個Bundle有自己的類加載器(不同Bundle可以有相同的類名)拣宏,Bundle與Bundle之間起到隔離的效果沈贝,同時如果一個Bundle依賴了另一個Bundle的某個類,那這個類的加載就委托給導(dǎo)出該類的BundleClassLoader進(jìn)行加載勋乾。
  • Android熱修復(fù)宋下,組件化

實現(xiàn)自定義類加載器需要繼承ClassLoader或者URLClassLoader,繼承ClassLoader則需要自己重寫findClass()方法辑莫,并編寫加載邏輯学歧,繼承URLClassLoader則可以省去編寫findClass()方法及class文件加載轉(zhuǎn)換成字節(jié)碼流的代碼。

自定義File類加載器

繼承ClassLoader

public class FileClassLoader extends ClassLoader {

    private String rootDir;

    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 獲取類的class文件字節(jié)數(shù)組
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            // 直接生成class對象
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 獲取class文件各吨,并轉(zhuǎn)換成字節(jié)碼流
     *
     * @param name
     * @return
     */
    private byte[] getClassData(String name) {
        String path = getClassPath(name);
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int read = 0;
            while ((read = is.read(buffer)) != -1) {
                baos.write(buffer, 0, read);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String getClassPath(String name) {
        return rootDir + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
    }
}

DemoObj.java

import com.oyty.classloader;

public class DemoObj {

    @Override
  public String toString() {
        return "I am demo obj";
  }
}

運行代碼枝笨,輸出I am demo obj,說明DemoObj類被成功加載绅你。需要注意的是如果DemoObj有包路徑的話,如本例中com.oyty.classloader昭躺,則編譯后的class文件也需要放在包路徑的文件夾下忌锯。本例中最后class文件的完整路徑是/Users/oyty/Documents/newworkspace/idea/classloader/com/oyty/classloader/DemoObj.class

一般情況下,自己開發(fā)的類加載只需要覆寫findClass(string name)方法即可领炫。java.lang.ClassLoader類的方法loadClass()封裝前面提到的委派模式偶垮。該方法首先會調(diào)用findLoadedClass()方法來檢查該類是否已經(jīng)被加載過;如果沒有加載過的話,會調(diào)用父類加載器的loadClass()方法來嘗試加載該類似舵;如果父類加載器無法加載該類的話脚猾,就調(diào)用findClass()方法來查找該類。因此為了保證類加載器都正確實現(xiàn)委派模式砚哗,在開發(fā)自己的類加載器時龙助,最好不要覆寫loadClass()方法,而是覆寫findClass()方法蛛芥。

loadClass()方法源碼如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

繼承URLClassLoader

public class FileUrlClassLoader extends URLClassLoader {
    public FileUrlClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public FileUrlClassLoader(URL[] urls) {
        super(urls);
    }

    public FileUrlClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }

    public static void main(String[] args) throws MalformedURLException {

        String rootDir = "/Users/oyty/Documents/newworkspace/idea/classloader";
        File file = new File(rootDir);
        URI uri = file.toURI();
        URL[] urls = {uri.toURL()};

        FileUrlClassLoader loader = new FileUrlClassLoader(urls);
        try {
            Class<?> obj = loader.loadClass("com.oyty.classloader.DemoObj");
            System.out.println(obj.newInstance().toString());
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }
}

可以知道提鸟,當(dāng)自定義類加載器繼承自URLClassLoader,將會非常簡潔仅淑,無需額外編寫findClass()方法和class文件的字節(jié)流轉(zhuǎn)換邏輯称勋。

自定義網(wǎng)絡(luò)類加載器

講一個網(wǎng)絡(luò)類加載器的實際用途:通過網(wǎng)絡(luò)類加載器實現(xiàn)組件的動態(tài)更新⊙木梗基本場景是:Java的字節(jié)碼(.class)文件存放在服務(wù)器上赡鲜,客戶端通過網(wǎng)絡(luò)的方式獲取字節(jié)代碼并執(zhí)行。當(dāng)有版本更新的時候庐船,只需要替換掉服務(wù)器上保存的文本即可银酬。

public class NetworkClassLoader extends ClassLoader {
    
    private String rootUrl;
    
    public NetworkClassLoader(String rootUrl) {
        this.rootUrl = rootUrl;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        else {
            return defineClass(name, classData, 0, classData.length);
        }
    }
    
    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            URL url = new URL(path);
            InputStream ins = url.openStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    private String classNameToPath(String className) {
        return rootUrl + "/"
                + className.replace('.', '/') + ".class";
    }
}

類NetworkClassLoader負(fù)責(zé)通過網(wǎng)絡(luò)下載Java類字節(jié)代碼并定義出Java類。在通過NetworkClassLoader加載了某個版本的類之后醉鳖,一般有兩種做法來使用它捡硅。第一種做法是使用Java反射API;另一種做法是使用接口盗棵。需要注意的是壮韭,并不能直接在客戶端代碼中引用從服務(wù)器上下載的類,因為客戶端代碼的類加載器找不到這些類纹因。使用Java反射API可以直接調(diào)用Java類的方法喷屋,而使用接口的做法則是把接口的類放在客戶端中,從服務(wù)器上加載實現(xiàn)此接口的不同版本的類瞭恰,在客戶端通過相同的接口來使用這些實現(xiàn)類屯曹。

雙親委派模型的破壞者--線程上下文類加載器

待完善......

類加載器與Web容器

對于運行在 Java EE?容器中的 Web 應(yīng)用來說,類加載器的實現(xiàn)方式與一般的 Java 應(yīng)用有所不同惊畏。不同的 Web 容器的實現(xiàn)方式也會有所不同恶耽。以 Apache Tomcat 來說,每個 Web 應(yīng)用都有一個對應(yīng)的類加載器實例颜启。該類加載器也使用代理模式偷俭,所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器缰盏。這與一般類加載器的順序是相反的涌萤。這是 Java Servlet 規(guī)范中的推薦做法淹遵,其目的是使得 Web 應(yīng)用自己的類的優(yōu)先級高于 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找范圍之內(nèi)的负溪。這也是為了保證 Java 核心庫的類型安全透揣。

絕大多數(shù)情況下,Web 應(yīng)用的開發(fā)人員不需要考慮與類加載器相關(guān)的細(xì)節(jié)川抡。下面給出幾條簡單的原則:

  • 每個 Web 應(yīng)用自己的 Java 類文件和使用的庫的 jar 包辐真,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面。
  • 多個應(yīng)用共享的 Java 類文件和 jar 包猖腕,分別放在 Web 容器指定的由所有 Web 應(yīng)用共享的目錄下面拆祈。
  • 當(dāng)出現(xiàn)找不到類的錯誤時,檢查當(dāng)前類的類加載器和當(dāng)前線程的上下文類加載器是否正確倘感。

參考:
https://zh.wikipedia.org/wiki/Java%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8
https://blog.csdn.net/javazejian/article/details/73413292
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末放坏,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子老玛,更是在濱河造成了極大的恐慌淤年,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜡豹,死亡現(xiàn)場離奇詭異麸粮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)镜廉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門弄诲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人娇唯,你說我怎么就攤上這事齐遵。” “怎么了塔插?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵梗摇,是天一觀的道長。 經(jīng)常有香客問我想许,道長伶授,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任流纹,我火速辦了婚禮糜烹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘漱凝。我一直安慰自己疮蹦,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布碉哑。 她就那樣靜靜地躺著挚币,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扣典。 梳的紋絲不亂的頭發(fā)上妆毕,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機(jī)與錄音贮尖,去河邊找鬼笛粘。 笑死,一個胖子當(dāng)著我的面吹牛湿硝,可吹牛的內(nèi)容都是我干的薪前。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼关斜,長吁一口氣:“原來是場噩夢啊……” “哼示括!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起痢畜,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤垛膝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后丁稀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吼拥,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年线衫,在試婚紗的時候發(fā)現(xiàn)自己被綠了凿可。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡授账,死狀恐怖枯跑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情矗积,我是刑警寧澤全肮,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站棘捣,受9級特大地震影響辜腺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜乍恐,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一评疗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧茵烈,春花似錦百匆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽存璃。三九已至,卻和暖如春雕拼,著一層夾襖步出監(jiān)牢的瞬間纵东,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工啥寇, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留偎球,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓辑甜,卻偏偏與公主長得像衰絮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子磷醋,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容