App下載

Java中的類(lèi)動(dòng)態(tài)加載和熱替換分別是什么?怎么用?

我要月亮奔我而來(lái) 2021-08-12 14:44:00 瀏覽數(shù) (1803)
反饋

前言

最近,遇到了兩個(gè)和Java類(lèi)的加載和卸載相關(guān)的問(wèn)題:

1) 是一道關(guān)于Java的判斷題:一個(gè)類(lèi)被首次加載后,會(huì)長(zhǎng)期留駐JVM,直到JVM退出。這個(gè)說(shuō)法,是不是正確的?

2) 在開(kāi)發(fā)的一個(gè)集成平臺(tái)中,需要集成類(lèi)似接口的多種工具,并且工具可能會(huì)有新增,同時(shí)在不同的環(huán)境部署會(huì)有裁剪(例如對(duì)外提供服務(wù)的應(yīng)用,不能提供特定的采購(gòu)的工具),如何才能更好地實(shí)現(xiàn)?

針對(duì)上面的第2點(diǎn),我們采用Java插件化開(kāi)發(fā)實(shí)現(xiàn)。上面的兩個(gè)問(wèn)題,都和Java的類(lèi)加載和熱替換機(jī)制有關(guān)。

1. Java的類(lèi)加載器和雙親委派模型

1.1 Java類(lèi)加載器

類(lèi)加載器,顧名思義,就是用來(lái)實(shí)現(xiàn)類(lèi)的加載操作。每個(gè)類(lèi)加載器都有一個(gè)獨(dú)立的類(lèi)名稱(chēng)空間,就是說(shuō)每個(gè)由該類(lèi)加載器加載的類(lèi),都在自己的類(lèi)名稱(chēng)空間,如果要比較兩個(gè)類(lèi)是否“相等”,首先這兩個(gè)類(lèi)必須在相同的類(lèi)命名空間,即由相同的類(lèi)加載器加載(即對(duì)于任何一個(gè)類(lèi),都必須由該類(lèi)本身和加載它的類(lèi)加載器一起確定其在JVM中的唯一性),不是同一個(gè)類(lèi)加載器加載的類(lèi),不會(huì)相等。

在Java中,主要有如下的類(lèi)加載器:

下面,簡(jiǎn)單介紹上面這幾種類(lèi)加載器:

  • 啟動(dòng)類(lèi)加載器(Bootstrap Class Loader):這個(gè)類(lèi)使用C++開(kāi)發(fā)(所有的類(lèi)加載器中,唯一使用C++開(kāi)發(fā)的類(lèi)加載器),用來(lái)加載<JAVA_HOME>/lib目錄中jar和tools.jar或者使用 -Xbootclasspath 參數(shù)指定的類(lèi)。
  • 擴(kuò)展類(lèi)加載器(Extension Class Loader):定義為misc.Launcher$ExtClassLoader,用來(lái)加載<JAVA_HOME>/lib/ext目錄或者使用java.ext.dir指定的類(lèi)。
  • 應(yīng)用程序類(lèi)加載器(Application Class Loader):定義為misc.Launcher$AppClassLoader,用來(lái)加載用戶(hù)類(lèi)路徑下面(classpath)下面所有的類(lèi),一般情況下,該類(lèi)是應(yīng)用程序默認(rèn)的類(lèi)加載器。
  • 用戶(hù)自定義類(lèi)加載器(User Class Loader):用戶(hù)自定義類(lèi)加載器,一般沒(méi)有必要,后面我們會(huì)專(zhuān)門(mén)來(lái)一部分介紹該類(lèi)型的類(lèi)加載器。

1.2 雙親委派模型

雙親委派模型,是從 Java1.2 開(kāi)始引入的一種類(lèi)加載器模式,在Java中,類(lèi)的加載操作通過(guò)java.lang.ClassLoader中的loadClass()方法完成,咱們首先看看該方法的實(shí)現(xiàn)(直接從Java源碼中撈出來(lái)的):

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;
        }
    }

我們結(jié)合上面的注釋?zhuān)瑏?lái)解釋下雙親委派模型的內(nèi)容:

1) 接收到一個(gè)類(lèi)加載請(qǐng)求后,首先判斷該類(lèi)是否有加載,如果已經(jīng)加載,則直接返回;

2) 如果尚未加載,首先獲取父類(lèi)加載器,如果可以獲取父類(lèi)加載器,則調(diào)用父類(lèi)的loadClass()方法來(lái)加載該類(lèi),如果無(wú)法獲取父類(lèi)加載器,則調(diào)用啟動(dòng)器加載器來(lái)加載該類(lèi);

3) 判斷該類(lèi)是否被父類(lèi)加載器或者啟動(dòng)類(lèi)加載器加載,如果已經(jīng)加載完成則返回,如果未成功加載,則自己嘗試來(lái)加載該類(lèi)。

