JVM總括四-類加載過程、雙親委派模型、對象實例化過程
目錄:JVM總括:目錄
一、 類加載過程
類加載過程就是將.class文件轉化為Class對象,類實例化的過程,(User user = new User(); 這個過程是對象實例化的過程);
一個.class文件只有一個Class對象(字節碼對象),可以有無數個對象(例如:new User(););

1、Load:
將編譯后的.class文件以二進制流的方式加載到JVM內存中,並轉化為特定的數據結構,用到的就是classLoad二類加載器。這個過程中校驗cafe babe魔法數、常量池、文件長度、是否有父類等。
其實加載階段用一句話來描述就是:把代碼數據加載到內存中。
2、Link:
分為驗證、准備、解析三步。
驗證:更為詳細的驗證,比如:final是否規范(二次賦值不規范)、static是否合理(靜態方法必須引用靜態變量)、類型是否正確。
准備:當完成字節碼文件的校驗之后,JVM 便會開始為類變量分配內存並初始化。這里需要注意兩個關鍵點,即內存分配的對象以及初始化的類型。
內存分配的對象。Java 中的變量有「類變量」和「類成員變量」兩種類型,「類變量」指的是被 static 修飾的變量,而其他所有類型的變量都屬於「類成員變量」。
在准備階段,JVM 只會為「類變量」分配內存,而不會為「類成員變量」分配內存。「類成員變量」的內存分配需要等到初始化階段才開始。
比如下面的代碼在准備階段,只會為 age 屬性分配內存,而不會為 website 屬性分配內存。
public static int age= 3;
public String website = "www.beijingdesigner.com";
例如下面的代碼在准備階段之后,age的值將是 0,而不是 3。
public static int age= 3;
但如果一個變量是常量(被 static final 修飾)的話,那么在准備階段,屬性便會被賦予用戶希望的值。例如下面的代碼在准備階段之后,number 的值將是 3,而不是 0。
public static final int number = 3;
這其中的道理我們想下就明白了。
兩個語句的區別是一個有 final 關鍵字修飾,另外一個沒有。被final修飾的常量,是不允許再被改變的。所以初始化就是3了。
解析:把類中的符號引用轉化為直接引用,完成內存結構布局。(符號引用轉化為直接引用:例如:test1() { test2(); },這里test1調用test2方法就是符號引用,但實際test2()通過一個指針指向test2方法的內存地址,這個指針負責調用,它就是直接引用)。
3、Init:
執行類構造器<clinit>,遞歸初始化父類的靜態代碼塊,不執行構造函數。(先執靜態變量,再執行行靜態代碼塊)。
任何小寫的class定義的類都有一個魔法屬性:class,
int j; Class<Integer> integerClass = int.class; Class<Integer> integerClass1 = Integer.class;
二、類加載的原則:雙親委派模型
類加載是怎么確定類文件的位置呢?

