欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JVM入門之類加載與字節(jié)碼技術(shù)(類加載與類的加載器)

 更新時間:2021年06月15日 17:07:19   作者:興趣使然的草帽路飛  
Java字節(jié)碼增強指的是在Java字節(jié)碼生成之后,對其進行修改,增強其功能,這種方式相當于對應(yīng)用程序的二進制文件進行修改。Java字節(jié)碼增強主要是為了減少冗余代碼,提高性能等

1. 類加載階段

1.1 加載階段

  • 將類的字節(jié)碼載入方法區(qū)中,內(nèi)部采用 C++ 的 instanceKlass 描述 java 類,它的重要 field 有:
    • _java_mirror 即 java 的類鏡像,例如對 String 來說,就是 String.class,作用是把 klass 暴 露給 java 使用
    • _super 即父類
    • _fields 即成員變量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即類加載器
    • _vtable 虛方法表
    • _itable 接口方法表
  • 如果這個類還有父類沒有加載,則先觸發(fā)父類的加載。
  • 加載和鏈接可能是交替運行的。

注意:

  • instanceKlass 這樣的【元數(shù)據(jù)】是存儲在方法區(qū)(1.8 后的元空間內(nèi)),但 _java_mirror 是存儲在堆中
  • 可以通過前面介紹的 HSDB 工具查看

在這里插入圖片描述

1.2 鏈接階段

驗證

驗證類是否符合 JVM規(guī)范,安全性檢查,阻止不合法的類繼續(xù)運行。用 UE 等支持二進制的編輯器修改 HelloWorld.class的魔數(shù),在控制臺運行:

E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value
3405691578 in class file cn/itcast/jvm/t5/HelloWorld
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
        at
java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
        at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

準備

為 static 變量分配空間,設(shè)置默認值:

  • static 變量在 JDK 7 之前存儲于 instanceKlass 末尾,從 JDK 7 開始,存儲于 _java_mirror 末尾
  • static 變量分配空間和賦值是兩個步驟,分配空間在準備階段完成,賦值在初始化階段完成
  • 如果 static 變量是 final 的基本類型,以及字符串常量,那么編譯階段值就確定了,賦值在準備階 段完成
  • 如果 static 變量是 final 的,但屬于引用類型,那么賦值也會在初始化階段完成
  • 將常量池中的符號引用解析為直接引用

解析

將常量池中的符號引用解析為直接引用

/**
* 解析的含義
*/
public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException,IOException {
        ClassLoader classloader = Load2.class.getClassLoader();
        // loadClass 方法不會導(dǎo)致類的解析和初始化
        Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
        // new C();
        System.in.read();
    }
}

class C {
	D d = new D();
}

class D {
}

1.3 初始化階段

< init()> V 方法

初始化即調(diào)用 < cinit>()V ,虛擬機會保證這個類的『構(gòu)造方法』的線程安全。

發(fā)生的時機

概括得說,類初始化是【懶惰的】

  • main 方法所在的類,總會被首先初始化
  • 首次訪問這個類的靜態(tài)變量或靜態(tài)方法時
  • 子類初始化,如果父類還沒初始化,會引發(fā)
  • 子類訪問父類的靜態(tài)變量,只會觸發(fā)父類的初始化
  • Class.forName
  • new 會導(dǎo)致初始化

不會導(dǎo)致類初始化的情況:

  • 訪問類的 static final 靜態(tài)常量(基本類型和字符串)不會觸發(fā)初始化
  • 類對象.class 不會觸發(fā)初始化
  • 創(chuàng)建該類的數(shù)組不會觸發(fā)初始化
  • 類加載器的 loadClass 方法

測試代碼:

class A {
    static int a = 0;
    static {
    	System.out.println("a init");
    }
}
class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
    	System.out.println("b init");
    }
}

驗證(測試時請先全部注釋,每次只執(zhí)行其中一個)

public class Load3 {
    // main方法的所在類總會被先初始化
    static {
    	System.out.println("main init");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        // 1. 靜態(tài)常量(基本類型和字符串)不會觸發(fā)初始化
        System.out.println(B.b);
        // 2. 類對象.class 不會觸發(fā)初始化
        System.out.println(B.class);
        // 3. 創(chuàng)建該類的數(shù)組不會觸發(fā)初始化
        System.out.println(new B[0]);
        // 4. 不會初始化類 B,但會加載 B、A
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        cl.loadClass("cn.itcast.jvm.t3.B");
        // 5. 不會初始化類 B,但會加載 B、A
        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
        Class.forName("cn.itcast.jvm.t3.B", false, c2);
        // 1. 首次訪問這個類的靜態(tài)變量或靜態(tài)方法時
        System.out.println(A.a);
        // 2. 子類初始化,如果父類還沒初始化,會引發(fā)
        System.out.println(B.c);
        // 3. 子類訪問父類靜態(tài)變量,只觸發(fā)父類初始化
        System.out.println(B.a);
        // 4. 會初始化類 B,并先初始化類 A
        Class.forName("cn.itcast.jvm.t3.B");
    }
}

