JVM 系列 ClassLoader

JVM 系列()ClassLoader

在前面一節中,主要介紹了 Class 的裝載過程,Class 的裝載大致上能夠分爲加載類、鏈接類和初始化 3 個階段。本小節將主要介紹紹 Java 語言中的 ClassLoader,類裝載器。它主要工做在 Class 裝載的加載階段從系統外部得到 Class 二進制數據流。java

1、ClassLoader

ClassLoader 是 Java 的核心組件,全部的 Class 都是由 ClassLoader 進行加載的, ClassLoader 負責經過各類方式將 Class 信息的二進制數據流讀入系統,然而後交給 Java 虛擬機進行鏈接、初始化等操做。所以, Classloader在整個裝載階段,只能影響到類的加載,而沒法經過 ClassLoader 去改變類的鏈接和初始化行爲。git

從代碼層面看, ClassLoader 是一個抽象類,它提供了一些重要的接口,用於自定義 Class 的加載流程和加載方式。 Classloader 的主要方法以下github

private final ClassLoader parent;

/**
 * 給定一個類名,加載一個類,返回表明這個類的 Class 實例,若是找不到類,則返回 ClassNotFoundException 異常
 */
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
}

/**
 * 將二進制字節碼流解析爲 Class 實例
 */
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError {
    return defineClass(name, b, off, len, null);
}

/**
 * 查找一個類,這是一個受保護的方法,也是重載 ClassLoader 時,重要的系統擴展點。
 * 這個方法會在 loadClass() 時被調用,用於自定義查找類的邏輯。
 */
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

/**
 * 尋找已經加載的類
 */
protected final Class<?> findLoadedClass(String name) {
    if (!checkName(name))
        return null;
    return findLoadedClass0(name);
}

在 ClassLoader 的結構中,還有一個重要的字段 parent,它也是一個 ClassLoader 的實例,這個字段所表示的 ClassLoader 也稱爲這個 ClassLoader 的雙親。在類加載的過程當中, ClassLoader 可能會將某些請求交予本身的雙親處理。apache

2、ClassLoader 分類

在標準的 Java 程序中,Java 虛擬機會建立 3 類 ClassLoader 爲整個應用程序服務。 它們分別是: BootstrapClassLoader(啓動類加載器)、 ExtensionClassLoader(擴展類加載器)和 AppClassLoader(應用類加載器,也稱爲系統類加載器)。 此外,每個應用程序還能夠擁有自定義的 ClassLoader,擴展 Java 虛擬機獲取 Class 數據的能力。性能優化

  • BootstrapClassLoader 加載 rt.jar
  • ExtensionClassLoader 加載 $JAVA_HOME/lib/ext/*.jar
  • AppClassLoader 加載 classpath 下的 *.jar

各個 ClassLoader 的層次自頂往下爲啓動類加載器、擴展類加載器、應用類加載器和自定義類加載器。其中,應用類加載器的雙親爲擴展類加載器,擴展類加載器的雙親爲啓動類加載器。當系統須要使用一個類時,在判斷類是否已經被加載時,會從當前底層類加載器進行判斷。當系統須要加載一個類時,會從頂層類開始加載,依次向下嘗試,直到到成功。框架

ClassLoader分類

在這些些 ClassLoader 中,啓動類加載器最爲特別,它是徹底由 C 代碼實現的,而且在 Java 中沒有對象與之對應。系統的核心類就是由啓動類加載器進行加載的,它也是虛擬機的核心組件。擴展類加載器和應用類加載器都有對應的 Java 對象可供使用。jvm

// 輸出所有的類加載器
public class PrintClassLoaderTree {

    public static void main(String[] args) {
        ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();
        while (classLoader != null) {
            System.out.println(classLoader);
            classLoader = classLoader.getParent();
        }
    }
}

結果以下:ide

sun.misc.Launcher$AppClassLoader@b4aac2
sun.misc.Launcher$ExtClassLoader@1dac704

3、ClassLoader 的雙親委託模式

系統中的 ClassLoader 在協協同工做時,默認會使用雙親委託模式。即在類加載的時候,系統會判斷當前類是否已經被加載,若是已經被加載,就會直接返回可用的類,不然就會嘗試加載,在嘗試加載時,會先請求雙親處理,如若是雙親請求失敗,則會本身加載。函數

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);     // (1)
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {           // (2)
                    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);        // (3)

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

(1) 當前 ClassLoader 試圖查找該類是否已經被加載,若是已經被加載則直接返回。性能

(2) 若是沒有被加載,則會請求其雙親加載(不是本身加載),若是雙親爲 null 時,則使用啓動類加載器加載。

(3) 若是雙親加載不成功,則由當前 ClassLoader 嘗試加載。

4、雙親委託模式的弊端

在前文中已經提到,檢查類是否已經加載的委託過程是單向的。這種方式雖然從結構上說比較清晰,使各個 ClassLoader 的職責很是明確,可是同時會帶來一個問題,即頂層的 ClassLoader 沒法訪問底層的 ClassLoader 所加載的類。

類加載順序

一般狀況下,啓動類加載器中的類爲系統核心類,包括一些重要的系統接口,而在應用類加載器中,爲應用類。按照這種模式,應用類訪問系統類天然是沒有問題,可是系統類訪問應用類就會出現問。好比,在系統類中,提供了一個接口,該接口須要在應用中得以實現,該接口還綁定一個工廠方法,用於建立該接口的實例,而接口和工廠方法都在啓動類加載器中。這時,就會出現該工廠方法沒法建立由應用類加載器加載的應用實例的問題。擁有這種問題的組件有不少,好比 JDBC、 XmlParser 等。

5、雙親委託模式的補充

在 Java 平臺中,把核心類(rt.jar)中提供外部服務,可由應用層自行實現的接口,一般能夠稱爲 ServiceProvider Interface,即 SPI。

下面以 javax.xml.parsers 中實現 XML 文件解析功能模塊爲例,說明如何在啓動類加載器中,訪問由應用類加載器實現的 SPI 接口實例。在 javax.xml.parsers.DocumentBuilderfactory 中有以下實現,用來構造一個 Documentbuilderfactory 實例,注意 Document Builderfactory是一個抽象類(加載在啓動類加載器中),能夠由應用程序自行實現,這裏也將介紹該方法如何返回一個在應用類加載器中的實例。

public static DocumentBuilderFactory newInstance() {
    return FactoryFinder.find(
            /* The default property name according to the JAXP spec */
            DocumentBuilderFactory.class, // "javax.xml.parsers.DocumentBuilderFactory"
            /* The fallback implementation class name */
            "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
}

