您好,登錄后才能下訂單哦!
本篇內容介紹了“如何理解JVM中類加載與字節碼技術(類加載與類的加載器)”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
將類的字節碼載入方法區中,內部采用 C++ 的 instanceKlass 描述 java 類,它的重要 field 有:
_java_mirror
即 java 的類鏡像,例如對 String 來說,就是 String.class,作用是把 klass 暴 露給 java 使用
_super
即父類
_fields
即成員變量
_methods
即方法
_constants
即常量池
_class_loader
即類加載器
_vtable
虛方法表
_itable
接口方法表
如果這個類還有父類沒有加載,則先觸發父類的加載。
加載和鏈接可能是交替運行的。
注意:
instanceKlass 這樣的【元數據】是存儲在方法區(1.8 后的元空間內),但 _java_mirror 是存儲在堆中
可以通過前面介紹的 HSDB 工具查看
驗證
驗證類是否符合 JVM規范,安全性檢查,阻止不合法的類繼續運行。用 UE 等支持二進制的編輯器修改 HelloWorld.class的魔數,在控制臺運行:
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 變量分配空間,設置默認值:
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 方法不會導致類的解析和初始化 Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C"); // new C(); System.in.read(); } } class C { D d = new D(); } class D { }
< init()> V 方法
初始化即調用 < cinit>()V ,虛擬機會保證這個類的『構造方法』的線程安全。
發生的時機
概括得說,類初始化是【懶惰的】
main 方法所在的類,總會被首先初始化
首次訪問這個類的靜態變量或靜態方法時
子類初始化,如果父類還沒初始化,會引發
子類訪問父類的靜態變量,只會觸發父類的初始化
Class.forName
new 會導致初始化
不會導致類初始化的情況:
訪問類的 static final 靜態常量(基本類型和字符串)不會觸發初始化
類對象.class 不會觸發初始化
創建該類的數組不會觸發初始化
類加載器的 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"); } }
驗證(測試時請先全部注釋,每次只執行其中一個)
public class Load3 { // main方法的所在類總會被先初始化 static { System.out.println("main init"); } public static void main(String[] args) throws ClassNotFoundException { // 1. 靜態常量(基本類型和字符串)不會觸發初始化 System.out.println(B.b); // 2. 類對象.class 不會觸發初始化 System.out.println(B.class); // 3. 創建該類的數組不會觸發初始化 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. 首次訪問這個類的靜態變量或靜態方法時 System.out.println(A.a); // 2. 子類初始化,如果父類還沒初始化,會引發 System.out.println(B.c); // 3. 子類訪問父類靜態變量,只觸發父類初始化 System.out.println(B.a); // 4. 會初始化類 B,并先初始化類 A Class.forName("cn.itcast.jvm.t3.B"); } }
從字節碼分析,使用 a,b,c 這三個常量是否會導致 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; }
典型應用 - 完成懶惰初始化單例模式:
public final class Singleton { private Singleton() { } // 內部類中保存單例 private static class LazyHolder { static final Singleton INSTANCE = new Singleton(); } // 第一次調用 getInstance 方法,才會導致內部類加載和初始化其靜態成員 public static Singleton getInstance() { return LazyHolder.INSTANCE; } }
以上的實現特點是:
懶惰實例化
初始化時的線程安全是有保障的
以 JDK 8 為例:
名稱 | 加載哪的類 | 說明 |
---|---|---|
Bootstrap ClassLoader(啟動類加載器) | JAVA_HOME/jre/lib | 無法直接訪問 |
Extension ClassLoader(擴展類加載器) | JAVA_HOME/jre/lib/ext | 上級為 Bootstrap,顯示為 null |
Application ClassLoader(應用程序類加載器) | classpath | 上級為 Extension |
自定義類加載器 | 自定義 | 上級為 Application |
類加載器的優先級(由高到低):啟動類加載器 -> 擴展類加載器 -> 應用程序類加載器 -> 自定義類加載器
用 Bootstrap 類加載器加載類:
package cn.itcast.jvm.t3.load; public class F { static { System.out.println("bootstrap F init"); } }
執行:
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對應的類加載器 System.out.println(aClass.getClassLoader()); } }
輸出:
-Xbootclasspath
表示設置 bootclasspath
其中 /a:. 表示將當前目錄追加至 bootclasspath
之后
可以有以下幾個方式替換啟動類路徑下的核心類:
java -Xbootclasspath: < new bootclasspath>
前追加:java -Xbootclasspath/a:<追加路徑>
后追加:java -Xbootclasspath/p:<追加路徑>
package cn.itcast.jvm.t3.load; public class G { static { System.out.println("classpath G init"); } }
程序執行:
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()); } }
輸出結果:
classpath G init sun.misc.Launcher$AppClassLoader@18b4aac2 // 這個類是由應用程序加載器加載
寫一個同名的類:
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包方式存在),重新執行 Load5_2
輸出:
ext G init sun.misc.Launcher$ExtClassLoader@29453f44 // 這個類是由擴展類加載器加載
所謂的雙親委派,就是指調用類加載器的 loadClass 方法時,查找類的規則。
注意:這里的雙親,翻譯為上級似乎更為合適,因為它們并沒有繼承關系
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1. 檢查該類是否已經加載 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. 每一層找不到,調用 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()); } }
執行流程為:
sun.misc.Launcher$AppClassLoader
// 1 處, 開始查看已加載的類,結果沒有
sun.misc.Launcher$AppClassLoader
// 2 處,委派上級 sun.misc.Launcher$ExtClassLoader.loadClass()
sun.misc.Launcher$ExtClassLoader
// 1 處,查看已加載的類,結果沒有
sun.misc.Launcher$ExtClassLoader
// 3 處,沒有上級了,則委派 BootstrapClassLoader 查找
BootstrapClassLoader 是在 JAVA_HOME/jre/lib
下找 H 這個類,顯然沒有
sun.misc.Launcher$ExtClassLoader
// 4 處,調用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext
下找 H 這個類,顯然沒有,回到 sun.misc.Launcher$AppClassLoader
的 // 2 處
繼續執行到 sun.misc.Launcher$AppClassLoader
// 4 處,調用它自己的 findClass 方法,在 classpath 下查找,找到了
我們在使用 JDBC 時,都需要加載 Driver 驅動,不知道你注意到沒有,不寫
Class.forName("com.mysql.jdbc.Driver")
也是可以讓 com.mysql.jdbc.Driver 正確加載的,你知道是怎么做的嗎? 讓我們追蹤一下源碼:
public class DriverManager { // 注冊驅動的集合 private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); // 初始化驅動 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 的靜態代碼塊中,怎么能正確加載 com.mysql.jdbc.Driver 呢?
繼續看 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 機制加載驅動,即 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 定義的驅動名加載驅動 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() 就是應用程序類加載器 Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
先看 2)發現它最后是使用 Class.forName 完成類的加載和初始化,關聯的是應用程序類加載器,因此 可以順利完成類加載
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)
約定如下,在 jar 包的 META-INF/services
包下,以接口全限定名名為文件,文件內容是實現類名稱
這樣就可以使用:
ServiceLoader<接口類型> allImpls = ServiceLoader.load(接口類型.class); Iterator<接口類型> iter = allImpls.iterator(); while(iter.hasNext()) { iter.next(); }
來得到實現類,體現的是【面向接口編程+解耦】的思想,在下面一些框架中都運用了此思想:
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); }
線程上下文類加載器是當前線程使用的類加載器,默認就是應用程序類加載器,它內部又是由 Class.forName 調用了線程上下文類加載器完成類加載,具體代碼在 ServiceLoader 的內部類 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 }
問問自己,什么時候需要自定義類加載器:
1)想加載非 classpath 隨意路徑中的類文件
2)都是通過接口來使用實現,希望解耦時,常用在框架設計
3)這些類希望予以隔離,不同應用的同名類都可以加載,不沖突,常見于 tomcat 容器
步驟:
繼承 ClassLoader 父類
要遵從雙親委派機制,重寫 findClass 方法 注意不是重寫 loadClass 方法,否則不會走雙親委派機制
讀取類文件的字節碼
調用父類的 defineClass 方法來加載類
使用者調用該類加載器的 loadClass 方法
“如何理解JVM中類加載與字節碼技術(類加載與類的加載器)”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。