簡述:Java虛擬機爲Java程序提供運行時環境,其中一項重要的任務就是管理類和對象的生命週期。類的生命週期。類的生命週期從類被加載、鏈接和初始化開始,到類被卸載結束。當類處於生命週期中時,它的二級制數據位於方法區內,在堆區中還會有一個相應的描述這個類的Class對象(當Java程序使用任何一個類時,系統都會爲之建立一個java.lang.Class對象)。只有當類處於生命週期中時,Java程序才能使用它,好比調用類的靜態成員或者建立類的實例。java
1、Java虛擬機及程序的生命週期數據庫
當經過java命令運行一個Java程序時,就啓動了一個Java虛擬機進程。Java虛擬機進程從啓動到終止的過程,稱爲Java虛擬機的生命週期。在如下狀況中,Java虛擬機將結束生命週期。編程
01.程序正常執行結束;安全
02.程序在執行中由於出現異常或錯誤而異常終止;網絡
03.執行了System.exit()或者Runtime.getRuntime().exit();數據結構
04.因爲操做系用出現錯誤而致使Java虛擬機進程終止;dom
當Java虛擬機處於生命週期中時,它的總任務就是運行Java程序。Java程序從開始運行帶終止的過程稱爲程序的生命週期,它和Java虛擬機的生命週期的一致的。測試
當Java程序運行結束時,JVM進程結束,在進程在內存中的狀態將所有丟失。下面經過一個小例子來講明。spa
public class A { public static int a = 5; }
public class ATest1 { public static void main(String[] args) { A.a++; System.out.println(A.a);//6 } }
public class ATest2 { public static void main(String[] args) { System.out.println(A.a);//5 } }
觀察輸出結果,緣由就是ATest1和ATest2是兩次運行JVM進程,第一次運行結束後它對A類所作的修改將所有丟失;第二次運行JVM將再次初始化A類。指針
一些初學者會認爲A類中的a是靜態成員變量,同一個類中全部實例的靜態變量共享同一塊內存區,錯誤的認爲第二次運行會輸出被第一次改變後的結果。但實際上兩次運行Java程序處於兩個不一樣的JVM進程中,故兩個JVM之間不會共享數據。
2、類的加載、鏈接和初始化
當Java程序須要使用某個類時,Java虛擬機會確保這個類已經被加載、鏈接和初始化。其中鏈接過程又包括驗證、準備和解析這三個子步驟。這些步驟必須嚴格按照如下順序執行。
01.加載:查找並加載類的二進制數據;
加載具體是指把類的.class文件中的二進制數據讀入到內存中,把它存放在運行時數據區的方法區內,而後再堆區建立一個java.lang.Class對象,用來封裝類在方法區內的數據結構。
Java虛擬機可以經過不一樣的類加載器從不一樣來源加載類的二進制數據,包括如下幾種:
001.從本地文件系統中加載class文件,也是最多見的方式;
002.從JAR、ZIP或者其餘類型的歸檔文件中提取class文件(例如使用JDBC編程時用到的數據庫驅動類就是放在JAR文件中,JVM能夠從JAR文件中直接加載所需的class文件);
003.經過網絡加載class文件;
004.把一個Java源文件動態的編譯爲class文件,並執行加載;
類加載的最終產品是位於運行時數據區的堆區的Class對象。Class對象封裝了類在方法區內的數據結構,而且向Java程序提供了訪問類在方法區內的數據結構對的接口。
類的加載是由類加載器完成的,類加載器一般由JVM提供,這些類加載器也是全部程序運行的基礎,JVM所提供的這些類加載器一般稱之爲系統類加載器。除此以外,開發者能夠經過繼承ClassLoader基類來建立自定義的類加載器。
類加載器並不須要等到某個類被「首次主動使用」時再加載它,JVM規範容許類加載器在預料某個類將要被使用時就預先加載它。若是在預先加載過程當中遇到class文件缺失或者存在錯誤,類加載器必須等到首次使用該類時才報錯(拋出一個LinkageError錯誤),若是這個類一直沒有被程序主動使用,那麼該類加載器不會報錯。
02.鏈接:包括驗證、準備和解析類的二進制數據;
001.驗證:確保被加載類的正確性
當類被加載後,就進入驗證階段。鏈接就是把已經讀入到內存中的類的二進制數據合併到JVM運行時環境中去。鏈接的第一步是類的驗證,其目的是保證被加載的類由正確的內部結構,而且與其餘類協調一致。若是JVM檢查到錯誤,那麼就會拋出相應的Error對象。
疑問:由Java編譯器生成的Java類的二進制數據確定是正確的,爲何還要進行類的驗證呢?
由於JVM不知道某個特定的class文件究竟是如何建立的、從哪來的,這個class文件多是由正常的Java編譯器生成的,也有多是惡意建立的(經過惡意class文件破壞JVM運行時環境),類的驗證能提升程序的健壯性,確保程序被安全的執行。
類的驗證主要包括如下內容:
類文件的結構檢查:確保類文件聽從Java類文件的固定格式。
語義檢查:確保類自己符合Java語言的語法規定(例如final類型的類沒有子類,final類型的方法沒有被覆寫)。
字節碼驗證:確保字節碼流能夠被JVM安全的執行。字節碼流表明Java方法(包括靜態方法和實例方法),它是由被稱做操做碼的單元字節指令組成的序列,每個操做碼後都跟着一個或多個操做數。字節碼驗證步驟會檢查每一個操做碼是否合法,便是否有着合法的操做數。
二進制兼容的檢查:確保相互引用的類之間協調一致。例如在A類的的a方法中調用B類的b方法。JVM在驗證A類時,會檢查在方法區是否存在B類的b方法,若是不存在(當A類和B類的版本不兼容,就會出現這種問題),就會拋出NoSuchMethodError錯誤。
002.準備:爲類的靜態變量分配內存,並將其初始化爲默認值
在準備階段,JVM爲類的靜態變量分配內存,並設置默認的初始值。例如以下狀況:
public class Demo { public static int a = 1; public static long b; static { b = 2; } }
在準備階段,將爲int類型的靜態變量a分配4個字節的內存空間並賦予默認值爲0;爲long類型的靜態變量b分配8個字節的內存空間並賦予默認值0。
003.解析:把類中的符號引用轉換成直接引用
在解析階段,JVM會把二進制數據中的 符號引用替換爲直接引用。例如在A類的a方法中調用B
類的b方法。
public class A { B b = new B(); public void a() { b.b();//這行代碼在A類的二進制數據中表示爲符號引用 } }
在A類的二進制數據中,包含了一個對B類b()方法的符號引用,它由b()方法的全名和相關描述組成。在解析階段,JVM將這個符號引用替換成爲一個指針,該指針指向B類b()方法在方法區內的內存位置,這個指針就是直接引用。
03.初始化:給類的靜態變量賦予正確的初始值;
在初始化階段,JVM執行類初始化語句,爲類的靜態變量賦予初始值。在程序中,靜態變量的初始化有兩種途徑:一是在靜態變量的聲明處進行初始化;二是在靜態代碼塊中進行初始化。
以下代碼,a和b都被顯式的初始化,而c沒有沒顯式的初始化,它將報紙默認值0。
public class A { private static int a = 1;//在變量聲明處初始化 public static long b; public static long c; static { b = 1;//在靜態代碼塊中初始化 } }
在本文中,若是未加特別說明,類的靜態變量都是指不能做爲 編譯時常量的靜態變量。Java編譯器和虛擬機對 編譯時常量有特殊的處理方式,具體可參參考下文中 類的初始化時機。
靜態變量的聲明語句,以及靜態代碼塊都被看做類初始化語句,JVM會按照初始化語句在類文件中的的書寫順序依次執行它們。
Java虛擬機初始化一個類包含如下步驟:
1.假如這個類尚未被加載和鏈接,那麼先進行加載和鏈接。
2.假如類中存在直接父類,而且這個父類尚未被初始化,那麼就先初始化直接父類。
3.假如類中存在初始化語句,那麼就依次執行。
當初始化一個類的直接父類時,也須要重複以上步驟,這會確保當程序主動使用一個類時,這個類以及它的全部父類(包括直接父類和間接父類)都已經被初始化。程序中第一個被初始化的類是Object類。
在類或接口被加載的時機上,Java虛擬機規範給實現提供的必定的靈活性,可是又嚴格定義了初始化的時機,全部的Java虛擬機實現必須在每一個類或接口被Java程序「首次主動使用」時才初始化它們。Java程序對類的使用可分爲兩種:主動使用和被動使用,在下面的類的初始化時機進行詳細闡述。
3、類的初始化時機
JVM只有在程序首次主動使用一個類或接口時纔會初始化它。只有如下6種方式被看做程序對類或接口的主動使用。
01.建立類的實例。包括new關鍵字來建立,或者經過反射、克隆及反序列化方式來建立實例。
02.調用類的靜態方法。
03.訪問某個類或接口的靜態變量,或者對該靜態變量賦值。
04.使用反射機制來建立某個類或接口對應的java.lang.Class對象。例如Class.forName("Test")操做,若是系統還未初始化Test類,這波操做會致使該Test類被初始化,並返回Test類對應的java.lang.Class對象。
05.初始化一個類的子類,該子類全部的父類都會被初始化。
06.JVM啓動時被標明爲啓動類的類(直接使用java.exe命令運行某個主類)。例如對於「java Test」命令,Test類就是啓動類(主類),JVM會先初始化這個主類。
除了以上6種狀況,其餘方式都被看做成是 被動使用,不會致使類的初始化。下面經過接個例子來驗證:
public class A { public static final int a = 2*3;//a爲編譯時常量 public static final String str = "haha";//str爲編譯時常量 public static final int b = (int)(Math.random()*5);//b不是編譯時常量 static { System.out.println("init A"); } }
「宏變量」:
1.對於final類型的靜態變量,若是在編譯時就能計算出變量的取值,那麼這種變量看做 編譯時常量。Java程序中對類的編譯時常量的使用,被看做是對類的被動使用,不會致使類的初始化。
上面例子的由於編譯時能計算出a爲6,因此程序訪問A.a時,是對A類的被動使用,不會致使A類初始化。
public class Test { public static void main(String[] args) { System.out.println(A.a); } }
在Test測試類中運行程序:控制檯只打印出6,並無打印靜態代碼塊中的 init A。
當Java編譯器生成A類的class文件時,他不會在main()方法的字節碼流中保存一個表示「A.a」的符號引用,而是直接在字節碼流中嵌入常量值6。所以當程序訪問A.a時,客觀上無須初始化A類。(當JVM加載並鏈接A類時,不會在方法區內爲它的編譯時常量a分配內存。)
2.對於final類型的靜態變量,若是在編譯時不能計算出變量的取值,那麼程序對類的這種變量的使用,被看做是對類的主動使用,會致使類的初始化
public class Test { public static void main(String[] args) { System.out.println(A.b); } }
訪問A類中不是編譯時常量的b,控制檯會打印出 init A 4。
這波操做JVM會初始化A類,使得變量b在方法區內擁有特定的內存和初始值。
3.當JVM初始化一個類時,要求它的全部父類都已經初始化完畢,可是這條規則並不適用於接口。
01.在初始化一個類時,並不會先初始化它所實現的接口。
02.在初始化一個接口時,並不會先初始化它的父接口。
所以,一個父接口不會由於它的子接口或者實現類被初始化而初始化,只有當程序首次使用特定接口的靜態變量時,才致使該接口的初始化。
4.只有當程序訪問的靜態變量或靜態方法的確在當前類或接口中定義時,纔可看做是對類或接口的主動使用。觀察下面例子:
class Father{//父類 static int a = 1; static { System.out.println("init father"); } static void method() { System.out.println("father method"); } } class Son extends Father { static { System.out.println("init son"); } } public class Demo { public static void main(String[] args) { System.out.println(Son.a); //僅僅初始化父類Father Son.method(); } }
控制檯結果:
init father
1
father method
5.調用ClassLoader類的loadClass()方法加載一個類,該方法只是加載該類,並非對類的主動使用,不致使類的初始化。使用Class.forName()靜態方法纔會致使強制初始化該類。
package cn.lifecycle; class A { static { System.out.println("init A"); } } public class B { public static void main(String[] args) throws Exception { ClassLoader loader = ClassLoader.getSystemClassLoader();//獲取系統類加載器 Class objClass = loader.loadClass("cn.lifecycle.A");//加載A System.out.println("after load A"); System.out.println("before init A"); objClass = Class.forName("cn.lifecycle.A");//初始化A } }
控制檯結果:
after load A
before init A
init A
4、類加載器
類加載器簡介:
類加載器負責將class文件(可能在磁盤上,也可能在網絡上)加載到內存中,併爲之生成對應的java.lang.Class對象。
類加載器負責加載全部的類,系統爲全部被載入內存中的類生成一個java.lang.Class實例。一旦一個類被載入到JVM中,同一個類就不會再次被載入了。那麼怎麼樣纔算是」同一個類「?
正如一個對象有一個惟一的表示同樣,一個載入JVM的類也有一個惟一的標識。在Java中,一個類用其完整限定名做爲標識;但在JVM中,一個類用其完整限定名和其類加載器做爲惟一標識。例如,在yzx包下的Student類,被類加載器ClassLoader的實例k1負責加載,則該Student類對應的Class對象在JVM中表示爲(Student、yzx、k1)。這就意味着兩個類加載器加載的同名類:(Student、yzx、k1)和(Student、yzx、k2)是不一樣的,它們所加載的類也是徹底不一樣,互不兼容的。
JVM自帶了如下幾種加載器:
當JVM啓動時,會造成由三個類加載器組成的初始類加載器層次結構。
01.Bootstrap ClassLoader:根類加載器
02.Extension ClassLoader:擴展類加載器
03.System ClassLoader:系統類加載器
類加載器用來把類的class文件加載到Java虛擬機中(內存中)。從jdk1.2開始,類的加載過程採用父親委託機制,這種機制能更好地保證Java平臺的安全。在此委託機制中,除了JVM自帶的根加載器之外,其他的類加載器有且只有一個父加載器。當Java程序請求加載器loader1加載A類時,loader1首先委託本身的父加載器去加載A類,若父加載器能加載,則由父加載器完成加載任務,不然才由加載器loader1自己加載A類。
未完...