FactoryFinder.find() 函數試圖加載並返回一個 DocumentBuilderFactory 實例。當這個實例在應用層 jar 包裏時,它會使用以下方法進行查找:

T provider = findServiceProvider(type);

其中 type 就是字符串 "Javax.xml.parsers.DocumentBuilderFactory", findServiceProvider 的主要內容以下代碼所示,這段代碼碼並不是 JDK 中的源碼,爲了節省版面,筆者作了適當的裁剪,只保留核心部分。

// jdk1.8 FactoryFinder 調用 ServiceLoader.load(type) 加載 jar 包中的實現類
private static <T> T findServiceProvider(final Class<T> type) {
    try {
        return AccessController.doPrivileged(new PrivilegedAction<T>() {
            public T run() {
                final ServiceLoader<T> serviceLoader = ServiceLoader.load(type);
                final Iterator<T> iterator = serviceLoader.iterator();
                if (iterator.hasNext()) {
                    return iterator.next();
                } else {
                    return null;
                }
             }
        });
    } catch(ServiceConfigurationError e) {
        final RuntimeException x = new RuntimeException(
                "Provider for " + type + " cannot be created", e);
        final FactoryConfigurationError error =
                new FactoryConfigurationError(x, x.getMessage());
        throw error;
    }
}

// ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

從以上代碼可知,ServiceLoader 得到了一個名爲上下文加載器的 ClassLoader,從如下代碼可知,上下文加載器是從 Thread.currentThread().getContextClassLoader() 中獲得的。並將此 ClassLoader 傳入 ServiceLoader.load(service, cl) 方法,由這個 ClassLoader 去完成實例的加載和建立,而不是由這段代碼所在的啓動類加載器去加載。

6、突破雙親模式

雙親模式的類加載方式是虛擬機默認的行爲,但並不是必須這麼作,經過重載 ClassLoader 能夠修改該行爲。事實上,很多應用軟件和框架都修改了這種行爲,好比 Tomcat 和 OSGi 框架,都有各自獨特的類加載順序。在本小節中,將演示如何打破默認的雙親模式。

下面的代碼經過重載 loadclass() 方法,改變類的加載次序,這裏給出部分核心代碼:

public class MyClassLoader extends ClassLoader {

    private String dir;