1、Bootstrap:最高級的類加載器,裝置最核心的類,如:Object、System、String;
2、Extension ClassLoader:JDK9之前的類加載器,以后的為Platform ClassLoader,加載系統的擴展類;
3、Application ClassLoader:應用類加載器,主要加載用戶自定義CLASSPATH路徑下的類。
類加載器並非繼承關系,只是以組合的方式服用父加載器功能,符合優先原則。
其中第二、第三層為java實現,用戶可以自定義類加載器,第三層為C++實現,所有為null:
ClassLoader classLoader = Son.class.getClassLoader(); ClassLoader parent = classLoader.getParent(); ClassLoader parent1 = parent.getParent(); System.out.println(classLoader); System.out.println(parent); System.out.println(parent1);
----------------------------------- sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@5acf9800 null
獲取Bootstrap加載的類庫:
URLClassPath bootstrapClassPath = Launcher.getBootstrapClassPath(); URL[] urLs = bootstrapClassPath.getURLs(); for (URL url : urLs){ System.out.println(url.toExternalForm()); }
--------------------------------------------------------- file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/resources.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/rt.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/sunrsasign.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/jsse.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/jce.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/charsets.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/jfr.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/classes
Bootstrap加載的路徑可以追加,在JVM增加啟動參數,不過不建議修改:
Xbootclasspath/D:/test/src
自定義類加載器的情況:
1、隔離加載類:框架中吧類加載到不同的環境中
2、修改類加載方式:除了Bootstrap,其他類並非一定要加入。
3、擴展加載源:比如加載數據庫;
4、防止源碼泄露:可以對類進行加密,那加載的時候需要自定義加載器對類進行解密加載。
自定義ClassLoder,繼承ClassLoader,重寫findClass(),調用defineClass():
public class UserClassLoader extends ClassLoader { private String classpath; public UserClassLoader(String classpath) { this.classpath = classpath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte [] classDate=getDate(name); if(classDate==null){ throw new FileNotFoundException(); } else{ //defineClass方法將字節碼轉化為類 return defineClass(name,classDate,0,classDate.length); } } catch (IOException e) { e.printStackTrace(); } return super.findClass(name); } //返回類的字節碼 private byte[] getDate(String className) throws IOException{ InputStream in = null; ByteArrayOutputStream out = null; String path=classpath + File.separatorChar +className.replace('.',File.separatorChar)+".class"; try { in=new FileInputStream(path); out=new ByteArrayOutputStream(); byte[] buffer=new byte[2048]; int len=0; while((len=in.read(buffer))!=-1){ out.write(buffer,0,len); } return out.toByteArray(); } catch (FileNotFoundException e) { e.printStackTrace(); } finally{ in.close(); out.close(); } return null; } }
調用,其中D:\\wp目錄下為.class文件,里面有個Son.class
//自定義類加載器的加載路徑 UserClassLoader myClassLoader=new UserClassLoader("D:\\wp"); //包名+類名 Class sonClass=myClassLoader.loadClass("com.Son"); if(sonClass!=null){ Object obj=sonClass.newInstance(); System.out.println(sonClass.getClassLoader().toString()); }
三、對象實例化
從字節碼、執行步驟兩個方面分析
1、字節碼方面:
Object object = new Object();,通過javap -verbose -p查看對象創建的字節碼指令:

(1)new:如果找不到Class對象就進行類加載,然后分配內存(本類路徑上所有的屬性都分配),其中對象的引用也是個變量也占內存(4個字節),這個指令執行完畢會把對象的壓入虛擬機棧頂。
(2)dup:在棧頂復雜引用,如果<init>有參數,把參數壓入操作棧,兩個引用,壓入棧底的用來賦值或保存到局部變量表中,棧頂引用作為句柄調用相關方法。
(3)invokespecial:調用對象實例化方法,通過棧頂方法調用<init>方法(也就是調用構造方法)。
2、執行步驟
(1)確認類元信息是否存在:接到new指令時,在metaspace檢查類元信息是否存在,沒有就在雙親委派模式下進行類加載,生成Class對象。
(2)分配對象內存:首先計算對象占用空間大小(成員變量是引用變量就分配4個字節大小的變量空間),在堆中划分內存空間給新對象(分配空間需要進行同步操作,如:cas)。
(3)設定默認值:成員變量設置不同形式的0值;
(4)設置對象頭:設置對象的哈希碼、鎖信息、對象所屬的類元信息,設置取決於JVM。
(5)執行init方法:初始化成員變量,執行代碼塊,調用類的構造方法,把堆對象首地址賦值給引用變量。
四、思考:ClassLoader.loadClasshe和Class.forName區別
1 //1 2 Class<Son> sonClass = Son.class; 3 //2 4 Class<?> aClass = Maint.class.getClassLoader().loadClass("com.Son"); 5 //3 6 Class<?> bClass = Class.forName("com.Son");
代碼2:
其實這種方法調運的是:ClassLoader.loadClass(name, false)方法
參數一:name,需要加載的類的名稱
參數二:false,這個類加載以后是否需要去連接(不需要linking)
代碼:3:
其實這種方法調運的是:Class.forName(className, true, ClassLoader.getCallerClassLoader())方法
參數一:className,需要加載的類的名稱。
參數二:true,是否對class進行初始化(需要initialize)
參數三:classLoader,對應的類加載器
其中1、2都是將.class文件加載到JVM中,得到Class對象,但是還沒有連接(Link),靜態代碼塊不會執行;
代碼3得到Class對象並且已經完成初始化<clinit>,靜態代碼塊會執行。( Class.forName("com.mysql.jdbc.Driver");//通過這種方式將驅動注冊到驅動管理器上)。
1、2、3都不會執行對象的構造函數。
五、思考:<clinit>和<init>區別
<clinit>是類(Class)初始化執行的方法,<init>是對象初始化執行的方法(構造函數);
