JVM入門之類加載與字節(jié)碼技術(shù)(類加載與類的加載器)
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)文章
我從jdk1.8升級到j(luò)dk11所遇到的坑都有這些
這篇文章主要介紹了從jdk1.8升級到j(luò)dk11將會遇到的一些坑,本文給大家分享解決方案對大家的學(xué)習(xí)或工作具有參考借鑒價值,對jdk1.8升級到j(luò)dk11相關(guān)知識感興趣的朋友,快來看看吧2021-08-08SpringMvc MultipartFile實現(xiàn)圖片文件上傳示例
本篇文章主要介紹了SpringMvc MultipartFile實現(xiàn)圖片文件上傳示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-02-02java servlet手機app訪問接口(三)高德地圖云存儲及檢索
這篇文章主要為大家詳細介紹了java servlet手機app訪問接口(三),高德地圖云存儲及檢索,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12將JSON字符串數(shù)組轉(zhuǎn)對象集合方法步驟
這篇文章主要給大家介紹了關(guān)于將JSON字符串數(shù)組轉(zhuǎn)對象集合的方法步驟,文中通過代碼示例介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-08-08深入了解HttpClient的ResponseHandler接口
這篇文章主要為大家介紹了深入了解HttpClient的ResponseHandler接口,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-10-10linux的shell命令檢測某個java程序是否執(zhí)行
ps -ef |grep java|grep2016-04-04