1.4 練習(xí)

從字節(jié)碼分析,使用 a,b,c 這三個常量是否會導(dǎo)致 E 初始化:

public class Load4 {
    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        System.out.println(E.c);
    }
}
class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;
}

典型應(yīng)用 - 完成懶惰初始化單例模式:

public final class Singleton {
    private Singleton() { }
    // 內(nèi)部類中保存單例
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    // 第一次調(diào)用 getInstance 方法,才會導(dǎo)致內(nèi)部類加載和初始化其靜態(tài)成員
    public static Singleton getInstance() {
    	return LazyHolder.INSTANCE;
    }
}

以上的實現(xiàn)特點是:

  • 懶惰實例化
  • 初始化時的線程安全是有保障的


2. 類加載器

以 JDK 8 為例:

名稱 加載哪的類 說明
Bootstrap ClassLoader(啟動類加載器) JAVA_HOME/jre/lib 無法直接訪問
Extension ClassLoader(擴展類加載器) JAVA_HOME/jre/lib/ext 上級為 Bootstrap,顯示為 null
Application ClassLoader(應(yīng)用程序類加載器) classpath 上級為 Extension
自定義類加載器 自定義 上級為 Application

類加載器的優(yōu)先級(由高到低):啟動類加載器 -> 擴展類加載器 -> 應(yīng)用程序類加載器 -> 自定義類加載器

2.1 啟動類加載器

用 Bootstrap 類加載器加載類:

package cn.itcast.jvm.t3.load;
public class F {
    static {
   		System.out.println("bootstrap F init");
    }
}

執(zhí)行:

package cn.itcast.jvm.t3.load;
public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
        // aClass.getClassLoader():獲得aClass對應(yīng)的類加載器
        System.out.println(aClass.getClassLoader());
    }
}

輸出:

  • -Xbootclasspath 表示設(shè)置 bootclasspath
  • 其中 /a:. 表示將當前目錄追加至 bootclasspath 之后
  • 可以有以下幾個方式替換啟動類路徑下的核心類:
    • java -Xbootclasspath: < new bootclasspath>
    • 前追加:java -Xbootclasspath/a:<追加路徑>
    • 后追加:java -Xbootclasspath/p:<追加路徑>

在這里插入圖片描述

2.2 擴展類加載器

package cn.itcast.jvm.t3.load;
public class G {
    static {
    	System.out.println("classpath G init");
    }
}

程序執(zhí)行:

public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

輸出結(jié)果:

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2 // 這個類是由應(yīng)用程序加載器加載

寫一個同名的類:

package cn.itcast.jvm.t3.load;
public class G {
    static {
    	System.out.println("ext G init");
    }
}

打個 jar 包:

E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class // 將G.class打jar包
已添加清單
正在添加: cn/itcast/jvm/t3/load/G.class(輸入 = 481) (輸出 = 322)(壓縮了 33%)

將 jar 包拷貝到JAVA_HOME/jre/lib/ext(擴展類加載器加載的類必須是以jar包方式存在),重新執(zhí)行 Load5_2

輸出:

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44 // 這個類是由擴展類加載器加載

2.3 雙親委派模式

所謂的雙親委派,就是指調(diào)用類加載器的 loadClass 方法時,查找類的規(guī)則。

注意:這里的雙親,翻譯為上級似乎更為合適,因為它們并沒有繼承關(guān)系

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 檢查該類是否已經(jīng)加載
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2. 有上級的話,委派上級 loadClass
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 如果沒有上級了(ExtClassLoader),則委派
                    BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {
                long t1 = System.nanoTime();
                // 4. 每一層找不到,調(diào)用 findClass 方法(每個類加載器自己擴展)來加載
                c = findClass(name);
                // 5. 記錄耗時
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
        	resolveClass(c);
        }
        return c;
    }
}

例如:

public class Load5_3 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Load5_3.class.getClassLoader()
        			.loadClass("cn.itcast.jvm.t3.load.H");
        System.out.println(aClass.getClassLoader());
    }
}

