JVM虛擬機個人總結(三)

今天來講一下關於類型的生命週期。

首先之前提到過類型的裝載,連接與初始化。接下來詳細的介紹一下各個過程。

Java虛擬機通過裝載、連接和初始化一個Java類型,使該類型可以被正在運行的Java程序使用。其中,裝載就是把二進制形式的Java類型讀入Java虛擬機中;而連接就是把這種已經讀入虛擬機的二進制形式的類型數據合併到虛擬機的運行時狀態中去。連接狀態分爲三個自步驟——驗證,準備和解析。“驗證”步驟確保了Java類型數據格式正確並且適於Java虛擬機使用,“準備”步驟則負責爲該類型分配他所需要的內存,比如爲它的類變量分配內存。”解析“步驟則負責把常量池中的符號引用轉化成直接引用、虛擬機的實現可以推遲解析這一步,它可以在當運行中的程勳真正使用某個符號引用時再去解析它(把該符號引用裝換爲直接引用)。
步驟如下圖:
這裏寫圖片描述

裝載、連接和初始化必須按順序執行,唯一的例外就是連接階段的解析,它可以在初始化之後再進行。

類和接口在被裝載和連接的時機上沒有嚴格規定,但是所有虛擬機實現必須在每個類和接口首次主動使用時初始化,需要符合以下要求之一:
1.當創建某個類的新實例時(或者通過new,或者通過反射、克隆或反序列化)
2.當調用某個類的靜態方法時
3.當使用某個類或接口的靜態字段,或者對該字段賦值時,用final修飾的靜態字段除外,它被初始化一個編譯時的常量表達式
4.當調用Java API中的某些反射方法時,比如Class中的方法或者java.lang.reflect包中的類的方法。
5.當初始化某個類的子類時(某個類初始化時,要求它的超類已經被初始化了)
6.當虛擬機啓動時某個被標明爲啓動類的類(即含有main()方法的那個類)

注意,對於接口來說,任何一個類的初始化要求它的超類在此之前已經初始化了這個不成立。只有某個接口所聲明的非常量字段被使用時,接口才會被初始化。

下面是進行每個步驟的詳細介紹:

裝載:
裝載階段由三個基本動作組成:
1.通過該類型的完全限定名,產生一個代表該類型的二進制數據流
2.解析這個二進制數據流爲方法區內的內部數據結構
3.創建一個表示該類型的java.lang.Class類的實例

產生二進制數據的方式:
1.從本地文件系統裝載一個Java class文件
2.通過網絡下載一個Java class文件
3.從一個zip、jar、cab或者其他某種歸檔文件中提取Java class文件
4.從一個專有數據庫中提取Java class文件
5.把一個Java源文件動態編譯爲class文件
6.動態爲某個類型計算其class文件格式
7.使用上述任何方法,但是使用不同於java class文件的其他二進制文件格式

有了類型的二進制數據後,Java虛擬機必須對這些數據進行足夠的處理,才能創建java.lang.Class的實例對象,它成爲Java程序與內部數據結構之間的接口。
虛擬機可以預先裝載class文件,但是裝載失敗時不會馬上報告錯誤,只有等到程序首次主動使用該類時才報告錯誤。

驗證:
連接第一個步驟驗證是爲了確認類型符號Java語言的語義並且它不會危及虛擬機的完整性。
在正式驗證階段檢查:
1.檢查final的類不能擁有子類
2.檢查final的方法不能被覆蓋
3.確保在類型和超類型之間沒有不兼容的方法聲明(比如兩個方法擁有同樣的名字、參數在數量、順序、類型上都相同,但是返回類型不同)
注意,當這些檢查需要查看其他類型的時候,只需要查看超類型、超類型在子類初始化前全部初始化,當實現了父接口的類被初始化時,不需要初始化父接口,但是要裝載父接口
4.檢查所有的常量池入口相互一致
5.檢查常量池中的所有的特殊字符串(類名、字段名和方法名、字段描述和方法描述符)是否符合格式
6.檢查字節碼的完整性(在”連接“過程中一次性驗證字節碼流,而非在程序執行的時候動態驗證)

準備:
在準備階段,Java虛擬機爲類變量分配內存,設置默認初始值。在到達初始化之前,類變量沒有被初始化爲真正的初始值(在準備階段不會執行任何Java代碼的)。初始值的默認值:
這裏寫圖片描述

解析:
解析過程就是在類型的常量池中尋找類、接口、字段和方法的符號引用。在這些符號引用替換成直接引用,這個過程可延遲到初始化之後,正在調用時運行。

初始化:
這個初始化過程就是程序員想要的初始化過程,把程序員設定的初始值賦值過來。
初始化一個類包含兩個步驟:
1.如果類存在直接超類的話,且直接超類還沒有被初始化,就先初始化直接超類
2.如果類存在一個類初始化方法,就執行此方法

超類總是在子類之前被初始化的。初始化接口不需要初始化它的父接口,所以只需要一個步驟:
1.如果接口存在一個接口初始化的地方,就執行此方法

