玩命學JVM(二)—類加載機制

前言

Java程序運行圖:
Alt

上一篇玩命學JVM(一)—認識JVM和位元組碼文件我們簡單認識了 JVM 和位元組碼文件。那JVM是如何使用位元組碼文件的呢?從上圖清晰地可以看到,JVM 通過類加載器完成了這一過程。

以下是類加載機制的知識框架:

Alt

接下來我們對思維導圖中重難點部分做補充。

1. 是什麼?

類的加載就是將 .class 文件的二進制數據讀入到內存中,將其放在 JVM 的運行時數據區的方法區內。然後在堆區內創建一個 java.lang.Class 對象,用於封裝類在方法區內的數據結構。

5. 雙親委派模型

雙親委派模型圖如下:
Alt

對於「雙親委派模型」,首先需要糾正一點,「雙親」並不是說它有「兩個親」。實際上行「雙親委派模型」和「雙」毫無關係,只和「親」有關係。
其實「雙親」是翻譯的一個錯誤,原文出處是「parent」,被翻譯成了「雙親」,在計算機領域更常見的說法是「父節點」。所以如果將「雙親委派模型」改為「父委派模型」,應該更好理解。

結合實際的類加載器來說,就是:

  1. 每個類加載器都會向上找自己父類加載器嘗試完成類加載;
  2. 父加載器加載失敗會向下找加載器嘗試加載。
    如圖:

Alt

接下來我們從源碼上來分析下 雙親委派模型
Bootstrap ClassLoader外,其它的類加載器都是ClassLoader的子類。加載類的方法為loadClass,查看源碼可發現,loadClassClassLoader中有具體的實現,且在各個子類中都沒有被覆蓋。

先介紹三個重要的函數,對後續的源碼閱讀有幫助:
loadClass:調用父類加載器的loadClass,加載失敗則調用自己的findClass方法。
findClass:根據名稱讀取文件存入位元組數組。
defineClass:把一個位元組數組轉為Class對象。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 在JVM中查看類是否已經被加載
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
	                    // 調用父類加載器的 loadClass方法,parent是該類加載器的父類,parent的值可能為 Application ClassLoader、Extension ClassLoader,當想要繼續往上找 Extension ClassLoader時,由於Bootstrap ClassLoader是C/C++實現的,所以在java中是Null
                        c = parent.loadClass(name, false);
                    } else {
	                    // 尋找 Bootstrap ClassLoader 加載
                        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();
                    // 父加載器開始嘗試加載.class文件,加載成功就返回一個java.lang.Class,加載不成功就拋出一個ClassNotFoundException,給子加載器去加載
                    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) {
            // 如果要解析這個.class文件的話,就解析一下,解析的作用主要就是將符號引用替換為直接引用的過程
                resolveClass(c);
            }
            return c;
        }
    }

所謂的雙親委派模型,就是利用了loadClass只在父類中實現了這一點。

自定義類加載器

自定義類加載主要有兩種方式:

  1. 遵守雙親委派模型:繼承ClassLoader,重寫findClass()方法。

  2. 破壞雙親委派模型:繼承ClassLoader,重寫loadClass()方法。 通常我們推薦採用第一種方法自定義類加載器,最大程度上的遵守雙親委派模型。

我們看一下實現步驟
(1)創建一個類繼承ClassLoader抽象類
(2)重寫findClass()方法
(3)在findClass()方法中調用defineClass()

第一步,自定義一個實體類Person.java,我把它編譯後的Person.class放在D盤根目錄下:

package com.xrq.classloader;

public class Person
{
    private String name;

    public Person()
    {

    }

    public Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public String toString()
    {
        return "I am a person, my name is " + name;
    }
}

第二步,自定義一個類加載器,裏面主要是一些IO和NIO的內容,另外注意一下 defineClass方法可以把二進制流位元組組成的文件轉換為一個java.lang.Class—-只要二進制位元組流的內容符合Class文件規 范。我們自定義的MyClassLoader繼承自java.lang.ClassLoader,就像上面說的,只實現findClass方法:

public class MyClassLoader extends ClassLoader
{
    public MyClassLoader()
    {
        
    }
    
    public MyClassLoader(ClassLoader parent)
    {
        super(parent);
    }
    
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        } 
        catch (Exception e)
        {
            e.printStackTrace();
        }
        
        return super.findClass(name);
    }
    
    private File getClassFile(String name)
    {
        File file = new File("D:/Person.class");
        return file;
    }
    
    private byte[] getClassBytes(File file) throws Exception
    {
        // 這裡要讀入.class的位元組,因此要使用位元組流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        
        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        
        fis.close();
        
        return baos.toByteArray();
    }
}

第三步,Class.forName有一個三個參數的重載方法,可以指定類加載器,平時我們使用的Class.forName(“XX.XX.XXX”)都是使用的系統類加載器Application ClassLoader。寫一個測試類:

public class TestMyClassLoader
{
    public static void main(String[] args) throws Exception
    {
        MyClassLoader mcl = new MyClassLoader();
        Class<?> c1 = Class.forName("com.xrq.classloader.Person", true, mcl);
        Object obj = c1.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}

運行結果:

I am a person, my name is null
com.xrq.classloader.MyClassLoader@5d888759

參考文獻

//baijiahao.baidu.com/s?id=1636309817155065432&wfr=spider&for=pc
//blog.csdn.net/qq_44836294/article/details/105439753

Tags: