深入分析JVM中的物件及引用(十六)
今天分析JVM中的物件建立過程和使用流程:
一、JVM
中物件的建立過程
1、物件的記憶體分配
虛擬機器遇到一條
new
指令時,首先檢查是否被類載入器載入,如果沒有,那必須先執行相應的類載入過程。
類載入就是把
class
載入到
JVM
的執行時資料區的過程(類載入後面有講解)。
1
)檢查載入
首先檢查這個指令的引數是否能在常量池中定位到一個類的符號引用(
符號引用 :
符號引用以一組符號來描述所引用的目標
),並且檢查類是否已經被載入、
解析和初始化過。
2
)分配記憶體
接下來虛擬機器將為新生物件分配記憶體。為物件分配空間的任務等同於把一塊確定大小的記憶體從
Java
堆中劃分出來。
2、指標碰撞
如果
Java
堆中記憶體是絕對規整的,所有用過的記憶體都放在一邊,空閒的記憶體放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅
是把那個指標向空閒空間那邊挪動一段與物件大小相等的距離,這種分配方式稱為“
指標碰撞
”。
3、空閒列表
如果
Java
堆中的記憶體並不是規整的,已使用的記憶體和空閒的記憶體相互交錯,那就沒有辦法簡單地進行指標碰撞了,虛擬機器就必須維護一個列表,記錄上 哪些記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄,這種分配方式稱為“空閒列表
”。
選擇哪種分配方式由
Java
堆是否規整決定,而
Java
堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
(這部分知識先了解,後續結合垃圾回收器一起去理解)
如果是
Serial
、
ParNew
等帶有壓縮的整理的垃圾回收器的話,系統採用的是指標碰撞,既簡單又高效。
如果是使用
CMS
這種不帶壓縮(整理)的垃圾回收器的話,理論上只能採用較複雜的空閒列表。
4、併發安全
除如何劃分可用空間之外,還有另外一個需要考慮的問題是物件建立在虛擬機器中是非常頻繁的行為,即使是僅僅修改一個指標所指向的位置,在併發情 況下也並不是執行緒安全的,可能出現正在給物件 A
分配記憶體,指標還沒來得及修改,物件
B
又同時使用了原來的指標來分配記憶體的情況。
5、CAS
機制
解決這個問題有兩種方案,一種是對分配記憶體空間的動作進行同步處理——實際上虛擬機器採用
CAS
配上失敗重試的方式保證更新操作的原子性;
6、分配緩衝
另一種是把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在
Java
堆中預先分配一小塊私有記憶體,也就是本地執行緒分配緩衝(
Thread Local Allocation Buffer,TLAB),
JVM
線上程初始化時,同時也會申請一塊指定大小的記憶體,只給當前執行緒使用,這樣每個執行緒都單獨擁有一個
Buffer
,如果 需要分配記憶體,就在自己的 Buffer
上分配,這樣就不存在競爭的情況,可以大大提升分配效率,當
Buffer
容量不夠的時候,再重新從
Eden
區域申請一塊 繼續使用。 TLAB 的目的是在為新物件分配記憶體空間時,讓每個
Java
應用執行緒能在使用自己專屬的分配指標來分配空間,減少同步開銷。 TLAB 只是讓每個執行緒有私有的分配指標,但底下存物件的記憶體空間還是給所有執行緒訪問的,只是其它執行緒無法在這個區域分配而已。當一個
TLAB
用滿(分 配指標 top
撞上分配極限
end
了),就新申請一個
TLAB
。
引數:
-XX:+UseTLAB
允許在年輕代空間中使用執行緒本地分配塊(
TLAB
)。預設情況下啟用此選項。要禁用
TLAB
,請指定
-XX:-UseTLAB
。
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
3
)記憶體空間初始化
(注意不是構造方法)記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值
(
如
int
值為
0
,
boolean
值為
false
等等
)
。這一步操作保證了物件
的例項欄位在
Java
程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。
4
)設定
接下來,虛擬機器要對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到類的後設資料資訊(
Java classes
在
Java hotspot VM
內部表示為類
後設資料)、物件的雜湊碼、物件的
GC
分代年齡等資訊。這些資訊存放在物件的物件頭之中。
5
)物件初始化
在上面工作都完成之後,從虛擬機器的視角來看,一個新的物件已經產生了,但從
Java
程式的視角來看,物件建立才剛剛開始,所有的欄位都還為零值。
所以,一般來說,執行
new
指令之後會接著把物件按照程式設計師的意願進行初始化
(
構造方法
)
,這樣一個真正可用的物件才算完全產生出來。
物件的記憶體佈局如圖:
在
HotSpot
虛擬機器中,物件在記憶體中儲存的佈局可以分為
3
塊區域:物件頭(
Header
)、例項資料(
Instance Data
)和對齊填充(
Padding
)。 物件頭包括兩部分資訊,第一部分用於儲存物件自身的執行時資料,如雜湊碼(HashCode
)、
GC
分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒 ID、偏向時間戳等。 物件頭的另外一部分是型別指標,即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。 如果物件是一個 java
陣列,那麼在物件頭中還有一塊用於記錄陣列長度的資料。 第三部分對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起著佔位符的作用。由於 HotSpot VM
的自動記憶體管理系統要求對物件的大小必須 是 8
位元組的整數倍。當物件其他資料部分沒有對齊時,就需要通過對齊填充來補全。
4、物件的訪問定位
建立物件是為了使用物件,我們的
Java
程式需要通過棧上的
reference
資料來操作堆上的具體物件。目前主流的訪問方式有使用控制程式碼和直接指標兩種。
1)控制程式碼
如果使用控制程式碼訪問的話,那麼
Java
堆中將會劃分出一塊記憶體來作為控制程式碼池,
reference
中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與類 型資料各自的具體地址資訊。 使用控制程式碼來訪問的最大好處就是 reference
中儲存的是穩定的控制程式碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制程式碼中的實 例資料指標,而 reference
本身不需要修改
.
2)直接指標
如果使用直接指標訪問,
reference
中儲存的直接就是物件地址。 這兩種物件訪問方式各有優勢,使用直接指標訪問方式的最大好處就是速度更快,它節省了一次指標定位的時間開銷,由於物件的訪問在 Java
中非常頻 繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。 對 Sun HotSpot
而言,它是使用直接指標訪問方式進行物件訪問的。
3)判斷物件的存活
在堆裡面存放著幾乎所有的物件例項,垃圾回收器在對對進行回收前,要做的事情就是確定這些物件中哪些還是“存活”著,哪些已經“死去”(死去 代表著不可能再被任何途徑使用得物件了)
5、什麼是垃圾?
C
語言申請記憶體:
malloc free
C++
:
new delete
C/C++
手動回收記憶體
Java: new
Java
是自動記憶體回收,程式設計上簡單,系統不容易出錯。
手動釋放記憶體,容易出兩種型別的問題:
1
、忘記回收
2
、多次回收
沒有任何引用指向的一個物件或者多個物件(迴圈引用)
1)引用計數法
在物件中新增一個引用計數器,每當有一個地方引用它,計數器就加
1
,當引用失效時,計數器減
1.
Python
在用,但主流虛擬機器沒有使用,因為存在物件相互引用的情況,這個時候需要引入額外的機制來處理,這樣做影響效率,
在程式碼中看到,只保留相互引用的物件還是被回收掉了,說明
JVM
中採用的不是引用計數法。
2)可達性分析
(
面試時重要的知識點,牢記
)
來判定物件是否存活的。這個演算法的基本思路就是通過一系列的稱為“
GC Roots
”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為 引用鏈(Reference Chain
),當一個物件到
GC Roots
沒有任何引用鏈相連時,則證明此物件是不可用的。 作為 GC Roots
的物件包括下面幾種
(重點是前面
4
種)
:
虛擬機器棧(棧幀中的本地變數表)中引用的物件;各個執行緒呼叫方法堆疊中使用到的引數、區域性變數、臨時變數等。
方法區中類靜態屬性引用的物件;java
類的引用型別靜態變數。
方法區中常量引用的物件;比如:字串常量池裡的引用。
本地方法棧中 JNI
(即一般說的
Native
方法)引用的物件。
JVM
的內部引用(
class
物件、異常物件
NullPointException
、
OutofMemoryError
,系統類載入器)。(非重點)
所有被同步鎖(synchronized
關鍵
)
持有的物件。(非重點)
JVM
內部的
JMXBean
、
JVMTI
中註冊的回撥、原生程式碼快取等(非重點)
JVM
實現中的“臨時性”物件,跨代引用的物件(
在使用分代模型回收只回收部分代的物件,這個後續會細講,先大致瞭解概念
)(非重點)
以上的回收都是物件,類的回收條件:
注意
Class
要被回收,條件比較苛刻,必須同時滿足以下的條件(僅僅是可以,不代表必然,因為還有一些引數可以進行控制):
1
、該類所有的例項都已經被回收,也就是堆中不存在該類的任何例項。
2
、 載入該類的
ClassLoader
已經被回收。
3、 該類對應的
java.lang.Class
物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
4
、 引數控制:
廢棄的常量和靜態變數的回收其實就和
Class
回收的條件差不多。
Finalize
方法
即使通過可達性分析判斷不可達的物件,也不是“非死不可”,它還會處於“緩刑”階段,真正要宣告一個物件死亡,需要經過兩次標記過程,一次是 沒有找到與 GCRoots
的引用鏈,它將被第一次標記。隨後進行一次篩選(如果物件覆蓋了
finalize
),我們可以在
finalize
中去拯救。
程式碼演示:
執行結果:
可以看到,物件可以被拯救一次
(finalize
執行第一次,但是不會執行第二次
)
程式碼改一下,再來一次。
執行結果:
物件沒有被拯救,這個就是
finalize
方法執行緩慢,還沒有完成拯救,垃圾回收器就已經回收掉了。
所以建議大家儘量不要使用
finalize
,因為這個方法太不可靠。在生產中你很難控制方法的執行或者物件的呼叫順序,建議大家忘了
finalize
方法!因為在
finalize
方法能做的工作,
java
中有更好的,比如
try-finally
或者其他方式可以做得更好。下篇分析JVM中物件的引用和分配策略,敬請期待!
相關文章
- java中的引用物件Java物件
- 3、JVM中的物件JVM物件
- PHP中物件的引用傳遞PHP物件
- 深入分析C++引用C++
- JVM - 引用JVM
- 【JVM】如何理解強引用、軟引用、弱引用、虛引用?JVM
- 深入分析JVM執行引擎JVM
- 物件與物件引用的區別物件
- JVM 符號引用和直接引用JVM符號
- 深入探究JVM之物件建立及分配策略JVM物件
- php中物件是引用型別嗎?PHP物件型別
- JVM 系列文章之 物件存活分析 - 引用計數 and 可達性分析JVM物件
- Python中的物件引用、可變性和垃圾回收Python物件
- [轉載] .NET 中可以有類似 JVM 的幻像引用嗎?JVM
- python中的引用傳遞,可變物件,不可變物件,list注意點Python物件
- Java HashMap中的載入因子及初始容量深入分析JavaHashMap
- Java物件在JVM中長啥樣Java物件JVM
- c++中物件的引用作為函式的引數C++物件函式
- Spring 迴圈引用(三)原始碼深入分析版Spring原始碼
- 物件的引用計數與dealloc物件
- JVM(四):深入分析Java位元組碼-下JVMJava
- JVM(三):深入分析Java位元組碼-上JVMJava
- 深入理解JVM虛擬機器-物件引用,GC與記憶體分配回收JVM虛擬機物件GC記憶體
- java中多型的理解——父類引用指向子類物件Java多型物件
- JVM系列(三) - JVM物件探祕JVM物件
- JVM中java例項物件在記憶體中的佈局JVMJava物件記憶體
- Java/Android中的強引用、軟引用、弱引用、虛引用JavaAndroid
- 十六.Apache的管理及優化Apache優化
- JVM 堆中物件分配、佈局和訪問JVM物件
- 十六、物件導向程式設計物件程式設計
- Java基礎11 物件引用Java物件
- PHP物件導向之&引用PHP物件
- 深入理解JVM——物件JVM物件
- JVM(八):Java 物件模型JVMJava物件模型
- [JVM]物件建立過程JVM物件
- Java中的引用Java
- JVM -- 物件的記憶體佈局JVM物件記憶體
- JVM是如何建立一個物件的?JVM物件