    public MyClassLoader(String dir) {
        this.dir = dir;
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findClass(className);
        if (clazz == null) {
            //System.out.println("can't load class:" + className + " need from parent");
            return super.loadClass(className, resolve);
        }
        return clazz;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        if (className.startsWith("java")) {
            return null;
        }

        Class<?> clazz = super.findLoadedClass(className);
        if (clazz == null) {
            FileInputStream fis = null;
            try {
                String classFile = getClassFile(className);
                fis = new FileInputStream(new File(classFile));
                FileChannel fc = fis.getChannel();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                WritableByteChannel wbc = Channels.newChannel(baos);
                ByteBuffer buf = ByteBuffer.allocate(1024);
                while (true) {
                    int len = fc.read(buf);
                    if (len == 0 || len == -1) {
                        break;
                    }
                    buf.flip();
                    wbc.write(buf);
                    buf.clear();
                }
                byte[] bytes = baos.toByteArray();
                clazz = defineClass(className, bytes, 0, bytes.length);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        ;
                    }
                }
            }
            return clazz;
        }
        return super.findClass(className);
    }

    public String getClassFile(String className) {
        String path = className.replace(".", File.separator);
        return dir + File.separator + path + ".class";
    }
}

以上代碼經過自定義 ClassLoader,重載 loadClass() 改變了默認的委託雙親加載的方式,經過 findClass() 讀取 class 文件,並將二進制流定義爲 Class 對象。若是加載不到,則委託雙親加載,這種方式顏倒了默認的加載順序。

public static void main(String[] args) throws Exception, InstantiationException {
    MyClassLoader myClassLoader = new MyClassLoader(
            "F:\\doc\\java\\code-2018\\disruptor\\target\\test-classes");
    Class<?> clazz = myClassLoader.loadClass(MyClass.class.getName(), true);

    ClassLoader classLoader = clazz.getClassLoader();
    while (classLoader != null) {
        System.out.println(classLoader);
        classLoader = classLoader.getParent();
    }

    Method method = clazz.getMethod("sayHello");
    method.invoke(clazz.newInstance());
}

自定義的類加載器默認的父加載器爲系統類加載器:

com.github.binarylei.jvm.classloader.MyClassLoader@7f7052
sun.misc.Launcher$AppClassLoader@b4aac2
sun.misc.Launcher$ExtClassLoader@899482

7、熱替換的實現

但對 Java 來講,熱替換並不是天生就支持,若是一個類已經加載到系統中,經過修改類文件並沒有法述緊統再來加載並重定義這個類。所以,在 Java 中實現這一功能的一個可行的方法就是靈活運用 ClassLoader。由不一樣 ClassLoader 加載的同名類屬於不一樣的類型,不能相互轉化和兼容。

熱替換的思路

import java.io.*;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

/**
 * @author: leigang
 * @version: 2018-07-04
 */
public class HotClassLoader extends ClassLoader {

    private String dir;

    public HotClassLoader(String dir) {
        this.dir = dir;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        if (!className.startsWith("com.github.binarylei")) {
            return null;
        }
        Class<?> clazz = super.findLoadedClass(className);
        if (clazz == null) {
            FileInputStream fis = null;
            try {
                String classFile = getClassFile(className);
                fis = new FileInputStream(new File(classFile));
                FileChannel fc = fis.getChannel();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                WritableByteChannel wbc = Channels.newChannel(baos);
                ByteBuffer buf = ByteBuffer.allocate(1024);
                while (true) {
                    int len = fc.read(buf);
                    if (len == 0 || len == -1) {
                        break;
                    }
                    buf.flip();
                    wbc.write(buf);
                    buf.clear();
                }
                byte[] bytes = baos.toByteArray();
                clazz = defineClass(className, bytes, 0, bytes.length);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        ;
                    }
                }
            }
            return clazz;
        }
        return super.findClass(className);
    }

    public String getClassFile(String className) {
        String path = className.replace(".", File.separator);
        return dir + File.separator + path + ".class";
    }
}

準備一個要替換的類:

package com.github.binarylei.jvm.classloader;

public class DemoA {
    public void sayHello() {
        System.out.println("++++++++++++");
        //System.out.println("------------");
    }
}

測試:

public static void main(String[] args) throws Exception, InstantiationException {
    while (true) {
        HotClassLoader myClassLoader = new HotClassLoader("D:\\tmp\\clz");
        Class<?> clazz = myClassLoader.loadClass("com.github.binarylei.jvm.classloader.DemoA");

        Method method = clazz.getMethod("sayHello");
        method.invoke(clazz.newInstance());

        Thread.sleep(1000);
    }
}

參考:

本文轉載至《實戰JAVA虛擬機 JVM故障診斷與性能優化》第十章


天天用心記錄一點點。內容也許不重要,但習慣很重要!

相關文章
相關標籤/搜索