Java基礎(chǔ)-class

本文同步于我的個人博客 ->>?https://blog.hehouhui.cn/archives/43? 點擊前往查看更多內(nèi)容
對象,類
類加載
一個類的完整生命周期如下:

Class 文件需要加載到虛擬機中之后才能運行和使用,那么虛擬機是如何加載這些 Class 文件呢?
系統(tǒng)加載 Class 類型的文件主要三步:加載->連接->初始化。連接過程又可分為三步:驗證->準備->解析。

加載
類加載過程的第一步,主要完成下面3件事情:
通過全類名獲取定義此類的二進制字節(jié)流
將字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
在內(nèi)存中生成一個代表該類的 Class 對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口
虛擬機規(guī)范上面這3點并不具體,因此是非常靈活的。比如:“通過全類名獲取定義此類的二進制字節(jié)流” 并沒有指明具體從哪里獲取、怎樣獲取。比如:比較常見的就是從 ZIP 包中讀取(日后出現(xiàn)的JAR、EAR、WAR格式的基礎(chǔ))、其他文件生成(典型應(yīng)用就是JSP)等等。
一個非數(shù)組類的加載階段(加載階段獲取類的二進制字節(jié)流的動作)是可控性最強的階段,這一步我們可以去完成還可以自定義類加載器去控制字節(jié)流的獲取方式(重寫一個類加載器的 loadClass()
方法)。數(shù)組類型不通過類加載器創(chuàng)建,它由 Java 虛擬機直接創(chuàng)建。
類加載器、雙親委派模型也是非常重要的知識點,這部分內(nèi)容會在后面的文章中單獨介紹到。
加載階段和連接階段的部分內(nèi)容是交叉進行的,加載階段尚未結(jié)束,連接階段可能就已經(jīng)開始了。
驗證

準備
準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中分配。對于該階段有以下幾點需要注意:
這時候進行內(nèi)存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中。
這里所設(shè)置的初始值"通常情況"下是數(shù)據(jù)類型默認的零值(如0、0L、null、false等),比如我們定義了
public static int value=111
,那么 value 變量在準備階段的初始值就是 0 而不是111(初始化階段才會賦值)。特殊情況:比如給 value 變量加上了 fianl 關(guān)鍵字public static final int value=111
,那么準備階段 value 的值就被賦值為 111。
基本數(shù)據(jù)類型的零值:

解析
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用限定符7類符號引用進行。
符號引用就是一組符號來描述目標,可以是任何字面量。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。在程序?qū)嶋H運行時,只有符號引用是不夠的,舉個例子:在程序執(zhí)行方法時,系統(tǒng)需要明確知道這個方法所在的位置。Java 虛擬機為每個類都準備了一張方法表來存放類中所有的方法。當需要調(diào)用一個類的方法的時候,只要知道這個方法在方發(fā)表中的偏移量就可以直接調(diào)用該方法了。通過解析操作符號引用就可以直接轉(zhuǎn)變?yōu)槟繕朔椒ㄔ陬愔蟹椒ū淼奈恢?,從而使得方法可以被調(diào)用。
綜上,解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程,也就是得到類或者字段、方法在內(nèi)存中的指針或者偏移量。
初始化
初始化是類加載的最后一步,也是真正執(zhí)行類中定義的 Java 程序代碼(字節(jié)碼),初始化階段是執(zhí)行初始化方法 ?()
方法的過程。
對于()
方法的調(diào)用,虛擬機會自己確保其在多線程環(huán)境中的安全性。因為 ()
方法是帶鎖線程安全,所以在多線程環(huán)境下進行類初始化的話可能會引起死鎖,并且這種死鎖很難被發(fā)現(xiàn)。
對于初始化階段,虛擬機嚴格規(guī)范了有且只有5種情況下,必須對類進行初始化(只有主動去使用類才會初始化類):
當遇到 new 、 getstatic、putstatic或invokestatic 這4條直接碼指令時,比如 new 一個類,讀取一個靜態(tài)字段(未被 final 修飾)、或調(diào)用一個類的靜態(tài)方法時。
當jvm執(zhí)行new指令時會初始化類。即當程序創(chuàng)建一個類的實例對象。
當jvm執(zhí)行g(shù)etstatic指令時會初始化類。即程序訪問類的靜態(tài)變量(不是靜態(tài)常量,常量會被加載到運行時常量池)。
當jvm執(zhí)行putstatic指令時會初始化類。即程序給類的靜態(tài)變量賦值。
當jvm執(zhí)行invokestatic指令時會初始化類。即程序調(diào)用類的靜態(tài)方法。
使用
java.lang.reflect
包的方法對類進行反射調(diào)用時如Class.forname("…"),newInstance()等等。 ,如果類沒初始化,需要觸發(fā)其初始化。初始化一個類,如果其父類還未初始化,則先觸發(fā)該父類的初始化。
當虛擬機啟動時,用戶需要定義一個要執(zhí)行的主類 (包含 main 方法的那個類),虛擬機會先初始化這個類。
MethodHandle和VarHandle可以看作是輕量級的反射調(diào)用機制,而要想使用這2個調(diào)用, 就必須先使用findStaticVarHandle來初始化要調(diào)用的類。
「補充,來自issue745」 當一個接口中定義了JDK8新加入的默認方法(被default關(guān)鍵字修飾的接口方法)時,如果有這個接口的實現(xiàn)類發(fā)生了初始化,那該接口要在其之前被初始化。
卸載
卸載這部分內(nèi)容來自 issue#662由 guang19 補充完善。
卸載類即該類的Class對象被GC。
卸載類需要滿足3個要求:
該類的所有的實例對象都已被GC,也就是說堆不存在該類的實例對象。
該類沒有在其他任何地方被引用
該類的類加載器的實例已被GC
所以,在JVM生命周期類,由jvm自帶的類加載器加載的類是不會被卸載的。但是由我們自定義的類加載器加載的類是可能被卸載的。
只要想通一點就好了,jdk自帶的BootstrapClassLoader,ExtClassLoader,AppClassLoader負責加載jdk提供的類,所以它們(類加載器的實例)肯定不會被回收。而我們自定義的類加載器的實例是可以被回收的,所以使用我們自定義加載器加載的類是可以被卸載掉的。
對象的創(chuàng)建
下圖便是 Java 對象的創(chuàng)建過程,我建議最好是能默寫出來,并且要掌握每一步在做什么。

