類是在運行期間第一次使用時動態加載的,而不是一次性加載全部類。由於若是一次性加載,那麼會佔用不少的內存。【感受和單例模式中的懶漢單例有殊途同歸之妙】java
包含了加載、驗證、準備、解析和初始化這 5 個階段git
加載是類加載的一個階段,注意不要混淆。程序員
加載過程完成如下三件事:github
①經過類的徹底限定名稱獲取定義該類的二進制字節流。數組
②將該字節流表示的靜態存儲結構轉換爲方法區的運行時存儲結構。緩存
③在內存中生成一個表明該類的 Class 對象,做爲方法區中該類各類數據的訪問入口。安全
其中二進制字節流能夠從如下方式中獲取:網絡
①從 ZIP 包讀取,成爲 JAR、EAR、WAR 格式的基礎。多線程
②從網絡中獲取,最典型的應用是 Applet。jvm
③運行時計算生成,例如動態代理技術,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理類的二進制字節流。
④由其餘文件生成,例如由 JSP 文件生成對應的 Class 類。
確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
類變量是被 static 修飾的變量,準備階段爲類變量分配內存並設置初始值,使用的是方法區的內存。
實例變量不會在這階段分配內存,它會在對象實例化時隨着對象一塊兒被分配在堆中。應該注意到,實例化不是類加載的一個過程,類加載發生在全部實例化操做以前,而且類加載只進行一次,實例化能夠進行屢次。
初始值通常爲 0 值,例以下面的類變量 value 被初始化爲 0 而不是 123。
public static int value = 123;
若是類變量是常量,那麼它將初始化爲表達式所定義的值而不是 0。例以下面的常量 value 被初始化爲 123 而不是 0。
public static final int value = 123;
解析階段是虛擬機常量池內的符號引用替換爲直接引用的過程。
符號引用:符號引用是一組符號來描述所引用的目標對象,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標對象並不必定已經加載到內存中。
直接引用:直接引用能夠是直接指向目標對象的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機內存佈局實現相關的,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同,若是有了直接引用,那引用的目標一定已經在內存中存在。
虛擬機規範並無規定解析階段發生的具體時間,只要求了在執行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic這13個用於操做符號引用的字節碼指令以前,先對它們使用的符號引用進行解析,因此虛擬機實現會根據須要來判斷,究竟是在類被加載器加載時就對常量池中的符號引用進行解析,仍是等到一個符號引用將要被使用前纔去解析它。
解析的動做主要針對類或接口、字段、類方法、接口方法四類符號引用進行。分別對應編譯後常量池內的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四種常量類型。
將常量池的符號引用替換爲直接引用的過程。【常量池爲JVM方法棧中的內存空間,棧中還有局部變量表、操做數棧】
其中解析過程在某些狀況下能夠在初始化階段以後再開始,這是爲了支持 Java 的動態綁定。
參考連接:https://www.zhihu.com/question/30300585?sort=created
初始化階段才真正開始執行類中定義的 Java 程序代碼。初始化階段是虛擬機執行類構造器 <clinit>() 方法的過程。在準備階段,類變量已經賦過一次系統要求的初始值,而在初始化階段,根據程序員經過程序制定的主觀計劃去初始化類變量和其它資源。
<clinit>() 是由編譯器自動收集類中全部類變量的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序由語句在源文件中出現的順序決定。特別注意的是,靜態語句塊只能訪問到定義在它以前的類變量,定義在它以後的類變量只能賦值,不能訪問。例如如下代碼:
package com.cnblogs.mufasa; public class demo2_5 { static class Parent { public static int A = 1; static { A = 2; } } static class Sub extends Parent { public static int B = A; } static { int i=0; // i=0;//非法向前引用 Illegal forward reference System.out.println(i); } public static void main(String[] args) { System.out.println(Sub.B); // 2 System.out.println(Sub.B); // 2 } }
0 2 2
接口中不可使用靜態語句塊,但仍然有類變量初始化的賦值操做,所以接口與類同樣都會生成 <clinit>() 方法。但接口與類不一樣的是,執行接口的 <clinit>() 方法不須要先執行父接口的 <clinit>() 方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的 <clinit>() 方法。
虛擬機會保證一個類的 <clinit>() 方法在多線程環境下被正確的加鎖和同步,若是多個線程同時初始化一個類,只會有一個線程執行這個類的 <clinit>() 方法,其它線程都會阻塞等待,直到活動線程執行 <clinit>() 方法完畢。若是在一個類的 <clinit>() 方法中有耗時的操做,就可能形成多個線程阻塞,在實際過程當中此種阻塞很隱蔽。
虛擬機規範中並沒有強制約束什麼時候進行加載,可是規範嚴格規定了有且只有下列五種狀況必須對類進行初始化(加載、驗證、準備都會隨之發生):
遇到 new、getstatic、putstatic、invokestatic 這四條字節碼指令時,若是類沒有進行過初始化,則必須先觸發其初始化。最多見的生成這 4 條指令的場景是:①使用 new 關鍵字實例化對象的時候;②③讀取或設置一個類的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候;④以及調用一個類的靜態方法的時候。
使用 java.lang.reflect 包的方法對類進行反射調用的時候,若是類沒有進行初始化,則須要先觸發其初始化【反射實例化】。
當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化【前後問題】。
當虛擬機啓動時,用戶須要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類【Java程序的開端】;
當使用 JDK 1.7 的動態語言支持時,若是一個 java.lang.invoke.MethodHandle 實例最後的解析結果爲 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化;
package com.cnblogs.mufasa.demo3_1; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.invoke.MethodHandle; import java.util.List; class test_reflect{ static { System.out.println("類加載初始化"); } public static String printOut(String str){ System.out.println("靜態方法調用"+str); return str+"123"; } } class child extends test_reflect{ static { System.out.println("子類靜態代碼塊"); } public static void printOut(){ System.out.println("子類靜態方法調用"); } } //class test_MethodHandles{ // //} public class Client2 { //4,用戶須要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類 // static { // System.out.println("虛擬機初始化主類"); // } public static void main(String[] args) throws Throwable { //2,使用 java.lang.reflect 包的方法對類進行反射調用的時候,若是類沒有進行初始化,則須要先觸發其初始化 // Class clazz=test_reflect.class; // Method[] methods=clazz.getDeclaredMethods(); // for(Method method:methods){ // method.invoke(clazz); // } /** * 輸出: * 類加載初始化 * 靜態方法調用 */ //3,當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化 // Class clazz=child.class; // Method[] methods=clazz.getDeclaredMethods(); // for(Method method:methods){ // method.invoke(clazz); // } //5,當使用 JDK 1.7 的動態語言支持時,若是一個 java.lang.invoke.MethodHandle // 實例最後的解析結果爲 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄, // 而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化;【MethodHandle須要繼續完成!】 MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodType mt = MethodType.methodType(String.class,char.class,char.class); try { MethodHandle mh = lookup.findVirtual(String.class,"replace", mt); String handled_str = (String) mh.invoke("abc",'a','c'); System.out.print(handled_str); } catch (NoSuchMethodException | IllegalAccessException e) { e.printStackTrace(); } } }
以上 5 種場景中的行爲稱爲對一個類進行主動引用。除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用。被動引用的常見例子包括:
System.out.println(SubClass.value); // value 字段在 SuperClass 中定義
SuperClass[] sca = new SuperClass[10];
System.out.println(ConstClass.HELLOWORLD);
package com.cnblogs.mufasa.demo3_2; class SuperClass{ static int value=10; static final int NUM=20; static { System.out.println("父類加載初始化-完成"); } } class SubClass extends SuperClass{ // static int value=11; // static final int NUM=22; static { System.out.println("子類加載初始化-完成"); } } public class Client { public static void main(String[] args) { //1,經過子類引用父類的靜態字段,不會致使子類初始化.【驗證成功】-【static變量分配方法區中的靜態池中,而且賦值】 // System.out.println(SubClass.value); //2,經過數組定義來引用類,不會觸發此類的初始化。【驗證成功】-【只是分配了這種類型的數組空間,並無真正開始使用】 // 該過程會對數組類進行初始化,數組類是一個由虛擬機自動生成的、直接繼承自 Object 的子類,其中包含了數組的屬性和方法 // SuperClass[] sca = new SuperClass[10]; //3,常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化。 //【驗證成功】-靜態常量數據在類加載的準備階段就直接在方法區中分配空間並初始化了,static的修飾遍歷只是先分配空間 // System.out.println(SuperClass.NUM); } }
兩個類相等,須要類自己相等,而且使用同一個類加載器進行加載。這是由於每個類加載器都擁有一個獨立的類名稱空間。兩個條件:①自己;②同一個類加載器。
這裏的相等,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果爲 true,也包括使用 instanceof 關鍵字作對象所屬關係斷定結果爲 true。
從 Java 虛擬機的角度來說,只存在如下兩種不一樣的類加載器:
啓動類加載器(Bootstrap ClassLoader),使用 C++ 實現,是虛擬機自身的一部分;
全部其它類的加載器,使用 Java 實現,獨立於虛擬機,繼承自抽象類 java.lang.ClassLoader。
從 Java 開發人員的角度看,類加載器能夠劃分得更細緻一些:
啓動類加載器(Bootstrap ClassLoader)此類加載器負責將存放在 <JRE_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,而且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即便放在 lib 目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器沒法被 Java 程序直接引用,用戶在編寫自定義類加載器時,若是須要把加載請求委派給啓動類加載器,直接使用 null 代替便可。
擴展類加載器(Extension ClassLoader)這個類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統變量所指定路徑中的全部類庫加載到內存中,開發者能夠直接使用擴展類加載器。
應用程序類加載器(Application ClassLoader)這個類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。因爲這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以通常稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。
應用程序是由三種類加載器互相配合從而實現類加載,除此以外還能夠加入本身定義的類加載器。
下圖展現了類加載器之間的層次關係,稱爲雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啓動類加載器外,其它的類加載器都要有本身的父類加載器。這裏的父子關係通常經過組合關係(Composition)來實現,而不是繼承關係(Inheritance)。
一個類加載器首先將類加載請求轉發到父類加載器,只有當父類加載器沒法完成時才嘗試本身加載。
使得 Java 類隨着它的類加載器一塊兒具備一種帶有優先級的層次關係,從而使得基礎類獲得統一。
例如 java.lang.Object 存放在 rt.jar 中,若是編寫另一個 java.lang.Object 並放到 ClassPath 中,程序能夠編譯經過。因爲雙親委派模型的存在,因此在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優先級更高,這是由於 rt.jar 中的 Object 使用的是啓動類加載器,而 ClassPath 中的 Object 使用的是應用程序類加載器。rt.jar 中的 Object 優先級更高,那麼程序中全部的 Object 都是這個 Object。
如下是抽象類 java.lang.ClassLoader 的代碼片斷,其中的 loadClass() 方法運行過程以下:①先檢查類是否已經加載過,②若是沒有則讓父類加載器去加載。③當父類加載器加載失敗時拋出 ClassNotFoundException,此時嘗試本身去加載。
FileSystemClassLoader 是自定義類加載器,繼承自 java.lang.ClassLoader,用於加載文件系統上的類。它首先根據類的全名在文件系統上查找類的字節代碼文件(.class 文件),而後讀取該文件內容,最後經過 defineClass() 方法來把這些字節代碼轉換成 java.lang.Class 類的實例。
java.lang.ClassLoader 的 loadClass() 實現了雙親委派模型的邏輯,自定義類加載器通常不去重寫它,可是須要重寫 findClass() 方法。
【本質:讀取.class字節流--經過defineClass--類的實例】
package com.cnblogs.mufasa.demo7; import java.io.*; class FileSystemClassLoader extends ClassLoader{ private String rootDir; public FileSystemClassLoader(String rootDir){ this.rootDir=rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException{ byte[] classData= new byte[0]; try { classData = getClassData(name); } catch (IOException e) { e.printStackTrace(); } if(classData==null){ throw new ClassNotFoundException(); }else { return defineClass(name,classData,0,classData.length); } } private byte[] getClassData(String className) throws IOException { String path=classNameToPath(className); System.out.println(path); try{ InputStream ins=new FileInputStream(path); ByteArrayOutputStream baos=new ByteArrayOutputStream(); int bufferSize=4096; byte[] buffer=new byte[bufferSize]; int bytesNumRead; while ((bytesNumRead=ins.read(buffer))!=-1){ baos.write(buffer,0,bytesNumRead); } return baos.toByteArray(); }catch (IOException e){ e.printStackTrace(); } return null; } private String classNameToPath(String className){ // System.out.println(rootDir+File.separatorChar+className.replace('.',File.separatorChar)+".class"); return rootDir+File.separatorChar+className.replace('.',File.separatorChar)+".class"; } } public class Client { public static void main(String[] args) throws ClassNotFoundException { FileSystemClassLoader fsc=new FileSystemClassLoader("E:\\data\\personal\\博客園\\2019.09.05Java類加載機制\\src\\com\\cnblogs\\mufasa\\test"); Class preClass=fsc.findClass("Test_classLoader"); System.out.println(preClass.getClass().getName()); } }
經過new或者反射獲取新的實例化對象的過程【】,【注意:原型模式生成的實例化對象,是已有實例化對象的clone結果,並無經過類的加載來實現】
方法區用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
和堆同樣不須要連續的內存,而且能夠動態擴展,動態擴展失敗同樣會拋出 OutOfMemoryError 異常。
對這塊區域進行垃圾回收的主要目標是對常量池的回收和對類的卸載,可是通常比較難實現。
HotSpot 虛擬機把它當成永久代來進行垃圾回收。但很難肯定永久代的大小,由於它受到不少因素影響,而且每次 Full GC 以後永久代的大小都會改變,因此常常會拋出 OutOfMemoryError 異常。爲了更容易管理方法區,從 JDK 1.8 開始,移除永久代,並把方法區移至元空間,它位於本地內存中,而不是虛擬機內存中。
方法區是一個 JVM 規範,永久代與元空間都是其一種實現方式。在 JDK 1.8 以後,原來永久代的數據被分到了堆和元空間中。元空間存儲類的元信息,靜態變量和常量池等放入堆中。
9.2
由Java虛擬機自帶的類加載器所加載的類,在虛擬機的生命週期中,始終不會被卸載。
前面介紹過,Java虛擬機自帶的類加載器包括啓動類加載器、擴展類加載器和應用程序類加載器。
Java虛擬機自己會始終引用這些類加載器,而這些類加載器則會始終引用它們所加載的類的Class對象,所以這些Class對象始終是可觸及的。
由用戶自定義的類加載器加載的類是能夠被卸載的。
loader1變量和obj變量間接應用表明Sample類的Class對象,而objClass變量則直接引用它。
若是程序運行過程當中,將上圖左側三個引用變量都置爲null,此時Sample對象結束生命週期,MyClassLoader對象結束生命週期,表明Sample類的Class對象也結束生命週期,Sample類在方法區內的二進制數據被卸載。
當再次有須要時,會檢查Sample類的Class對象是否存在,若是存在會直接使用,再也不從新加載;若是不存在Sample類會被從新加載,在Java虛擬機的堆區會生成一個新的表明Sample類的Class實例(能夠經過哈希碼查看是不是同一個實例)。
(1) 啓動類加載器加載的類型在整個運行期間是不可能被卸載的(jvm和jls規範);
(2) 被系統類加載器和標準擴展類加載器加載的類型在運行期間不太可能被卸載,由於系統類加載器實例或者標準擴展類的實例基本上在整個運行期間總能直接或者間接的訪問的到,其達到unreachable的可能性極小。(固然,在虛擬機快退出的時候能夠,由於無論ClassLoader實例或者Class(java.lang.Class)實例也都是在堆中存在,一樣遵循垃圾收集的規則);
(3) 被開發者自定義的類加載器實例加載的類型只有在很簡單的上下文環境中才能被卸載,並且通常還要藉助於強制調用虛擬機的垃圾收集功能才能夠作到.能夠預想,稍微複雜點的應用場景中(尤爲不少時候,用戶在開發自定義類加載器實例的時候採用緩存的策略以提升系統性能),被加載的類型在運行期間也是幾乎不太可能被卸載的(至少卸載的時間是不肯定的)
綜合以上三點, 一個已經加載的類型被卸載的概率很小至少被卸載的時間是不肯定的。同時咱們能夠看的出來,開發者在開發代碼時候,不該該對虛擬機的類型卸載作任何假設的前提下來實現系統中的特定功能。