執(zhí)行流程為:

  • sun.misc.Launcher$AppClassLoader // 1 處, 開始查看已加載的類,結(jié)果沒有
  • sun.misc.Launcher$AppClassLoader // 2 處,委派上級 sun.misc.Launcher$ExtClassLoader.loadClass()
  • sun.misc.Launcher$ExtClassLoader // 1 處,查看已加載的類,結(jié)果沒有
  • sun.misc.Launcher$ExtClassLoader // 3 處,沒有上級了,則委派 BootstrapClassLoader 查找
  • BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 這個類,顯然沒有
  • sun.misc.Launcher$ExtClassLoader // 4 處,調(diào)用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 這個類,顯然沒有,回到 sun.misc.Launcher$AppClassLoader 的 // 2 處
  • 繼續(xù)執(zhí)行到 sun.misc.Launcher$AppClassLoader // 4 處,調(diào)用它自己的 findClass 方法,在 classpath 下查找,找到了

2.4 線程上下文類加載器

我們在使用 JDBC 時,都需要加載 Driver 驅(qū)動,不知道你注意到?jīng)]有,不寫

Class.forName("com.mysql.jdbc.Driver")

也是可以讓 com.mysql.jdbc.Driver 正確加載的,你知道是怎么做的嗎? 讓我們追蹤一下源碼:

public class DriverManager {
    // 注冊驅(qū)動的集合
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers 
        = new CopyOnWriteArrayList<>();
    // 初始化驅(qū)動
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

先不看別的,看看 DriverManager 的類加載器:

System.out.println(DriverManager.class.getClassLoader());

打印 null,表示它的類加載器是 Bootstrap ClassLoader,會到 JAVA_HOME/jre/lib 下搜索類,但 JAVA_HOME/jre/lib 下顯然沒有 mysql-connector-java-5.1.47.jar 包,這樣問題來了,在 DriverManager 的靜態(tài)代碼塊中,怎么能正確加載 com.mysql.jdbc.Driver 呢?

繼續(xù)看 loadInitialDrivers() 方法:

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
            	return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
    	drivers = null;
    }
    // 1)使用 ServiceLoader 機制加載驅(qū)動,即 SPI
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
    	public Void run() {
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
                // Do nothing
            }
            return null;
        }
    });
    println("DriverManager.initialize: jdbc.drivers = " + drivers);
    // 2)使用 jdbc.drivers 定義的驅(qū)動名加載驅(qū)動
    if (drivers == null || drivers.equals("")) {
    	return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 這里的 ClassLoader.getSystemClassLoader() 就是應(yīng)用程序類加載器
            Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
        	println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

先看 2)發(fā)現(xiàn)它最后是使用 Class.forName 完成類的加載和初始化,關(guān)聯(lián)的是應(yīng)用程序類加載器,因此 可以順利完成類加載

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

約定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名為文件,文件內(nèi)容是實現(xiàn)類名稱

在這里插入圖片描述

這樣就可以使用:

ServiceLoader<接口類型> allImpls = ServiceLoader.load(接口類型.class);
Iterator<接口類型> iter = allImpls.iterator();
while(iter.hasNext()) {
	iter.next();
}

來得到實現(xiàn)類,體現(xiàn)的是【面向接口編程+解耦】的思想,在下面一些框架中都運用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(對 SPI 進行了擴展)

接著看 ServiceLoader.load 方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 獲取線程上下文類加載器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

線程上下文類加載器是當前線程使用的類加載器,默認就是應(yīng)用程序類加載器,它內(nèi)部又是由 Class.forName 調(diào)用了線程上下文類加載器完成類加載,具體代碼在 ServiceLoader 的內(nèi)部類 LazyIterator 中:

private S nextService() {
    if (!hasNextService())
    	throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
    	c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service, "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service, "Provider " + cn + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service, "Provider " + cn + " could not be instantiated", x);
    }
    throw new Error(); // This cannot happen
}

2.5 自定義類加載器

問問自己,什么時候需要自定義類加載器:

  • 1)想加載非 classpath 隨意路徑中的類文件
  • 2)都是通過接口來使用實現(xiàn),希望解耦時,常用在框架設(shè)計
  • 3)這些類希望予以隔離,不同應(yīng)用的同名類都可以加載,不沖突,常見于 tomcat 容器

步驟:

  • 繼承 ClassLoader 父類
  • 要遵從雙親委派機制,重寫 findClass 方法 注意不是重寫 loadClass 方法,否則不會走雙親委派機制
  • 讀取類文件的字節(jié)碼
  • 調(diào)用父類的 defineClass 方法來加載類
  • 使用者調(diào)用該類加載器的 loadClass 方法

3.總結(jié)

到此這篇關(guān)于JVM入門之類加載與字節(jié)碼技術(shù)(類加載與類的加載器)的文章就介紹到這了,更多相關(guān)JVM 類加載與字節(jié)碼技術(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論