前兩周看到了Java編程思想的初始化及類的加載那里,一直沒找時間把它總結(jié)出來,對于初始化和類的加載過程,感覺Java編程思想講的較淺還不夠深入,于是我結(jié)合Java瘋狂講義2和人家博客后,就打算按照自己的理解來把它梳理一下。
之前我一直對Java有個疑問,那就是一個Java類文件在首次被使用時,需要經(jīng)過什么步驟。這里我參考了書籍我畫個JVM對首次使用類的處理步驟流程圖:
首先我先從類的加載入手吧,Java語言其他的語言,采用了一種不同的加載方式。每個類的編譯代碼都存在于它自己的獨立文件中,該文件只在需要使用程序代碼時才會被加載。那么什么情況下類的字節(jié)碼會被加載呢?這里我畫了個思維導圖:
類的加載是將類的class文件讀入內(nèi)存中,并為之創(chuàng)建一個java.lang.Class對象。這過程需要借助類的加載器來完成。天啊,對于Java接觸不長久來說的人,什么叫類的加載器啊,幸好谷歌和百度老人懂,于是我上網(wǎng)查了一下看三個多小時,終于弄明白了是怎么一回事啦。
Java的類加載器是負責加載所有的類,系統(tǒng)為所有被載入內(nèi)存中的類生成一個java.lang.Class實例。一旦一個類被載入JVM中,同一個類就不會再次載入(通常是由一個類其全限定類名和其類加載器作為其唯一標識),在Java中類加載器有三類,這里我來畫個表圖來描述這三類。
基于這方面我做了一些驗證,先從根類加載器開始,以下是一些代碼驗證:
代碼:
輸出結(jié)果:
file:/D:/java/lib/resources.jar
file:/D:/java/lib/rt.jar
file:/D:/java/lib/sunrsasign.jar
file:/D:/java/lib/jsse.jar
file:/D:/java/lib/jce.jar
file:/D:/java/lib/charsets.jar
file:/D:/java/lib/jfr.jar
file:/D:/java/classes
這里為什么會出現(xiàn)編譯錯誤呢?這類加載器是由JVM本身去實現(xiàn),開發(fā)者是無法獲得該引用的,所以會出現(xiàn)編譯錯誤。從輸出結(jié)果來看,它確實加載lib的幾個庫類,這里我不太明白,就是我去安裝包查看文件時,發(fā)現(xiàn)文件沒有classes,和sunsasign.jar這兩個文件,這里我目前還不知道它們兩個是怎么來的,希望閱覽者知情的話,麻煩留言告知一下,本人將會非常感謝。
現(xiàn)在我來看看擴展類的情況,以下是代碼驗證:
- public class ClassLoaderTest
- {
- public static void main(String[] args)
- {
- //獲得系統(tǒng)類加載器
- ClassLoader systemLoader=ClassLoader.getSystemClassLoader();
- //獲得擴展類加載器
- ClassLoader extensionLoader=systemLoader.getParent();
- //輸出擴展加載器指定的加載路徑
- System.out.println("擴展類加載器路徑:"+"\n"+System.getProperty("java.ext.dirs"));
- //獲得擴展類加載器的加載的庫類
- URL[] urls=((URLClassLoader)extensionLoader).getURLs();
- System.out.println("擴展類加載器加載的庫類");
- for(int i=0;i<urls.length;i++)
- {
- System.out.println(urls[i]);
- }
- }
- }
輸出結(jié)果:
擴展類加載器路徑:
D:\java\lib\ext;C:\windows\Sun\Java\lib\ext
擴展類加載器加載的庫類
file:/D:/java/lib/ext/access-bridge-64.jar
file:/D:/java/lib/ext/dnsns.jar
file:/D:/java/lib/ext/jaccess.jar
file:/D:/java/lib/ext/localedata.jar
file:/D:/java/lib/ext/sunec.jar
file:/D:/java/lib/ext/sunjce_provider.jar
file:/D:/java/lib/ext/sunmscapi.jar
file:/D:/java/lib/ext/zipfs.jar
從這輸出結(jié)果也看出擴展類確實加載java.ext.dirs下的庫類,這里就跟我查看該目錄的文件jar包就一一對應了。
現(xiàn)在我驗證一下最后一種系統(tǒng)類的加載器,目錄工程路徑為D:/AndroidProject/test:
代碼:
- public class ClassLoaderTest
- {
- public static void main(String[] args)
- {
- //獲得系統(tǒng)類加載器
- ClassLoader systemLoader=ClassLoader.getSystemClassLoader();
- //輸出系統(tǒng)類加載器classpath環(huán)境變量指定的加載路徑
- System.out.println("系統(tǒng)類加載器路徑:"+"\n"+System.getProperty("java.class.path"));
- //獲得系統(tǒng)類加載器的加載的庫類
- URL[] urls=((URLClassLoader)systemLoader).getURLs();
- System.out.println("系統(tǒng)類加載器加載的庫類");
- for(int i=0;i<urls.length;i++)
- {
- System.out.println(urls[i]);
- }
- }
- }
輸出結(jié)果:
系統(tǒng)類加載器路徑:
D:\AndroidProject\test\bin
系統(tǒng)類加載器加載的庫類
file:/D:/AndroidProject/test/bin/
這些就是這三個類的加載器了所負責加載的路徑測試了,現(xiàn)在我想串聯(lián)起自己剛開始初學Java時,感到的奇怪的地方。
疑問一:我們?yōu)槭裁纯梢杂靡恍㏒tring,System,Object類呢?是誰幫我們把這些類載入內(nèi)存的。
疑問二:我們會為什么可以不用設(shè)置classpath環(huán)境變量來指定系統(tǒng)類加載器所加載的類呢?
解答一:由上面知道,根類加載器是由JVM本身實現(xiàn),是主要由C++寫的一個加載,從它負責的加載路徑可以看出,它是負責加載核心庫的一個伴隨JVM啟動的加載器,而那些String,System,Object等類剛好位于這些核心庫類中,再結(jié)合JAVA語言是采用需要使用該類時才會加載該類,所以當我們在程序使用那些Java庫類時,是根類加載器幫我們加載到內(nèi)存,而不是其他的加載器。
解答二:剛剛學Java時,通常會按照網(wǎng)上設(shè)置path和classpath環(huán)境變量的,但是那是我發(fā)現(xiàn)不設(shè)置classpath也沒什么影響啊,那誰幫我們找到了指定要加載類的路徑呢?從上面可以看出是系統(tǒng)類加載器負責加載我們的寫的那些類,對已classpath的描述這里甲骨文公司有兩段描述:The
class path is the path that the Java runtime environment searches for classes and other resource files. The class search path (more commonly known by the shorter name, "class path") can be set using either the -classpath option
when calling a JDK tool (the preferred method) or by setting the CLASSPATH environment variable. The -classpath option
is preferred because you can set it individually for each application without affecting other applications and without other applications modifying its value.The default class path is the current directory. Setting the CLASSPATH variable
or using the -classpath command-line option overrides that default, so if you want to include the current directory in the search path, you must include "."
in the new settings.這里我借助了谷歌翻譯,自己就再把翻譯改正一下,大概意思:類路徑是讓Java運行環(huán)境來尋找類和其它資源文件的路徑。類搜索路徑(普遍稱為"class
path"的短名)可以用JDK tool(首選方法) -classpath操作或者classpath環(huán)境變量來設(shè)置其值,這個-classpath操作是較好的方式,因為你可以單獨為每個應用程序設(shè)置而不會影響其他的應用且不會讓其他的應用可以修改它,默認類搜索的路徑是當前路徑,可以設(shè)置classpath環(huán)境變量或使用-classpath命令符可以改變類搜索路徑,但如果你要在當前路徑搜索加載類的話,你就需要把當前路徑加到classpath環(huán)境變量或-classpath命令中(即
".")。如果classpath沒設(shè)置,當前路徑會作為類搜索路徑(即系統(tǒng)類加載器會加載當前的路徑的類),如果classpath設(shè)置的話,類搜素路徑即為classpath設(shè)置的路徑,(即即系統(tǒng)類加載器會加載classpath設(shè)置路徑下的類)
這三個加載器之間存在繼承關(guān)系,如果我們自己需要開發(fā)自定義的加載器,通常需要繼承ClassLoader子類。這里我畫個圖來描述它們之間的關(guān)系:
剛開始看到三個圖,我感到很奇怪,擴展類和系統(tǒng)類不是說繼承根類加載器的怎么,怎么看到上面兩個UML繼承圖沒有出現(xiàn)根類加載器呢?這里我上網(wǎng)查后才得之其中的原因,這里我就不描述什么,閱覽者可以點擊下面鏈接來了解一下:
加載器之間的關(guān)系的詳解
大多人知道類加載機制有三種:
1全盤負責。就是由一個類加載器負責加載某個class時,該class所依賴的和引用的其他class也將由該類加載器負責載入。
2父類委托。所謂父類委托,則是讓父類加載器試圖加載該class,只有在父加載器無法加載該類時才嘗試從自己的類路徑加載該類。
3緩存機制。緩存機制將會保證所有加載過的class都會被緩存,當程序中需要使用某個class時,類加載器先從緩存區(qū)中搜尋該class,只有當緩存區(qū)中不存在該class對象時,系統(tǒng)才會讀取該類對應的二進制數(shù)據(jù)。
JVM默認采取的加載機制是父類委托機制,我參考書籍寫的一個通過父類委托機制實現(xiàn)類加載器加載class步驟來畫個流程圖:
現(xiàn)在我寫一些代碼驗證一下:
現(xiàn)在我在D:\AndroidProject\test\src\test目錄下建立一個標準JavaBean類,其代碼:
- package test;
-
- public class JavaBean
- {
- private String name;
- private int id;
- public JavaBean()
- {
- super();
- }
- public JavaBean(String name, int id)
- {
- super();
- this.name = name;
- this.id = id;
- }
- public String getName()
- {
- return name;
- }
- public void setName(String name)
- {
- this.name = name;
- }
- public int getId()
- {
- return id;
- }
- public void setId(int id)
- {
- this.id = id;
- }
-
- }
現(xiàn)在我在相同路徑下再寫一個ClassLoaderTest類來加載這個類,代碼:
- public class ClassLoaderTest
- {
- public static void main(String[] args)
- {
- try
- {
- ////調(diào)用加載當前類的類加載器(這里即為系統(tǒng)類加載器)加載JavaBean
- Class test=Class.forName("test.JavaBean");
- //查看被加載的JavaBean類型是被那個類加載器加載的
- System.out.println(test.getClassLoader());
- } catch (ClassNotFoundException e)
- {
- System.out.println("異常");
- e.printStackTrace();
- }
- }
- }
輸出結(jié)果:
sun.misc.Launcher$AppClassLoader@605df3c5
很明顯JavaBean類是由系統(tǒng)類加載器加載,在當前目錄下還存在JavaBean class文件前提下,我再把JavaBean class打成jar包放到擴展類加載器所加載的路徑,會發(fā)生什么情況呢?
再次運行ClassLoaderTest類,其輸出結(jié)果:
sun.misc.Launcher$ExtClassLoader@25082661
如果我把當前路徑中 JavaBean class和擴張類加載器類加載路徑下的JavaBean jar包刪掉同時,再根類加載器加載的路徑下放一個JavaBean jar包會發(fā)生什么情況:
再次運行ClassLoaderTest類,輸出結(jié)果:
異常
java.lang.ClassNotFoundException: test.JavaBean
at java.net.URLClassLoader$1.run(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Unknown Source)
at test.ClassLoaderTest.main(ClassLoaderTest.java:16)
拋出了異常,有人可能就疑問,根據(jù)上面類加載器加載class的流程圖,不是可以由根類加載器加載的嗎?為什么拋出了異常呢?我用了谷歌查了一大推外國資料,都沒找原因,只看從上面那個鏈接是這樣的描述:
虛擬機出于安全等因素考慮,不會加載<
Java_Runtime_Home >/lib存在的陌生類,開發(fā)者通過將要加載的非JDK自身的類放置到此目錄下期待啟動類加載器加載是不可能的。
由于本人接觸Java時間不長,就不知道該原因是不是這樣了。但是總這些測試來看,當一個類調(diào)用的加載器默認是系統(tǒng)類加載器,如果父加載器負責加載的路徑下有該類的class 文件,就有父加載器去加載,否則有當前加載器去當前路徑或classpath指定路徑加載該類,可以看出Java默認的加載機制確實通過委托機制去加載一個類的。
那么JVM加載了一個類后,是不是馬上初始化呢?不是,還有經(jīng)過類的連接這一步,連接總共有三個步驟,我畫思維導圖來描述:
從圖的可以看出,一個類的運行時的異常通常是在類的連接的驗證階段拋出的。
現(xiàn)在我來總結(jié)最后一個類的初始化過程了,類初始化過程會涉及到靜態(tài)字段,靜態(tài)代碼快,普通代碼塊,普通變量,常量,構(gòu)造器這一系列,動作,我記得我剛開始學Java時,常常被它們初始順序給弄混淆了,在這里我結(jié)合書籍,總結(jié)出這些初始化的順序,這里我再畫個順序流程圖來描述:
現(xiàn)在我根據(jù)這個我寫一些代碼驗證:
代碼: - public class ExampleParent
- {
- //靜態(tài)代碼塊
- static
- {
- System.out.println("ExampleParent的靜態(tài)代碼塊");
- }
- //普通代碼塊
- {
- System.out.println("ExampleParent的普通代碼塊");
- }
- //構(gòu)造器
- public ExampleParent()
- {
- System.out.println("ExampleParent的構(gòu)造器");
- }
- }
- public class Example extends ExampleParent
- {
- //成員變量
- ExampleParent exampleParent3=new ExampleParent();
- //靜態(tài)的字段
- static ExampleParent exampleParent1=new ExampleParent();
- //靜態(tài)的代碼塊
- static
- {
- System.out.println("Example的靜態(tài)代碼塊");
- }
- //普通代碼塊
- {
- System.out.println("Example的普通代碼塊");
- }
- private String name;
- private int id;
- //構(gòu)造器
- public Example(String name, int id)
- {
- super();
- this.name = name;
- this.id = id;
- System.out.println("name是"+name+" id是"+id);
- }
- }
- public class ExampleTest extends Example
- {
- public ExampleTest(String name, int id)
- {
- super(name, id);
- }
- public ExampleTest()
- {
- this("beyondboy",88);
- }
- //普通代碼塊
- {
- System.out.println("ExampleTest的普通代碼塊");
- }
- //靜態(tài)代碼塊
- static
- {
- System.out.println("ExampleTest的靜態(tài)代碼塊");
- }
- //靜態(tài)字段
- static Example example=new Example("beyondboy", 10);
- public static void main(String[] args)
- {
- System.out.println("ExampleTest的靜態(tài)main方法");
- new ExampleTest();
- new ExampleTest();
- }
- }
輸出結(jié)果:
ExampleParent的靜態(tài)代碼塊
ExampleParent的普通代碼塊
ExampleParent的構(gòu)造器
Example的靜態(tài)代碼塊
ExampleTest的靜態(tài)代碼塊
ExampleParent的普通代碼塊
ExampleParent的構(gòu)造器
ExampleParent的普通代碼塊
ExampleParent的構(gòu)造器
Example的普通代碼塊
name是beyondboy id是10
ExampleTest的靜態(tài)main方法
ExampleParent的普通代碼塊
ExampleParent的構(gòu)造器
ExampleParent的普通代碼塊
ExampleParent的構(gòu)造器
Example的普通代碼塊
name是beyondboy id是88
ExampleTest的普通代碼塊
ExampleParent的普通代碼塊
ExampleParent的構(gòu)造器
ExampleParent的普通代碼塊
ExampleParent的構(gòu)造器
Example的普通代碼塊
name是beyondboy id是88
ExampleTest的普通代碼塊
因為要期末考試了,所以我的時間較緊,就沒有詳細分步去寫各種情況,我就一次性綜合了所有出現(xiàn)的情況,這里輸出結(jié)果比較長,可能對Java初學者來說看不太懂,如果看不懂的話,就自己百度一下,按照其上面流程圖去觀察,去猜別人寫的例子。對于Java的初始化整個過程我就暫時總結(jié)到這,以后等我下學期學了計算機算法分析與設(shè)計,我會嘗試再把它串聯(lián)到JVM GC垃圾回收過程,整篇文章我使用了整整一天的時間去寫,由于時間較緊,可能會有不少的錯誤,希望閱覽者能給予指正!
|