所有的類變量初始化語句和類型的靜態初始化器都被Java編譯器收集在一起,放到一個方法中,只能Java虛擬機內部調用。
在下面一個圖:
這裏寫圖片描述
這裏的執行順序是根據程序順序運行的。並非所有類都擁有一個方法,如果類沒有聲明任何類變量,也沒有靜態初始化語句,那麼就沒有方法。如果類聲明瞭類變量,但是沒有明確使用類變量初始化語句或者靜態初始化語句初始化它們,那麼也不會有。如果類僅包含靜態final變量的類變量初始化語句,而且這些類變量初始化語句採用編譯時常量表達式也不會有,比如:
這裏寫圖片描述
接口跟這個一個道理。

那麼主動使用和被動使用時什麼?

使用一個非常量的靜態字段只有當類或接口的確聲明瞭這個字段纔是主動使用。比如,類中聲明的字段可能會被子類引用,接口中聲明的字段可能會被子接口或者實現了這個接口的類引用。這時對於子類、子接口和實現了接口的類來說,這就是被動使用——不會觸發它們的初始化。只有當字段的確是被類或者接口聲明的時候纔是主動調用。例:
這裏寫圖片描述
這裏寫圖片描述
上面的例子中,NewbornBaby類沒有被初始化,也不需要被加載
這裏寫圖片描述
如果一個字段既是static又是final的,並且使用一個編譯時常量表達式初始化,使用這樣的字段不是主動使用,例如:
這裏寫圖片描述
這裏寫圖片描述
可以看到Angry和Dog沒有被初始化

講完了類的生命週期,那麼對象的生命週期又是什麼?

實例化一個類有四種途徑:
1.明確的使用了new操作符
2.調用Class或者java.lang.reflect.Constructor對象的newInstance()方法。
3.調用任何對象的clone方法
4.通過java.io.ObjectInputStream類的getObject方法反序列化

還有三種隱含的實例化。在類裝載的過程中,對於Java虛擬機裝載的每一個類型,他會暗中實例化一個Class對象來代表這個類型。其次,當Java虛擬機裝載了在常量池中包含CONSTANT_String_info入口的類的時候,他會創建新的String對象的實例來表示這些常量字符串,還有一條通過執行包含字符串連接操作符的表達式產生對象,如下圖:
這裏寫圖片描述
其中隱含創建了3個String對象和一個StringBuffer對象,其中兩個對象的引用傳遞到main()方法的args數組的一部分,另一個String對象代表arg[0]和arg[1]的連接,一個StringBuffer代表輸出的文字部分

創建一個新實例會發生什麼?
首先需要在堆中爲保存對象的實例對象分配內存,一旦虛擬機爲新的對象準備好了堆內存,它立即把實例變量的變量初始化爲默認的初始值(不是程序員設置的,跟類變量連接中的準備一樣)。一旦虛擬機完成了爲新對象分配內存和實例變量賦默認初始值之後,隨後就爲實例變量賦正確的初始值(程序員設置的)。另外兩種初始化方法:
1。如果對象是clone()調用來創建的,虛擬機把原來被克隆的實例變量中的值拷貝到新對象中。
2.如果對象是通過一個ObjectInputStream的readObject調用反序列發的。虛擬機通過從輸出流中讀入的值來初始化那些非暫時性的實例變量。否則虛擬機調用對象的實例初始化方法。
Java編譯器爲它編譯的每一個類都至少生成一個實例初始化方法。在class文件中稱爲,如果類沒有明確地聲明任何構造方法,編譯器默認產生一個無參數的構造方法,它僅僅調用超類的無參構造方法。
這裏寫圖片描述
這裏寫圖片描述

那麼垃圾收集和對象的終結又是怎樣的?
Java虛擬機必須實現具有自動堆存儲管理策略——大部分採用垃圾收集器。程序可以明確或隱含地爲對象分配內存,但是不能明確的釋放內存。finalize終結方法最多一個對象只調用一次。

卸載類型?
如果程序不在引用某類型,那麼這個類型就無法再對未來的計算過程產生影響。類型變成不可觸及的,就可以被垃圾收集。
使用啓動類裝載器裝載的類型是永遠可觸及的,所以永遠不會被卸載。只有使用用戶定義的類裝載器裝載的類型纔會變成不可觸及的,從而被虛擬機回收。如果某個類型的Class實例被發現無法通過正常的垃圾收集堆觸及,那麼這個類型就是不可觸及的。
判斷動態裝載的類型Class實例在正常的垃圾收集過程中是否可以觸及有兩種方式:
1.如果程序保持對Class實例的明確引用就是可觸及的
2.如果在堆中還存在一個可觸及的對象,在方法區中它的類型數據指向一個Class實例,那麼這個Class實例就是可觸及的。從類型數據(也就是方法區),Java虛擬機必須能夠確定對象的類,它的所有超類以及所有超接口的class實例

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章