上面的描述,說(shuō)明了loadClass()方法的實(shí)現(xiàn),我們進(jìn)一步對(duì)上面的步驟進(jìn)行解釋?zhuān)?/p>

  • 因?yàn)轭?lèi)加載器首先調(diào)父類(lèi)加載器來(lái)進(jìn)行加載,從loadClass()方法的實(shí)現(xiàn),我們知道父類(lèi)加載器會(huì)嘗試調(diào)自己的父類(lèi)加載器,直到啟動(dòng)類(lèi)加載器,所以,任何一個(gè)類(lèi)的加載,都會(huì)最終委托到啟動(dòng)類(lèi)加載器來(lái)首先加載;
  • 在前面有進(jìn)行介紹,啟動(dòng)類(lèi)加載器、擴(kuò)展類(lèi)加載器、應(yīng)用程序類(lèi)加載器,都有自己加載的類(lèi)的范圍,例如啟動(dòng)類(lèi)加載器只加載JDK核心庫(kù),因此并不是父類(lèi)加載器就可以都加載成功,父類(lèi)加載器無(wú)法加載(一般如上面代碼,拋出來(lái)ClassNotFoundException),此時(shí)會(huì)由自己加載。

最后啰嗦一下,再進(jìn)行一下總結(jié):

雙親委派模型:如果一個(gè)類(lèi)加載器收到類(lèi)加載請(qǐng)求,會(huì)首先把加載請(qǐng)求委派給父類(lèi)加載器完成,每個(gè)層次的類(lèi)加載器都是這樣,最終所有的加載請(qǐng)求都傳動(dòng)到最根的啟動(dòng)類(lèi)加載器來(lái)完成,如果父類(lèi)加載器無(wú)法完成該加載請(qǐng)求(即自己加載的范圍內(nèi)找不到該類(lèi)),子類(lèi)加載器才會(huì)嘗試自己加載。

這樣的雙親委派模型有個(gè)好處:就是所有的類(lèi)都盡可能由頂層的類(lèi)加載器加載,保證了加載的類(lèi)的唯一性,如果每個(gè)類(lèi)都隨機(jī)由不同的類(lèi)加載器加載,則類(lèi)的實(shí)現(xiàn)關(guān)系無(wú)法保證,對(duì)于保證Java程序的穩(wěn)定運(yùn)行意義重大。

2. Java的類(lèi)動(dòng)態(tài)加載和卸載

2.1 Java類(lèi)的卸載

在Java中,每個(gè)類(lèi)都有相應(yīng)的Class Loader,同樣的,每個(gè)實(shí)例對(duì)象也會(huì)有相應(yīng)的類(lèi),當(dāng)滿足如下三個(gè)條件時(shí),JVM就會(huì)卸載這個(gè)類(lèi):

1) 該類(lèi)所有實(shí)例對(duì)象不可達(dá)

2) 該類(lèi)的Class對(duì)象不可達(dá)

3) 該類(lèi)的Class Loader不可達(dá)

那么,上面示例對(duì)象、Class對(duì)象和類(lèi)的Class Loader直接是什么關(guān)系呢?

在類(lèi)加載器的內(nèi)部實(shí)現(xiàn)中,用一個(gè)Java集合來(lái)存放所加載類(lèi)的引用。而一個(gè)Class對(duì)象總是會(huì)引用它的類(lèi)加載器,調(diào)用Class對(duì)象的getClassLoader()方法,就能獲得它的類(lèi)加載器。所以,Class實(shí)例和加載它的加載器之間為雙向引用關(guān)系。

一個(gè)類(lèi)的實(shí)例總是引用代表這個(gè)類(lèi)的Class對(duì)象。在Object類(lèi)中定義了getClass()方法,這個(gè)方法返回代表對(duì)象所屬類(lèi)的Class對(duì)象的引用。此外,所有的Java類(lèi)都有一個(gè)靜態(tài)屬性class,它引用代表這個(gè)類(lèi)的Class對(duì)象。

Java虛擬機(jī)自帶的類(lèi)加載器(前面介紹的三種類(lèi)加載器)在JVM運(yùn)行過(guò)程中,會(huì)始終存在,而這些類(lèi)加載器則會(huì)始終引用它們所加載的類(lèi)的Class對(duì)象,因此這些Class對(duì)象始終是可觸及的。因此,由Java虛擬機(jī)自帶的類(lèi)加載器所加載的類(lèi),在虛擬機(jī)的生命周期中,始終不會(huì)被卸載。

