1. 說明
java是一門完全的面向對象編程語言。對於開發者而言,面向對象的思想無疑是非常重要的,但是對於對象本身我們也有必要知道,對象從怎么來的?又怎么死的。
通常我們創建一個對象最常見的方式如下
Object object=new Object();
這樣對象就被創建了,我們可以操作object去實現我們需要的功能,但是問題在於,怎么創建的?執行了所有代碼后,對象又去哪兒了呢?
2. 如何創建
學過java的都知道,對象創建后,會調用構造方法和代碼塊完成初始化(這里不探究父類子類的執行順序),這個過程是可控的,是程序員所能夠完全操作的過程。但是往深一點分析,對象是如何被創建的呢。
-
對象存放在什么地方
首先應該思考的問題是java虛擬機在什么地方存放對象。
java虛擬機將內存按照以下內容進行管理
-
方法區
-
虛擬機棧
-
本地方法棧
-
java堆
-
程序計數器
如果不考慮逃逸分析和棧上分配,java虛擬機將對象分配在堆中。
-
-
開辟多大的內存空間
既然是要存放對象,自然需要在堆中開辟一塊內存,那么問題來了,應該為對象開辟多大的內存空間呢?
為了使得問題變得簡單,通常我們希望對象分配多大內存在分配之前是已知的,那么我們的內存大小是否已知呢?答案是是的,看看類中有什么,屬性和方法,屬性由基本數據類型和引用數據類型,這些數據類型所占的內存都是已知的,所以在分配之前我們知道應該分配多大內存。
-
對象由什么組成
對象內存由三個部分組成:對象頭,對象體,對齊填充
-
對象頭
第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit,官方稱它為“Mark Word”。
Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲盡量多的信息,它會根據對象的狀態復用自己的存儲空間。
對象頭的另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據。
-
實例數據
實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。
-
對齊填充
自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
-
-
怎么分配的
-
指針碰撞
假設Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放着一個指針作為分界點的指示器,那所分配內存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離。
-
空閑列表
如果Java堆中的內存並不是規整的,已使用的內存和空閑的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間划分給對象實例,並更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。
-
線程分配緩沖
上面的兩種方式看上去沒有什么問題,但是考慮一下多線程環境下分配對象,很可能將空閑的內存和已經使用的內存搞混,如何解決這個問題呢。java虛擬機為每一個線程分配了一個線程分配緩沖(TLAB)不同線程分配內存的時候現在線程分配緩沖中分配,這樣對於線程分配緩沖而言,每次只有一個線程在他上面分配內存,這樣就不會將使用過和未使用的內存搞混了。
-
-
為什么對象屬性有初始值
已經有了一塊內存用來存對象了,但是這塊內存可能之前被別人使用過,內存中的數據還是以前的數據,那么為什么我們訪問對象的一個成員變量,即使我們沒有初始化也會有個初始值呢?
當內存分配完成后,jvm會將對象分配的內存空間初始化為零值(不包括對象頭)。
這樣我們就有了一個真正意義上的對象,之前我們分析過,對象可以按照程序員的要求進行初始化,初始化的方法有很多,構造方法,代碼塊,直接初始化等等,java虛擬機會按照一定的順序將這些過程封裝成一個
<init>
方法,調用后就得到了一個對象了。 -
小總結
- 檢查類是否加載進入方法區,如果沒有則執行類加載過程
- 分配一塊固定大小的內存塊
- 將內存塊初始化零值
- 執行
<init>
方法
3. 如何回收
java的一個特點是,對象不需要程序員手動回收,而是通過java虛擬機實現垃圾自動回收。
既然要回收我們一定要搞清楚這幾個問題?
- 什么對象需要回收
- 對象如何回收
對象如何回收依靠具體的垃圾回收器實現,顯然在這里知道什么對象需要回收更重要。
-
分析
什么對象需要回收呢?聯想一下什么樣的東西是垃圾?所謂垃圾指的是這個東西已經沒有人使用了,那么它就是垃圾,對象也是一樣,當一個對象沒有地方引用了,這個對象就是垃圾,就應該被回收,如何直到這個對象沒用了呢?一般有兩種方法,但是這里先不說,而是以我們自己的思路去分析。
public class Test{ public static Object func1(){ Object o1=new Object(); return o1; } public static void func2(){ Object o2=func1(); } public static void main(String[] args){ func2(); } }
我們通常是通過引用互相賦值來實現對象引用關系的變換,以上面的代碼為例。在func2方法中調用func1方法,會先創建一個對象,該對象通過o1進行訪問。如果沒有func2方法中
Object o2=func1()
,o1所指向的對象就無法被訪問了,而現在由於將o1賦給了02,雖然o1已經不再指向對象,但是依然可以通過o2去訪問,也就是說該對象還能用,不能被回收,當func2方法執行完,由於main方法中無法保存之前那個對象的引用,導致該對象在也無法被訪問,因此該對象可以被回收。所以我們可以記錄每個對象有多少個引用存在,當引用數目為0的時候,則這個對象可以被回收。
但是存在這么一個問題
Class Test{ Test t; public static Object func1(){ Test t1,t2; t1.t=t2; t2.t=t1; } public static void main(String[] args){ func1(); } }
func執行完,很顯然t1和t2應該被回收,但是t2保存了t1的引用,t1保存了t2的引用,那么他們的引用數都不是0。
雖然上面這種算法是很簡單實現的,但是因為存在互相引用的權限,所以我們只能再次進行思考。
我們會在上面地方使用引用指向一個對象呢?類的靜態變量,類的常量,棧中的執行方法時的局部變量。
既然我們只會在這幾種地方使用引用執行一個對象,那么不管對象之間是否存在相互引用,只要他們沒有直接或者間接的被上面幾種引用指向,那么這個對象就是可以回收的,這樣看來,那些引用就像根一樣,所有可用的對象必然都有一個這樣的根。
其實上面兩種思路對應着兩種不同的垃圾回收算法
- 引用計數法
- 可達性分析
-
引用計數法
為每一個對象維護一個引用計數器,每當該對象被引用則加一,取消引用則減一,當引用計數器為0時,說明該對象無法被引用,則可以進行回收。這種方法的優點是實現簡單,效率高,但是缺點是無法解決循環引用的問題。
-
可達性分析
可達性分析算法,從被稱為GC_Root的引用進行查找所走過的路徑稱為引用鏈,不在任何一條引用鏈的對象就是不可達的,可以被回收,能夠作為GC_Root的對象包括虛擬機棧上(局部變量表)的引用,方法區紅靜態變量,方法區中的常量。