1. 程式人生 > >Java內存管理-一文掌握虛擬機創建對象的秘密(九)

Java內存管理-一文掌握虛擬機創建對象的秘密(九)

say tee style bit b- https 如果 bsp 發生

勿在流沙築高臺,出來混遲早要還的。

做一個積極的人

編碼、改bug、提升自己

我有一個樂園,面向編程,春暖花開!

【福利】JVM系列學習資源無套路贈送

回顧一下:

本文是接著上一篇內容:Java內存管理-愚人節new一個對象送給你(八),繼續整理!主要內容講解HotSpot虛擬機在Java堆中對象是如何創建、內存分配布局和訪問方式。

本文地圖:

技術分享圖片

一、給你創建一個對象

如果你是一直從第一季看過來的,那一定知道前面有個地方講過類的整個生命周期,之前只是講到了初始化階段,類是如何使用和類是如何被卸載還沒有進行講解!那本文就簡單介紹一下類的使用,我們new 一個 “如花” 似玉的girl

這裏再回顧一下,類從被加載到虛擬機內存中開始,到卸載出內存為止,它的生命周期包括了七個階段:

  • 加載(Loading)
  • 驗證(Verification)
  • 準備(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸載(Unloading)

在Java中我們用使用一個類,很多時候是創建這個類的一個實例,也就是常說的創建一個對象。其實在Java程序運行過程中,無時無刻都有對象被創建出來。創建對象(如克隆、反序列化)通常僅僅是一個new關鍵字而已。但是在Java虛擬機中一個對象(只是普通的java對象,不包括數組和Class對象等)的創建是怎麽一個過程呢?

第一:虛擬機遇到一條new指令時,首先會去檢查這個指令的參數是否能夠在常量池中定位到一個類的符號引用。然後檢查這個符號引用代表的類是否已經被加載、解析和初始化過。如果沒有進行類加載則執行相應的類加載的過程。 記住:要new對象,要先加載類!

第二:類加載檢查通過後,虛擬機將為新生的對象分配內存。對象所需的內存大小在類加載的時候便可以完全確定(如何確定對象的下文說明) 。為對象分配內存的任務等同於把一塊確定大小的內存從Java堆中劃分出來。分配方式有 “指針碰撞”“空閑列表” 兩種,選擇那種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定(對象在堆上的劃分,這是個復雜的問題,後文繼續探討,這裏只要明白是在對象是在堆上分配內存即可)。 記住:要new對象,要有先分配內存空間!

第三:內存分配完成,虛擬機需要將分配的內存空間都初始化為零值(零值這個概念之前文章也介紹過,這裏就不再說明),這一步的操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,因為程序能訪問這些字段的數據類型對應的零值。 記住:要new對象,虛擬機會幫你為對象的實例字段自動賦予零值!

第四:虛擬機要對對象進行必要的設置,如這個對象是哪個類的實例、如何才能找到類的元數據信息(JDK7是方法區保存)、對象的哈希碼、對象的GC分代年齡等信息。這些信息都存放在對象的對象頭(Object Header)中。

上面工作都完成之後,在虛擬機看來,一個對象就已經產生了。但是從Java程序的角度看,對象的創建才剛剛開始,因為<init> 方法還還沒有執行,所有的字段都是為零值。所以一般來說,在new指令之後會接著執行<init>方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象才算完全產生出來!

記住:對象不是你想new,想new就可以new的!

下面用通過圖解的例子簡單說明(版本jdk1.7):

第一: 一個PrettyGirl類!

public class PrettyGirl {

    /**
     * 姑娘姓字名誰
     */
    String name;

    /**
     * 芳齡幾何
     */
    int age;

    /**
     * 家住何方
     */
    static String address;

    /**
     * 可曾婚配否
     */
    boolean marry;

    void sayHello(){
        System.out.println("Hello...");
    }

    @Override
    public String toString() {
        return "PrettyGirl{" +
                "name=‘" + name + ‘\‘‘ +
                ", age=" + age +
                ", marry=" + marry +
                ‘}‘;
    }
}