類加載檢查
虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到這個類的符號引用,并且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程。
分配內(nèi)存
在類加載檢查通過后,接下來虛擬機將為新生對象分配內(nèi)存。對象所需的內(nèi)存大小在類加載完成后便可確定,為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從 Java 堆中劃分出來。分配方式有 “指針碰撞” 和 “空閑列表” 兩種,選擇哪種分配方式由 Java 堆是否規(guī)整決定,而 Java 堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
內(nèi)存分配的兩種方式:(補充內(nèi)容,需要掌握)
選擇以上兩種方式中的哪一種,取決于 Java 堆內(nèi)存是否規(guī)整。而 Java 堆內(nèi)存是否規(guī)整,取決于 GC 收集器的算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,復(fù)制算法內(nèi)存也是規(guī)整的

內(nèi)存分配并發(fā)問題(補充內(nèi)容,需要掌握)
在創(chuàng)建對象的時候有一個很重要的問題,就是線程安全,因為在實際開發(fā)過程中,創(chuàng)建對象是很頻繁的事情,作為虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機采用兩種方式來保證線程安全:
CAS+失敗重試: CAS 是樂觀鎖的一種實現(xiàn)方式。所謂樂觀鎖就是,每次不加鎖而是假設(shè)沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性。
TLAB: 為每一個線程預(yù)先在 Eden 區(qū)分配一塊兒內(nèi)存,JVM 在給線程中的對象分配內(nèi)存時,首先在 TLAB 分配,當對象大于 TLAB 中的剩余內(nèi)存或 TLAB 的內(nèi)存已用盡時,再采用上述的 CAS 進行內(nèi)存分配
初始化零值
內(nèi)存分配完成后,虛擬機需要將分配到的內(nèi)存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值。
設(shè)置對象頭
初始化零值完成之后,虛擬機要對對象進行必要的設(shè)置,例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據(jù)虛擬機當前運行狀態(tài)的不同,如是否啟用偏向鎖等,對象頭會有不同的設(shè)置方式。
執(zhí)行init方法
在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經(jīng)產(chǎn)生了,但從 Java 程序的視角來看,對象創(chuàng)建才剛開始,方法還沒有執(zhí)行,所有的字段都還為零。所以一般來說,執(zhí)行 new 指令之后會接著執(zhí)行
方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產(chǎn)生出來。
對象的內(nèi)存區(qū)域
在 Hotspot 虛擬機中,對象在內(nèi)存中的布局可以分為 3 塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充。
Hotspot 虛擬機的對象頭包括兩部分信息,第一部分用于存儲對象自身的運行時數(shù)據(jù)(哈希碼、GC 分代年齡、鎖狀態(tài)標志等等),另一部分是類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是那個類的實例。
實例數(shù)據(jù)部分是對象真正存儲的有效信息,也是在程序中所定義的各種類型的字段內(nèi)容。
對齊填充部分不是必然存在的,也沒有什么特別的含義,僅僅起占位作用。 因為 Hotspot 虛擬機的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是 8 字節(jié)的整數(shù)倍,換句話說就是對象的大小必須是 8 字節(jié)的整數(shù)倍。而對象頭部分正好是 8 字節(jié)的倍數(shù)(1 倍或 2 倍),因此,當對象實例數(shù)據(jù)部分沒有對齊時,就需要通過對齊填充來補全。
對象的訪問定位
建立對象就是為了使用對象,我們的 Java 程序通過棧上的 reference 數(shù)據(jù)來操作堆上的具體對象。對象的訪問方式由虛擬機實現(xiàn)而定,目前主流的訪問方式有①使用句柄和②直接指針兩種:
句柄: 如果使用句柄的話,那么 Java 堆中將會劃分出一塊內(nèi)存來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息;

直接指針: 如果使用直接指針訪問,那么 Java 堆對象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,而 reference 中存儲的直接就是對象的地址。

這兩種對象訪問方式各有優(yōu)勢。使用句柄來訪問的最大好處是 reference 中存儲的是穩(wěn)定的句柄地址,在對象被移動時只會改變句柄中的實例數(shù)據(jù)指針,而 reference 本身不需要修改。使用直接指針訪問方式最大的好處就是速度快,它節(jié)省了一次指針定位的時間開銷。
本文來源?https://blog.hehouhui.cn/archives/43?