咱們在建立普通對象的時候只須要new關鍵字就解決了,可是在new的背後到底經歷了什麼呢?咱們建立一個對象的過程究竟是什麼樣子呢?程序員
咱們的Java虛擬機在遇到一條字節碼new指令時,首先經歷如下的步驟:算法
咱們先不介紹類加載過程,後面若是出了相關博文會在這裏給一個超連接(點擊跳轉)。緩存
在咱們的類檢查經過後,也就是到了咱們的虛擬機爲咱們的新生對象分配內存。咱們的內存分配方式有兩種安全
假設Java堆中內存是絕對規整的,全部被使用過的內存都放在一邊,空閒的內存被放在另外一邊,中間放着一個指針做爲分界點的指示器,那所分配的內存就僅僅是把那個指針向空閒方向挪動一段與對象大小相等的舉例,這種分配方式稱爲「指針碰撞」(Bump ThePointer)。通常使用Serial、ParNew等帶壓縮整理過程的收集器。服務器
假設Java堆中的內存並非規整的,已被使用的內存和空閒的內存相互交錯在一塊兒,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方式稱爲「空閒列表」(Free List)。通常使用CMS這種基於清除(Sweep)算法的收集器。併發
注意,由於咱們的虛擬機建立對象是很是頻繁的,因此僅僅只是修改一個指針的位置,在併發裏也是不安全的。好比給A對象分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的狀況。咱們針對這種併發安全的問題也提出了兩種解決方案:函數
分配完內存就須要將咱們的內存空間都初始化爲零值了。而後開始往咱們的對象的對象頭裏填充一些信息,好比該對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。至此,咱們從虛擬機的角度來看,一個對象已經產生了。可是從Java程序來看,咱們還須要進行構造函數(
(補充:爲了能在多數狀況下可以更快的分配內存,設計了一個叫做LinearAllocation Buffer的分配緩衝區,經過空閒列表拿到一大塊分配緩衝區以後,在它裏面仍然可使用指針碰撞方式來分配。)線程
咱們上面介紹了到了在虛擬機中一個對象的建立,咱們接下來介紹的就是對象在堆內存中的存儲佈局。能夠劃分爲三個部分:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。設計
咱們的對象頭主要包括了兩類信息。
關於咱們的Mark Word會根據對象的狀態來複用空間,也就是處於什麼狀態,就會如何分配咱們的比特存儲空間。好比處於對象未被同步鎖鎖定的狀態下(無鎖態),Mark Word的32個比特存儲空間中的25個比特用於存儲對象哈希碼,4個比特用於存儲對象分代年齡,2個比特用於存儲鎖標誌位,1個比特固定爲0。下面給上其餘狀態的空間分佈:
實例數據是咱們對象真正存儲的有效信息,也就是咱們在程序代碼裏面所定義的各類類型的字段內容,不管是從父類繼承下來的,仍是在子類中定義的字段都必須記錄起來。具體的存儲順序能夠受到虛擬機分配策略參數(-XX:FieldsAllocationStyle參數)和字段在Java源碼中定義順序的影響。
沒有特別的含義,它僅僅起着佔位符的做用。因爲HotSpot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是任何對象的大小都必須是8字節的整數倍。對象頭部分已經被精心設計成正好是8字節的倍數(1倍或者2倍),所以,若是對象實例數據部分沒有對齊的話,就須要經過對齊填充來補全。
咱們建立對象是爲了後續使用對象,Java程序會經過棧上的reference數據(指向對象的引用)來操做堆上的具體對象。具體的主流對象訪問方式主要使用句柄和直接指針兩種。
若是使用句柄訪問的話,Java堆中將可能會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自具體的地址信息,結構以下:
使用句柄來訪問的最大好處就是reference中存儲的是穩定句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而reference自己不須要被修改。
若是使用直接指針的話,那麼咱們的Java堆中對象的內存佈局就必須考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象實例數據,若是隻是訪問對象自己的話,就會了少了一次間接訪問的開銷,結構以下:
使用直接指針訪問的優勢是速度快,少了一次指針定位的時間開銷。若是對象訪問十分頻繁的話,那麼即是極爲可觀的執行成本!
咱們的對象在被垃圾回收器回收的時候會進行判斷對象是否存活,而後才選擇是否回收。而咱們進行判斷的兩種方式以下:
在對象中添加一個引用計數器,若是被引用計數器加 1,引用失效時計數器減 1,若是計數器爲 0 則被標記爲垃圾。原理簡單,效率高,可是在 Java 中不多使用,由於存在對象間循環引用的問題,致使計數器沒法清零。
主流語言的內存管理都使用可達性分析判斷對象是否存活。基本思路是經過一系列稱爲 GC Roots 的根對象做爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程走過的路徑稱爲引用鏈,若是某個對象到 GC Roots 沒有任何引用鏈相連,則會被標記爲垃圾。可做爲 GC Roots 的對象包括虛擬機棧和本地方法棧中引用的對象、類靜態屬性引用的對象、常量引用的對象。
在上面對象定位說到了reference是傳統的某塊內存、對象的引用。可是在JDK1.2以後,咱們的引用被細分紅了四種引用,經過強弱依次遞減分別是,強引用,軟引用,弱引用,虛引用四種。
Object obj = new Object()
就屬於強引用。只要對象有強引用指向且 GC Roots 可達,在內存回收時即便瀕臨內存耗盡也不會被回收。深刻理解Java虛擬機:JVM高級特性(第三版)