技術分享圖片

方法區除了保存類的結構,還保存靜態屬性與靜態方法。編寫中小型程序時,一般不會造成方法區的內存溢出!在JDK1.8 沒有方法區的概念,前面文章中也有提到,這裏為了講解使用圖解還是JDK1.7!

第二:實例化new兩個漂亮女孩!

public static void main(String[] args) {
    PrettyGirl pg1 = new PrettyGirl();
    pg1.name = "Alice";
    pg1.age = 18;
    pg1.address = "changsha";

    PrettyGirl pg2 = new PrettyGirl();
    pg2.name = "Alexia";
    pg2.age = 28;

    System.out.println(pg1 + " ---" + pg1.address);
    System.out.println(pg2 + "----" + pg2.address);

}  
----打印結果:--------
PrettyGirl{name=‘Alice‘, age=18, marry=false} ---changsha
PrettyGirl{name=‘Alexia‘, age=28, marry=false}----changsha    

技術分享圖片

在棧內存為 pg1 變量申請一個空間,在堆內存為PrettyGirl對象申請空間,初始化完畢後將其地址值返回給pg1 ,通過pg1 .name和pg1 .age修改其值,靜態的變量address是類公有的!

堆存放對象持有的數據,同時保持對原類的引用。可以簡單的理解為對象屬性的值保存在堆中,對象調用的方法保存在方法區。

從上圖也可以看到有一個區域是棧,在程序運行的時候,每當遇到方法 調用時候,Java虛擬機就會在棧中劃分一塊內存稱為棧幀(線程私有,堆和方法區線程共享的)。就如上面的程序,在調用main方法的時候,會創建一下棧,棧幀中的內存供局部變量(包括基本類型和引用類型)使用,基本類型和引用類型後文會詳情介紹。當方法調用結束後,虛擬機會回收次棧幀占用的內存。


tips: 回顧

1、堆內存溢出會發生 OutOfMemoryError 錯誤,提示信息“Java heap Space”。

2、在棧中會有兩個異常:

  • 如果線程請求的棧的深度大於虛擬機所允許的最大深度,將拋出StackOverflowError 異常(遞歸可能會導致此異常)!
  • 如果虛擬機在擴展棧時候無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。

3、如果有方法區 也會出現OutOfMemoryError 錯誤,提示信息 “PermGen space”。(JDK8 後無此錯誤提示)

每個區域都有一些參數可以設置,參數學習續持續更新!


二、對象的內存布局

感慨,創建一個對象還是挺不容易的!

在HotSpot虛擬機中,對象在內存中的布局可以分為3塊區域:對象頭(Header)、實例數據(Instance data)和對象填充(Padding)。

那下面就對這三塊區域進行簡單介紹:

1、對象頭- 還是一個看臉的時代!

對象頭包括兩部分信息。第一部分用於存儲對象自身的運行時數據,如

  • 哈希碼(HashCode),一個對象的hashcode是唯一的,如判斷一個對象是不是單例的!
  • GC分代年齡(標明是新生代還是老年代..)
  • 鎖狀態標誌、線程持有的鎖、偏向線程ID(多線程,同步的時候用到)
  • 其他等等….

註: 上面的幾個點,要結合和關聯其他相關知識,理解會更加深入一點。

如 哈希碼hashCode,對下面兩個問題如果你又自己的一些思考,歡迎留言探討!

1、重寫了equals 必須要重寫hashcode,思考一下,為什麽?如果不重寫在使用HashMap的時候會有出現什麽問題?

2、HashMap中相同key存入數據不替換,而是進行疊加存儲,怎麽實現?

問題2提示:只要重寫了key的hashCode()和Map的put()方法,其實就可以實現對於相同key下疊加存儲不同的value了。

