Java是一門面向對象的編程語言。java
面向對象以抽象爲基礎,有封裝、繼承、多態三大特性。程序員
宇宙萬物,通過抽象,都可納入相應的種類。不一樣種類之間,有着相對井然的分別。編程
Java中的類,即是基於現實世界中的類別抽象出來的。api
類自己表示一類事物,是對這類事物共性的抽象與封裝。類封裝了一類事物的屬性和方法。數組
類與類之間,有着不一樣的層級。安全
以生物界中的分類爲例,遵循「界門綱目科屬種」的級別體系,人類(亦可稱爲「人種」)的層級體系是:動物界---脊索動物門---哺乳綱---靈長目---人科---人屬---人種。數據結構
從人種到動物界,依次繼承父類的共有屬性和方法,並且又獨具形態。eclipse
舉例來講,動物都須要吃東西來維持生命所需的能量,同是吃東西,不一樣種類的動物各有特色。編程語言
又譬如,動物界與植物界的一個關鍵區別是,可否移動。在動物界之中,都是移動,可是各子類的移動方式幾乎互不相同。ide
舉例來講,人經過走路、奔跑、攀爬等來移動,鳥經過飛翔、兩下肢等來移動,魚則經過在水中漂游來移動等。這使得動物的移動功能豐富多彩。
不只如此,即使屬於同一種類的個體,在表現出來的公有功能方面,也是各不相同。
譬如,雖然同爲人類,廣泛具有說話的功能,可是每一個具體的我的在說話時,音色又各自不一樣。
咱們生活的世界,就是這樣豐富多彩。既有共性的東西,又有具體不一樣的風格。
Java語言源於爲解決現實世界中各類各樣應用問題提供一整套解決方案。
因此,咱們生活的現實世界,乃至整個宇宙,深深地映射入Java語言中。
世界與宇宙何其深邃與複雜,一樣,Java的博大精深不言而喻。
能夠說,每一個Java程序的運行,都是爲了解決某個或某種應用問題而生。
古人說「格物致知」,咱們探祕Java程序運行的內在原理,有助於幫助咱們深刻認識Java世界的運行機制。
每一個Java程序,都離不開類和對象。
因此,咱們就從類加載提及。
1、類的生命週期
想象一下,你在Eclipse裏寫了一個Java程序,經過javac(Java編譯器),將Java源代碼編譯爲.class字節碼文件。
字節碼文件靜靜地躺在你的電腦磁盤裏,你要運行這個Java程序,就要去運行編譯後的字節碼文件。
加載.class字節碼文件到內存,造成供JVM使用的類,併到這個類從內存中銷燬,這即是類的生命週期。
總的來講,類的生命週期通過了如圖所示的階段:
1.加載
關於加載,其實,就是根據.class文件找到類的信息將其加載到方法區中,而後在堆區中實例化一個java.lang.Class對象,做爲方法區中這個類信息的入口。
須要簡單科普一下的是:Java程序運行起來時成爲進程,操做系統須要爲該進程分配內存空間。Java程序的進程會將所分得的內存空間再予以分區,主要有棧區(存儲局部變量)、堆區(存儲建立的對象)、方法區(存儲類的方法代碼,以及類的靜態成員變量信息,還有常量池)、程序計數器(記錄線程的執行信息)、本地方法棧(與 操做系統底層交互時使用)。如圖所示:
2.連接
有的出處稱爲「鏈接」,若從英文單詞「linking」判斷,則翻譯爲「連接」比較合適。
連接通常會與加載階段和初始化階段交叉進行。
連接的過程由三部分組成:驗證、準備和解析。
(1)驗證:該階段是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
(2)準備:主要是爲由static修飾的成員變量分配內存空間,並設置默認的初始值。默認初始值以下:
①8種基本數據類型的默認初始值是0。
②引用類型默認的初始值是null。
③對於有static final修飾的常量會直接賦值,例如:static final int x=10;則x默認就是10。
(3)解析:就是把常量池中的符號引用轉換爲直接引用,也就是說,JVM會將全部的類或接口名、字段名、方法名轉換爲具體的內存地址。
3.初始化
這是將靜態成員變量(也稱爲「類變量」)賦值的過程。
也就是說,只有static修飾的變量才能被初始化,執行的順序是:
父類靜態域(靜態成員變量)或者靜態代碼塊,而後是子類靜態域或者子類靜態代碼塊。
並不是全部的類都會被初始化,只有那些被直接引用(主動引用)的類纔會被初始化。在Java中,類被直接引用的狀況有:
①經過new關鍵字實例化對象、讀取或設置類的靜態變量、調用類的靜態方法;
②經過反射方式執行以上三種行爲;
③初始化子類的時候,會觸發父類的初始化;
④做爲程序入口直接運行時(也就是直接調用main方法);
除了以上4種狀況,其餘使用類的方式叫作被動引用,被動引用不會觸發類的初始化。
被動引用舉例:
(1)子類調用父類的靜態變量,子類不會被初始化,只有父類被初始化。對於靜態字段,只有直接定義這個字段的類纔會被初始化。
(2)經過數組定義來引用類,不會觸發類的初始化。
(3)訪問類的常亮,不會初始化類。
4.使用
類在使用過程當中也存在三步:對象實例化、垃圾收集、對象終結。
(1)對象實例化:就是執行類中構造函數的內容,若是該類存在父類,JVM會經過顯式或者隱式的方式先執行父類的構造函數,在堆內存中爲父類的實例變量開闢空間,並賦予默認的初始值;而後,引用變量獲取對象的首地址,經過操做對象來調用實例變量和方法。
(2)垃圾收集:當對象再也不被引用的時候,就會被JVM虛擬機標上特別的垃圾標識,在堆區中等待被GC回收。
(3)對象的終結:對象被GC回收後,對象就再也不存在了,對象的生命也就走到了盡頭。
5.卸載
這是類的生命週期中最後的一步。
程序中再也不有該類的引用,該類會被JVM執行垃圾回收,類在本次程序運行中的生命結束。
2、雙親委派
Java中的類加載存在層次性,一個重要的加載模型是雙親委派。
先來看Java中類加載器的層次體系:
什麼是類加載器呢?
簡而言之,類加載器能夠將.class字節碼
文件加載到JVM
內存中的方法區造成類模板(或者稱爲該類的數據結構/鏡像),並在堆區中產生Class
對象。
若是站在JVM
的角度來看,只存在兩種類加載器:
1.啓動類加載器(Bootstrap ClassLoader
):
也稱爲「根加載器」。由C++
語言實現(針對HotSpot
),負責將存放在<JAVA_HOME>\lib
目錄或-Xbootclasspath
參數指定的路徑中的類庫加載到內存中。
2.其餘類加載器:
由Java語言實現,繼承自抽象類ClassLoader。如:
(1)擴展類加載器(Extension ClassLoader):負責加載<JAVA_HOME>\lib\ext目錄或java.ext.dirs系統變量指定的路徑中的全部類庫。
(2)應用程序類加載器(Application ClassLoader)。負責加載用戶類路徑(classpath)上的指定類庫,咱們能夠直接使用這個類加載器。通常狀況下,若是咱們沒有自定義類加載器,默認就是用這個加載器。經過在控制檯打印(System.out.println(System.getProperty("java.class.path"));),能夠看到應用程序類加載器加載的路徑信息。如圖所示:
C:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar; C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar; E:\workspace\eclipse\work_j2ee\java1_8\bin
雙親委派模型的工做過程是:
若是一個類加載器收到類加載的請求,它會先判斷這個類是否已經加載過,若已經加載過,就再也不重複加載;若還未加載過,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器完成,若該類加載器無父類加載器,則將加載請求委派給根類加載器。每一個類加載器都是如此(根類加載器除外)。只有當父類加載器在本身的搜索範圍內找不到指定的類時(即ClassNotFoundException
),子類加載器纔會嘗試本身去加載。
Java在類加載中採用雙親委派模型有什麼好處呢?
使得Java類同其類加載器一塊兒具有了一種帶優先級的層次關係,從而保證了程序運行中類的惟一性。
咱們知道,程序運行起來時,每一個類在堆內存中的Class對象僅有惟一的一個,不會引發程序運行中類的混亂,其根源在於Java類加載中採用的雙親委派模型。
3、自定義類加載器
有的時候,咱們須要當前程序之外的class文件,這時,咱們就須要自定義類加載器,對相應的class文件進行加載。
自定義類加載器的步驟是:
1.繼承ClassLoader
2.重寫findClass()方法
3.調用defineClass()方法
接下來自定義一個類加載器,加載E:/test下的Test2.class文件。
Test2.class文件的源代碼文件Test2.java:
package bwie2; public class Test2 { public void say() { System.out.println("Hello China"); } }
接着,建立自定義類加載器:
package bwie; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class MyCloassLoader2 extends ClassLoader { private String classPath;// 要加載的類路徑 public MyCloassLoader2(String classPath) {// 構造方法傳參 this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException {// 查找類 byte[] classData = getData(name); if (classData == null) { //若字節碼爲空,則拋出異常 throw new ClassNotFoundException(); } else { // defineClass,將字節碼轉化爲類 return defineClass(name, classData, 0, classData.length); } //return super.findClass(name); } // 返回類的字節碼 private byte[] getData(String className) { 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[1024]; int len = 0; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } in.close(); out.close(); return out.toByteArray(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } }
而後,經過測試類進行測試:
package bwie; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class Test { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { //自定義類加載器的加載路徑 MyCloassLoader2 classLoader = new MyCloassLoader2("E:/test"); //包名+類名 Class<?> clazz = classLoader.loadClass("bwie2.Test2"); if(clazz!=null) { Object obj = clazz.newInstance(); Method method = clazz.getMethod("say"); method.invoke(obj); System.out.println(clazz.getClassLoader().toString()); } } }
程序執行後,控制檯打印如圖所示:
可見,筆者使用自定義的類加載器MyCloassLoader2成功地加載了程序之外的class文件。
4、深刻講解反射
反射是Java語言中一個很是重要的機制。
程序員們通常都知道:經過反射,能夠獲取類與對象的全部信息,執行若干操做(如建立對象,方法調用),還能夠修改類的數據結構(如修改訪問權限)。
在Java中,反射對應的單詞是reflect。
提到反射,難免讓人霎時想起光的反射(Reflection of light)。
Java裏運用反射,是否與光的反射有關?這也涉及Java爲何要取名爲反射。
舉個例子來講,一個美女站在鏡子前,請問,鏡子裏的美女和鏡子前的美女,是否同一個美女?
答案是確定的。
咱們再來看Java程序的加載與運行。
一個被編譯爲.class字節碼文件的類,通過JVM的加載,在方法區中造成對應的類模板。
那麼請問,JVM加載出的類模板,與加載前的類,是否是同一個類?
答案是確定的。
你們想一下:一我的站在鏡子前,經過光的反射,能夠在鏡子裏產生一個鏡像。鏡像與鏡子前的人是同一我的。這是運用了光的反射規則。
實際上,咱們能看到五彩繽紛的世界,一個重要緣由是光的反射的存在。
光的反射外在表現爲一種現象,本質是一種機制和規則。
一樣,一個表現爲.class字節碼文件的類,通過JVM中的類加載器加載,在方法區中造成類模板,也至關於類的「鏡像」。
你們再想下:Java中,加載前、表現爲.class字節碼文件的類,與加載後、在方法區中造成的類模板,同屬於一個類,這與光的反射是否是有殊途同歸之妙?
這也就是Java爲何將類加載後、在內存的方法區中造成類模板的機制,稱爲反射的原因。
看來,Java語言的締造者不愧是大牛,將技術比喻得那麼貼切,又那麼接近生活!
你們還會看到,上圖中,堆區裏有個Class對象,類加載時會在堆區中產生Class對象。
程序加載運行時,一個類在內存中的Class對象與類模板都是惟一的。
程序中經過Class對象操做類模板。
能夠說,程序中要運用反射,就離不開Class對象。那麼,Class對象到底是什麼?
若是咱們把JVM看做是人的話,對於程序員來講,經過閱讀Java源代碼,可以瞭解一個類的數據結構,那麼,Java程序在運行中,JVM又是如何讀懂類的數據結構的呢?
這要歸功於類加載器加載class文件在方法區生成該類的模板。若是說,class文件靜態地存儲了類信息,類加載器加載出來的類模板至關於類在動態運行環境中的數據結構,JVM就是經過這個類模板來認識與操做這個類的。
編程語言實現了人機交互。Java語言也是如此。
咱們要操控JVM虛擬機去操做內存中的某個類,應該怎麼辦呢?Java語言爲全部Java數據類型(基本數據類型與引用數據類型)均提供了class屬性,經過該屬性能夠返回Class對象,這個Class對象是咱們在程序中運用反射機制,是咱們與JVM交互、指揮JVM去操做類模板的接口性工具。
機器懂的,咱們未必懂。怎麼辦呢?找個中間人,經過中間人操做機器。這就比如,咱們經過操做系統去操做電腦硬件那樣。
咱們經過Class對象,指揮JVM操做程序動態運行中的類模板。
5、對象的生命週期
在Java中,對象的生命週期包括如下幾個階段:
1. 建立階段(Created) 2. 應用階段(In Use) 3. 不可見階段(Invisible) 4. 不可達階段(Unreachable) 5. 收集階段(Collected) 6. 終結階段(Finalized) 7. 對象空間重分配階段(De-allocated)
如圖所示:
1.建立階段(Created)
在建立階段系統經過下面的幾個步驟來完成對象的建立過程:
l 爲對象分配存儲空間
l 開始構造對象
l 從超類到子類對static成員進行初始化
l 超類成員變量按順序初始化,遞歸調用超類的構造方法
l 子類成員變量按順序初始化,子類構造方法調用
一旦對象被建立,並被分派給某些變量賦值,這個對象的狀態就切換到了應用階段。
2.應用階段(In Use)
對象至少被一個強引用持有着。
3.不可見階段(Invisible)
當一個對象處於不可見階段時,說明程序自己再也不持有該對象的任何強引用,雖然這些引用仍然是存在着的。
簡單來講,就是程序的執行已經超出了該對象的做用域了。
好比,在使用某個局部變量count時,已經超出該局部變量的做用域(不可見),那麼就稱該變量count處於不可見階段。這種狀況下,編譯期在編譯階段一般就會提示與報錯。
4.不可達階段(Unreachable)
對象處於不可達階段是指該對象再也不被任何強引用所持有。
與「不可見階段」相比,「不可達階段」是指程序再也不持有該對象的任何強引用,這種狀況下,該對象仍可能被JVM等系統下的某些已裝載的靜態變量或線程或JNI等強引用持有着,這些特殊的強引用被稱爲」GC root」。這些GC root可能會致使對象的內存泄露,使得對象沒法被回收。
5.可收集階段、終結階段與釋放階段
這是對象生命週期的最後一個階段:可收集階段、終結階段與釋放階段。
當對象處於這個階段的時候,可能處於下面三種狀況:(1)垃圾回收器發現該對象已經不可到達,則對象進入「可收集階段」。(2)finalize方法已經被執行,則對象空間等待被垃圾回收器進行回收,即「終結階段」。(3)對象空間已被重用,即「對象空間從新分配階段」。當對象處於上面的三種狀況時,該對象就處於可收集階段、終結階段與釋放階段了。JVM虛擬機就能夠直接將該對象回收了。