那么,我們是不是就完全不能在Java程序運(yùn)行過(guò)程中,動(dòng)態(tài)修改我們使用的類(lèi)了嗎?答案是否定的!根據(jù)上面的分析,通過(guò)Java虛擬機(jī)自帶的類(lèi)加載器加載的類(lèi)無(wú)法卸載,我們可以自定義類(lèi)加載器來(lái)加載Java程序,通過(guò)自定義類(lèi)加載器加載的Java類(lèi),是可以被卸載的。

2.2 自定義類(lèi)加載器

前面介紹到,類(lèi)加載的雙親委派模型,是推薦模型,在loadClass中實(shí)現(xiàn)的,并不是必須使用的模型。我們可以通過(guò)自定義類(lèi)加載器,直接加載我們需要的Java類(lèi),而不委托給父類(lèi)加載器。

如上圖所示,我們有自定義的類(lèi)加載器MyClassLoader,用來(lái)加載類(lèi)MyClass,則在JVM中,會(huì)存在上面三類(lèi)引用(上圖忽略這三種類(lèi)型對(duì)象對(duì)其他的對(duì)象的引用)。如果我們將左邊的三個(gè)引用變量,均設(shè)置為null,那么此時(shí),已經(jīng)加載的MyClass將會(huì)被卸載。

2.3 動(dòng)態(tài)卸載存在的問(wèn)題

動(dòng)態(tài)卸載需要借助于JVM的垃圾收集功能才可以做到,但是我們知道,JVM的垃圾回收,只有在堆內(nèi)存占用比較高的時(shí)候,才會(huì)觸發(fā)。即使我們調(diào)用了System.gc(),也不會(huì)立即執(zhí)行垃圾回收操作,而只是告訴JVM需要執(zhí)行垃圾回收,至于什么時(shí)候垃圾回收,則要看JVM自己的垃圾回收策略。

但是我們不需要悲觀,即使動(dòng)態(tài)卸載不是那么牢靠,但是實(shí)現(xiàn)動(dòng)態(tài)的Java類(lèi)的熱替換還是有希望的。

3. Java類(lèi)的熱替換

下面通過(guò)代碼來(lái)介紹Java類(lèi)的熱替換方法(代碼簡(jiǎn)陋,主要為了說(shuō)明問(wèn)題):

如下面的代碼:

首先定義一個(gè)自定義類(lèi)加載器:

package zmj;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class FileClassLoader extends ClassLoader {
    private String fileName;

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public Class loadClass(String name) throws ClassNotFoundException {
        if (name.startsWith("java")) {
            return getSystemClassLoader().loadClass(name);
        }
        Class cls = null;
        File classF = new File(fileName);
        try {
            cls = instantiateClass(name, new FileInputStream(classF), classF.length());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return cls;
    }

    private Class instantiateClass(String name, InputStream fin, long len) throws IOException {
        byte[] raw = new byte[(int) len];
        fin.read(raw);
        fin.close();
        return defineClass(name, raw, 0, raw.length);
    }
}

上面在loadClass時(shí),先判斷類(lèi)name(包含package的全限定名)是否以java開(kāi)始,如果是java開(kāi)始,則使用JVM自帶的類(lèi)加載器加載。

然后定義一個(gè)簡(jiǎn)單的動(dòng)態(tài)加載類(lèi):

package zmj;

public class SayHello {
    public void say() {
        System.out.println("hello ping...");
    }
}

在執(zhí)行過(guò)程中,會(huì)動(dòng)態(tài)修改打印內(nèi)容,測(cè)試類(lèi)的熱加載。

然后定義一個(gè)調(diào)用類(lèi):

package zmj;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws InterruptedException, ClassNotFoundException,
            IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        while (true) {
            FileClassLoader fileClassLoader = new FileClassLoader();
            fileClassLoader.setFileName("D:/workspace/idea/test/class-loader-test/target/classes/zmj/SayHello.class");
            Object obj = null;
            obj = fileClassLoader.loadClass("zmj.SayHello").newInstance();
            Method m = obj.getClass().getMethod("say", new Class[]{});
            m.invoke(obj, new Object[]{});
            Thread.sleep(2000);
        }
    }
}

當(dāng)我們運(yùn)行上面Main程序過(guò)程中,我們動(dòng)態(tài)修改執(zhí)行內(nèi)容(SayHello中,從 hello zmj... 更改為 hello ping...),最終展示的內(nèi)容如下:

hello zmj...

hello zmj...

hello zmj...

hello ping...

hello ping...

hello ping...

以上就是Java中類(lèi)動(dòng)態(tài)加載和熱替換的詳細(xì)內(nèi)容,想要了解更多關(guān)于Java類(lèi)動(dòng)態(tài)加載和熱替換的資料,請(qǐng)多多關(guān)注W3Cschool其它相關(guān)文章!


2 人點(diǎn)贊