第二部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過指針來確定這個對象是那個類的實例。(就如我們上圖的箭頭,可以簡單理解為指針!)

說明:

(1)、並不是所有的虛擬機實現都是必須在對象數據上保留類型指針,也就是查找對象的元數據並一定經過對象本身!

(2)、如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的的數據,因為虛擬機可以通過普通Java對象的元數據確定Java對象的大小,但是從數組的元數據卻無法確定數組的大小。

2、實例數據-了解了外在美,還要註重內在美!

實例數據部分是對象真正存儲的有效信息,也就是程序代碼中定義的各種類型的字段內容。

不論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。記錄的存儲順序會受到虛擬機分配策略參數和字段在Java源碼中的定義的順序相關。

3、對齊填充-對齊填充成為標準網紅!

對象的填充並不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用!由於HotSpot VM的自動內存管理系統要求兌現的起始地址必須是8字節的整數倍,也就是說對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的整數倍,因此當對象實例數據部分沒有對齊時候,就需要填充來補全。

(類比記憶對齊填充,由於審美的標準,有一些人天生就是俊俏的臉蛋和好的身材,不需要進行其他的填充,有一些人可能有好看的臉蛋,但是某些地方和標準還差點意思,就需要填充來達到標準)


tips:字節

字節(byte)計算機裏用來存儲空間的基本計量單位。8個二進制位(bit)構成了一個字節(byte)即1byte=8bit。


三、如何“約”(定位)一個對象

認識了一個對象後,不能總是聊微信,也要約一下吃個飯啥的! 那在Java中建立了一個對象,那肯定是要使用對象的。Java程序是如果找到具體的對象的呢?

在Java程序中需要通過棧上的reference數據來操作堆上的具體對象(如開篇的圖示,棧上面的引入指向堆中具體對象)。但是由於Reference類型在Java虛擬機規範中只規定了一個指向對象的引用,並沒有定義這個引用應該通過何種方式去定位、訪問堆中的對象的具體位置,所以對象訪問方式也是取決於虛擬機實現而定的。

目前主流的訪問方式有使用句柄直接指針兩種。

第一:句柄

使用句柄訪問,在Java對中將會劃分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象的實例數據與類型數據各自 的具體地址信息,如圖,

技術分享圖片

第二:直接指針

使用直接指針,在Java堆對象的布局中就必須考慮如果放置訪問類型數組的相關信息,而reference中存儲的直接就是對象的地址,如圖:

技術分享圖片

兩種方式都各自優勢,簡單總結:

句柄:最大的好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集移動對象是非常普通的行為)時只會改變句柄中的實例數據指針,而Reference本身不需要修改。

直接指針:最大的好處就是速度更快,它節省一次指針定位的開銷,在Java中對象的訪問是非常頻繁的,因此能減少這類開銷對提高性能還是非常客觀的。

虛擬機Hotspot使用的就是直接指針這種方式。但是其他的語言和框架中使用句柄的情況也很常見!

四、本文總結

本文主要整理了Java中一個對象的創建,對象的內存布局以及如何定位一個對象! 也讓我們知道對象不是你想new就可以new的,new出的對象想要“約”也是有不同方式的。

因為我也是在整理和學習中,如果文中內容有不對的地方,歡迎留言指出,謝謝!

五、參考資料

《深入理解Java虛擬機》


謝謝你的閱讀,如果您覺得這篇博文對你有幫助,請點贊或者喜歡,讓更多的人看到!祝你每天開心愉快!


不管做什麽,只要堅持下去就會看到不一樣!在路上,不卑不亢!

博客首頁 : http://blog.csdn.net/u010648555

願你我在人生的路上能都變成最好的自己,能夠成為一個獨擋一面的人技術分享圖片

© 每天都在變得更好的阿飛雲

Java內存管理-一文掌握虛擬機創建對象的秘密(九)