java 靜態變數生命週期(類生命週期)
引言
最近有位細心的朋友在閱讀筆者的文章時,對java類的生命週期問題有一些疑惑,筆者開啟百度搜了一下相關的問題,看到網上的資料很少有把這個問題講明白的,主要是因為目前國內java方面的教材大多隻是告訴你“怎樣做”,但至於“為什麼這樣做”卻不多說,所以造成大家在基礎和原理方面的知識比較匱乏,所以筆者今天就斗膽來講一下這個問題,權當拋磚引玉,希望對在這個問題上有疑惑的朋友有所幫助,文中有說的不對的地方,也希望各路高手前來指正。
首先來了解一下jvm(java虛擬機器)中的幾個比較重要的記憶體區域,這幾個區域在java類的生命週期中扮演著比較重要的角色:
- 方法區:
- 常量池:常量池是方法區的一部分,主要用來存放常量和類中的符號引用等資訊。
- 堆區:用於存放類的物件例項。
- 棧區:也叫java虛擬機器棧,是由一個一個的棧幀組成的後進先出的棧式結構,棧楨中存放方法執行時產生的區域性變數、方法出口等資訊。當呼叫一個方法時,虛擬機器棧中就會建立一個棧幀存放這些資料,當方法呼叫完成時,棧幀消失,如果方法中呼叫了其他方法,則繼續在棧頂建立新的棧楨。
除了以上四個記憶體區域之外,jvm中的執行時記憶體區域還包括本地方法棧和程式計數器,這兩個區域與java類的生命週期關係不是很大,在這裡就不說了,感興趣的朋友可以自己百度一下。
類的生命週期
當我們編寫一個java的原始檔後,經過編譯會生成一個字尾名為class的檔案,這種檔案叫做位元組碼檔案,只有這種位元組碼檔案才能夠在java虛擬機器中執行,java類的生命週期就是指一個class檔案從載入到解除安裝的全過程。
一個java類的完整的生命週期會經歷載入、連線、初始化、使用、和解除安裝五個階段,當然也有在載入或者連線之後沒有被初始化就直接被使用的情況,如圖所示:
下面我們就依次來說一說這五個階段。
載入
在java中,我們經常會接觸到一個詞——類載入,它和這裡的載入並不是一回事,通常我們說類載入指的是類的生命週期中載入、連線、初始化三個階段。在載入階段,java虛擬機器會做什麼工作呢?其實很簡單,就是找到需要載入的類並把類的資訊載入到jvm的方法區中,然後在堆區中例項化一個java.lang.Class物件,作為方法區中這個類的資訊的入口。
類的載入方式比較靈活,我們最常用的載入方式有兩種,一種是根據類的全路徑名找到相應的class檔案,然後從class檔案中讀取檔案內容;另一種是從jar檔案中讀取。另外,還有下面幾種方式也比較常用:
- 從網路中獲取:比如10年前十分流行的Applet。
- 根據一定的規則實時生成,比如設計模式中的動態代理模式,就是根據相應的類自動生成它的代理類。
- 從非class檔案中獲取,其實這與直接從class檔案中獲取的方式本質上是一樣的,這些非class檔案在jvm中執行之前會被轉換為可被jvm所識別的位元組碼檔案。
對於載入的時機,各個虛擬機器的做法並不一樣,但是有一個原則,就是當jvm“預期”到一個類將要被使用時,就會在使用它之前對這個類進行載入。比如說,在一段程式碼中出現了一個類的名字,jvm在執行這段程式碼之前並不能確定這個類是否會被使用到,於是,有些jvm會在執行前就載入這個類,而有些則在真正需要用的時候才會去載入它,這取決於具體的jvm實現。我們常用的hotspot虛擬機器是採用的後者,就是說當真正用到一個類的時候才對它進行載入。
載入階段是類的生命週期中的第一個階段,載入階段之後,是連線階段。有一點需要注意,就是有時連線階段並不會等載入階段完全完成之後才開始,而是交叉進行,可能一個類只加載了一部分之後,連線階段就已經開始了。但是這兩個階段總的開始時間和完成時間總是固定的:載入階段總是在連線階段之前開始,連線階段總是在載入階段完成之後完成。
連線
連線階段比較複雜,一般會跟載入階段和初始化階段交叉進行,這個階段的主要任務就是做一些載入後的驗證工作以及一些初始化前的準備工作,可以細分為三個步驟:驗證、準備和解析。
- 驗證:當一個類被載入之後,必須要驗證一下這個類是否合法,比如這個類是不是符合位元組碼的格式、變數與方法是不是有重複、資料型別是不是有效、繼承與實現是否合乎標準等等。總之,這個階段的目的就是保證載入的類是能夠被jvm所執行。
- 準備:準備階段的工作就是為類的靜態變數分配記憶體並設為jvm預設的初值,對於非靜態的變數,則不會為它們分配記憶體。有一點需要注意,這時候,靜態變數的初值為jvm預設的初值,而不是我們在程式中設定的初值。jvm預設的初值是這樣的: 解析:這一階段的任務就是把常量池中的符號引用轉換為直接引用。那麼什麼是符號引用,什麼又是直接引用呢?我們來舉個例子:我們要找一個人,我們現有的資訊是這個人的身份證號是1234567890。只有這個資訊我們顯然找不到這個人,但是通過公安局的身份系統,我們輸入1234567890這個號之後,就會得到它的全部資訊:比如安徽省黃山市餘暇村18號張三,通過這個資訊我們就能找到這個人了。這裡,123456790就好比是一個符號引用,而安徽省黃山市餘暇村18號張三就是直接引用。在記憶體中也是一樣,比如我們要在記憶體中找一個類裡面的一個叫做show的方法,顯然是找不到。但是在解析階段,jvm就會把show這個名字轉換為指向方法區的的一塊記憶體地址,比如c17164,通過c17164就可以找到show這個方法具體分配在記憶體的哪一個區域了。這裡show就是符號引用,而c17164就是直接引用。在解析階段,jvm會將所有的類或介面名、欄位名、方法名轉換為具體的記憶體地址。
- 基本型別(int、long、short、char、byte、boolean、float、double)的預設值為0。
- 引用型別的預設值為null。
- 常量的預設值為我們程式中設定的值,比如我們在程式中定義final static int a = 100,則準備階段中a的初值就是100。
連線階段完成之後會根據使用的情況(直接引用還是被動引用)來選擇是否對類進行初始化。
初始化
如果一個類被直接引用,就會觸發類的初始化。在java中,直接引用的情況有:
- 通過new關鍵字例項化物件、讀取或設定類的靜態變數、呼叫類的靜態方法。
- 通過反射方式執行以上三種行為。
- 初始化子類的時候,會觸發父類的初始化。
- 作為程式入口直接執行時(也就是直接呼叫main方法)。
除了以上四種情況,其他使用類的方式叫做被動引用,而被動引用不會觸發類的初始化。請看主動引用的示例程式碼:
- import java.lang.reflect.Field;
- import java.lang.reflect.Method;
- class InitClass{
- static {
- System.out.println("初始化InitClass");
- }
- public static String a = null;
- public static void method(){}
- }
- class SubInitClass extends InitClass{}
- public class Test1 {
- /**
- * 主動引用引起類的初始化的第四種情況就是執行Test1的main方法時
- * 導致Test1初始化,這一點很好理解,就不特別演示了。
- * 本程式碼演示了前三種情況,以下程式碼都會引起InitClass的初始化,
- * 但由於初始化只會進行一次,執行時請將註解去掉,依次執行檢視結果。
- * @param args
- * @throws Exception
- */
- public static void main(String[] args) throws Exception{
- // 主動引用引起類的初始化一: new物件、讀取或設定類的靜態變數、呼叫類的靜態方法。
- // new InitClass();
- // InitClass.a = "";
- // String a = InitClass.a;
- // InitClass.method();
- // 主動引用引起類的初始化二:通過反射例項化物件、讀取或設定類的靜態變數、呼叫類的靜態方法。
- // Class cls = InitClass.class;
- // cls.newInstance();
- // Field f = cls.getDeclaredField("a");
- // f.get(null);
- // f.set(null, "s");
- // Method md = cls.getDeclaredMethod("method");
- // md.invoke(null, null);
- // 主動引用引起類的初始化三:例項化子類,引起父類初始化。
- // new SubInitClass();
- }
- }
上面的程式演示了主動引用觸發類的初始化的四種情況。
類的初始化過程是這樣的:按照順序自上而下執行類中的變數賦值語句和靜態語句,如果有父類,則首先按照順序執行父類中的變數賦值語句和靜態語句。先看一個例子,首先建兩個類用來顯示賦值操作:
- public class Field1{
- public Field1(){
- System.out.println("Field1構造方法");
- }
- }
- public class Field2{
- public Field2(){
- System.out.println("Field2構造方法");
- }
- }
下面是演示初始化順序的程式碼:
- class InitClass2{
- static{
- System.out.println("執行父類靜態程式碼");
- }
- public static Field1 f1 = new Field1();
- public static Field1 f2;
- }
- class SubInitClass2 extends InitClass2{
- static{
- System.out.println("執行子類靜態程式碼");
- }
- public static Field2 f2 = new Field2();
- }
- public class Test2 {
- public static void main(String[] args) throws ClassNotFoundException{
- new SubInitClass2();
- }
- }
上面的程式碼中,初始化的順序是:第03行,第05行,第11行,第13行。第04行是宣告操作,沒有賦值,所以不會被執行。而下面的程式碼:
- class InitClass2{
- public static Field1 f1 = new Field1();
- public static Field1 f2;
- static{
- System.out.println("執行父類靜態程式碼");
- }
- }
- class SubInitClass2 extends InitClass2{
- public static Field2 f2 = new Field2();
- static{
- System.out.println("執行子類靜態程式碼");
- }
- }
- public class Test2 {
- public static void main(String[] args) throws ClassNotFoundException{
- new SubInitClass2();
- }
- }
初始化順序為:第02行、第05行、第10行、第12行,各位可以執行程式檢視結果。
在類的初始化階段,只會初始化與類相關的靜態賦值語句和靜態語句,也就是有static關鍵字修飾的資訊,而沒有static修飾的賦值語句和執行語句在例項化物件的時候才會執行。
使用
類的使用包括主動引用和被動引用,主動引用在初始化的章節中已經說過了,下面我們主要來說一下被動引用:
- 引用父類的靜態欄位,只會引起父類的初始化,而不會引起子類的初始化。
- 定義類陣列,不會引起類的初始化。
- 引用類的常量,不會引起類的初始化。
被動引用的示例程式碼:
- class InitClass{
- static {
- System.out.println("初始化InitClass");
- }
- public static String a = null;
- public final static String b = "b";
- public static void method(){}
- }
- class SubInitClass extends InitClass{
- static {
- System.out.println("初始化SubInitClass");
- }
- }
- public class Test4 {
- public static void main(String[] args) throws Exception{
- // String a = SubInitClass.a;// 引用父類的靜態欄位,只會引起父類初始化,而不會引起子類的初始化
- // String b = InitClass.b;// 使用類的常量不會引起類的初始化
- SubInitClass[] sc = new SubInitClass[10];// 定義類陣列不會引起類的初始化
- }
- }
最後總結一下使用階段:使用階段包括主動引用和被動引用,主動飲用會引起類的初始化,而被動引用不會引起類的初始化。
當使用階段完成之後,java類就進入了解除安裝階段。
解除安裝
關於類的解除安裝,筆者在單例模式討論篇:單例模式與垃圾回收一文中有過描述,在類使用完之後,如果滿足下面的情況,類就會被解除安裝:
- 該類所有的例項都已經被回收,也就是java堆中不存在該類的任何例項。
- 載入該類的ClassLoader已經被回收。
- 該類對應的java.lang.Class物件沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
如果以上三個條件全部滿足,jvm就會在方法區垃圾回收的時候對類進行解除安裝,類的解除安裝過程其實就是在方法區中清空類資訊,java類的整個生命週期就結束了。
總結
做java的朋友對於物件的生命週期可能都比較熟悉,物件基本上都是在jvm的堆區中建立,在建立物件之前,會觸發類載入(載入、連線、初始化),當類初始化完成後,根據類資訊在堆區中例項化類物件,初始化非靜態變數、非靜態程式碼以及預設構造方法,當物件使用完之後會在合適的時候被jvm垃圾收集器回收。讀完本文後我們知道,物件的生命週期只是類的生命週期中使用階段的主動引用的一種情況(即例項化類物件)。而類的整個生命週期則要比物件的生命週期長的多。
相關推薦
java 靜態變數生命週期(類生命週期)
引言 最近有位細心的朋友在閱讀筆者的文章時,對java類的生命週期問題有一些疑惑,筆者開啟百度搜了一下相關的問題,看到網上的資料很少有把這個問題講明白的,主要是因為目前國內java方面的教材大多隻是告訴你“怎樣做”,但至於“為什麼這樣做”卻不多說,所以造成大家在基礎和原理方面的知識比較匱乏,
JAVA學習筆記整理六(類集框架)
public class SetTest { public static void main(String[] args) { // hashSet Set hashSet = new HashSet(); hashSet.add("A"); hashSet.add("A"
java 靜態變量生命周期(類生命周期)
targe ref 鍵值對 靜態代碼塊 經歷 per tails jvm的內存 代碼 轉載自:http://www.cnblogs.com/hf-cherish/p/4970267.html 侵刪 Static: 加載:java虛擬機在加載類的過程中為靜態變量分配
Java 靜態變數生命週期
Static: 載入:java虛擬機器在載入類的過程中為靜態變數分配記憶體。類變數:static變數在記憶體中只有一個,存放在方法區,屬於類變數,被所有例項所共享銷燬:類被解除安裝時,靜態變數被銷燬,並釋放記憶體空間。static變數的生命週期取決於類的生命週期類初始化順序: 靜態變數、靜態程式碼塊初始化
知識儲備:Spring中Bean的生命週期(基於註解版)
一:前言 在Spring專案中,通常配置Spring都是使用XML的形式進行配置,配置bean是通過<bean></bean>標籤將bean加入IOC容器中,但在Spring註解版中,可以通過Java程式碼進行配置,即建立一個java類在其類頭上標註@Configurat
從原始碼角度看Spring生命週期(官方最全)
Spring在beanfactory中給出了spring的生命週期的list列表 一、bean初始化前的處理 Bean factory implementations should support the standard bean lifecycle interfaces as
java: 封裝快取池(int與Integer)、常量池(拘留池)、static變數 static程式碼塊 static方法、 final變數、final 方法、final類 整理
java 記憶體模型: JVM主要管理兩種型別記憶體:堆和非堆,堆記憶體(Heap Memory)是在 Java 虛擬機器啟動時建立, 非堆記憶體(Non-heap Memory)是在 JVM堆之
java 測試開發基礎知識(類加載,JVM等)
常量表達式 對數 前端 .cn 都是 新的 ron 技術分享 區域 寫在開頭: 面試的時候別人很可能會問你的java原理,.class load 原理, jvm機制,這些都是Java的底層知識,特整理如下: 1. 首先,編寫一個java程序,大家會用ide編寫一個例
Java練習 SDUT-3339_計算長方形的周長和麵積(類和物件)
計算長方形的周長和麵積(類和物件) Time Limit: 1000 ms Memory Limit: 65536 KiB Problem Description 設計一個長方形類Rect,計算長方形的周長與面積。 成員變數:整型、私有的資料成員length(長)、width(寬); 構造方法如下: (
java基礎複習(類和物件)
建構函式(構造器) 1、this() super()都必須是建構函式裡的第一句宣告 若同時出現,那麼原則是: 引數少的構造器用this呼叫引數多的,在引數最多的建構函式裡呼叫 super 靜態變數、靜態方法、常量 static: 被所有的例項共享
Java靜態變數初始化及建構函式的執行順序與執行時機分析
對於Java初學者來說,關於靜態變數、非靜態變數的初始化時機與順序,以及建構函式的執行時機與順序都會感覺有點理不清頭緒,下面文章使用例項程式幫大家解決這方面的疑惑。雖然簡單,但是對Java入門者來說還是有一定的幫助作用。
java學習(類與物件) 第二更 建立汽車物件 並錄入汽車資訊
最近學習了java的類與物件。 java面向物件程式設計的有三大特徵: ①封裝性 所謂封裝,也就是把客觀事物封裝成抽象的類,並且類可以把自己的資料和方法只讓可信的類或者 物件操作,對不可信的進行資訊隱藏。簡而言之就是,內部操作對外部而言不可見(保護性) ②繼承性 繼承是指它可以使用現有類的所
Java之計算長方形的周長和麵積(類和物件)
Problem Description 設計一個長方形類Rect,計算長方形的周長與面積。 成員變數:整型、私有的資料成員length(長)、width(寬); 構造方法如下: (1)Rect(int length) —— 1個整數表示正方形的邊長 (2)Rect(
Java基礎-變數,判斷,類
基本資料型別 1.整數型別: byte(1 位元組 ), short(2 位元組 ), int(4 位元組 ), long(8 位元組 ) 1位元組=8位,而每一個數的第一位為符號位,並且-0(負零)用-128表示,所以byte的範圍為:-2^(位元組8-1)
java學習(類與物件) 第二更 建立汽車物件 並錄入汽車資訊
最近學習了java的類與物件。 java面向物件程式設計的有三大特徵: ①封裝性 所謂封裝,也就是把客觀事物封裝成抽象的類,並且類可以把自己的資料和方法只讓可信的類或者 物件操作,對不可信的進行資訊隱藏。簡而言之就是,內部操作對外部而言不可見(保護性) ②繼承性
2018年11月13日Java學習之關鍵字static(類成員和類方法),單例設計,類的成員之:初始化塊
1.類變數(類屬性)由該類的所有例項共享 static 修飾的變數就是類變數,可以直接不建立物件訪問靜態成員,所有例項可以共同修改這個值 2.類方法 static修飾的方法可以用類名.方法名()訪問 在static方法內部只能訪問類的static屬性,不能訪問
Java 靜態變數、方法等的總結
1. 靜態變數和方法從屬於類,不需要通過物件呼叫,可以直接通過類呼叫 public class Test{ public static void main(String[] args){ System.out.println(Student.name); System.out.
譯:Java區域性變數型別推斷(Var型別)的26條細則
原文連結:https://dzone.com/articles/var-work-in-progress 作者:Anghel Leonard 譯者:沈歌 Java區域性變數型別推斷(LVTI),簡稱var型別(識別符號var不是一個關鍵字,是一個預留型別名),Java 10中通過JEP 286: L
java靜態變數,靜態程式碼塊,普通程式碼塊,建構函式載入順序
前言: java成員變數和構造方法等載入順序是一個很基礎又很容易搞混的東西,今天寫下來算是更明確一些 正文: 廢話不多說上程式碼 public class Father { public Father(){ System.out.print
JAVA靜態變數的使用
很多的地方建議把一個一個不變的變數使用static表明,然後所有的變數字母都是用大寫來表示。這樣的優點 是在讀程式碼的時候我們知道這個字母代表什麼, 當這個字母變化的時候,我們只需要修改一個地方就可以修改所有地方了public static String GEND