註意:本篇博客,主要參考自以下三本書 《分散式Java應用:基礎與實踐》 《深入理解Java虛擬機(第二版)》 《突破程式員基本功的16課》 說明:關於JVM記憶體結構,查看《第一章 JVM記憶體結構》,下麵所講的JVM記憶體分配主要是指在Hotspot JVM下新建對象在堆記憶體中分配的情況。 1、創建一
註意:本篇博客,主要參考自以下三本書
《分散式Java應用:基礎與實踐》
《深入理解Java虛擬機(第二版)》
《突破程式員基本功的16課》
說明:關於JVM記憶體結構,查看《第一章 JVM記憶體結構》,下麵所講的JVM記憶體分配主要是指在Hotspot JVM下新建對象在堆記憶體中分配的情況。
1、創建一個真正對象的基本過程
五步:
- 類載入機制檢查
- JVM首先檢查一個new指令的參數是否能在常量池中定位到一個符號引用,並且檢查該符號引用代表的類是否已被載入、解析和初始化過(實際上就是在檢查new的對象所屬的類是否已經執行過類載入機制)。如果沒有,先進行類載入機制載入類。關於類載入機制,之後再說。
- 分配記憶體
- 把一塊兒確定大小的記憶體從Java堆中劃分出來
- 初始化零值(操作實例數據部分--對象記憶體佈局三部分之一)
- 對象的實例欄位不需要賦初始值也可以直接使用其預設零值,就是這裡起得作用
- 每一種類型的對象都有不同的預設零值
- 設置對象頭(操作對象頭部分--對象記憶體佈局三部分之一)
- 參看對象記憶體佈局《附 Java對象記憶體佈局》
- 執行<init>
- 為對象的欄位賦值(在第三步只是初始化了零值,這裡會根據所寫程式給實例賦值)
2、記憶體分配概念
- 在類載入完成後,一個對象所需的記憶體大小就可以完全確定了,具體的情況查看對象的記憶體佈局。
- 為對象分配空間,即把一塊兒確定大小(上述確定下來的對象記憶體大小)的記憶體從Java堆中劃分出來
3、記憶體分配兩種方式
- 指針碰撞
- 適用場合:堆記憶體規整(即沒有記憶體碎片)的情況下
- 原理:用過的記憶體全部整合到一邊,沒有用過的記憶體放在另一邊,中間有一個分界值指針,只需要向著沒用過的記憶體方向將該指針移動對象記憶體大小位置即可
- GC收集器:Serial、ParNew
- 空閑列表
- 適用場合:堆記憶體不規整的情況下
- 原理:虛擬機會維護一個列表,該列表中會記錄哪些記憶體塊是可用的,在分配的時候,找一塊兒足夠大的記憶體塊兒來劃分給對象實例(這一塊兒可以類比memcached的slab模型),最後更新列表記錄。關於memcached slab記憶體模型介紹查看《第六章 memcached剖析》
- GC收集器:CMS
- 註意
- 選擇以上兩種方式中的哪一種,取決於Java堆記憶體是否規整
- Java堆記憶體是否規整,取決於GC收集器的演算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮")
4、記憶體分配併發問題
堆記憶體是各個線程的共用區域,所以在操作堆記憶體的時候,需要處理併發問題。處理的方式有兩種:
- CAS+失敗重試
- 具體的做法與AtomicInteger的getAndSet(int newValue)方法的實現方式類似,關於AtomicInteger的源碼解析,查看《第十一章 AtomicInteger源碼解析》
- TLAB(Thread Local Allocation Buffer)
- 原理:為每一個線程預先在Eden區分配一塊兒記憶體,JVM在給線程中的對象分配記憶體時,首先在TLAB分配,當對象大於TLAB中的剩餘記憶體或TLAB的記憶體已用盡時,再採用上述的CAS進行記憶體分配
- -XX:+/-UseTLAB:是否使用TLAB
- -XX:TLABWasteTargetPercent設置TLAB可占用的Eden區的比率,預設為1%
- JVM會根據以下三個條件來給每個線程分配合適大小的TLAB
- -XX:TLABWasteTargetPercent
- 線程數量
- 線程是否頻繁分配對象
- -XX:PrintTLAB:查看TLAB的使用情況
5、總結
- 儘量少創建對象
- 根據第一塊兒所說,創建一個對象的過程比較複雜,耗時較多,所以儘量減少對象的創建
- 對象創建的少,將來垃圾收集器收集的垃圾也就少,提高效率
- 對象創建的少,占用記憶體也就少,那麼剩餘的系統記憶體也就相對多,系統運行也就快
- 避免在經常使用的方法中或迴圈中創建對象
- 多個小的對象比大對象分配起來更加高效
- 這是根據TLAB得出來的,多個小對象可以並行在各自的TLAB分配記憶體,而大對象的話,可能只能通過CAS同步來分配記憶體
- 衡量上述兩點
- 對於String
- 儘量使用直接量:eg. String str = "hello";//常量會直接存在"常量池",而非String str = new String("hello");//除了將"hello"存在"常量池"之外,還會創建一個char[]
- 不要使用String去拼接字元串,會形成許多臨時字元串:如下,
String s1 = "hello1"; String s2 = "hello2"; String s3 = "hello3"; String s4 = s1+s2+s3;
View Code實際上,我們只想要字元串s1+s2+s3,但是在上述的拼接過程中,會形成s1+s2的臨時字元串。拼接字元串,使用StringBuilder,該類相較於StringBuffer由於不是同步類,其運行效果會更好。
- 儘早釋放無用對象的引用(幫助垃圾回收)
public void info(){ Object obj = new Object(); System.out.println(obj.hashCode()); obj = null;//顯式釋放無用對象 }
View Code如上邊方法所示,其中的obj是一個局部變數,在方法執行結束後,棧幀就會出棧並被回收,棧幀中所存儲的局部變數一起被回收掉了,所以這裡的"obj=null;"就沒用了,但是看下邊
public void info(){ Object obj = new Object(); System.out.println(obj.hashCode()); obj = null;//顯式釋放無用對象 //下邊還有一些很耗時、很耗記憶體的操作,這些操作與obj無關 }
View Code這時候,如果我們加上了"obj=null;"這一句,那麼就有可能在方法執行結束之前,obj被回收。
- 儘量少使用static變數,因為static變數屬於類變數,存儲於方法區,其所占記憶體無法被垃圾回收器回收掉,除非static所屬的類被卸載掉。
- 常用的對象放入緩存或連接池(其實,連接池也可以看做是一個緩存)
- 考慮使用SoftReference(關於幾種引用方式,之後會說)
- 當記憶體足夠時,功能等同於普通變數
- 當記憶體不足時,釋放軟引用所引用的對象
- 一般用於大數組、大對象
- 通過軟引用所獲取的對象可能為null(當記憶體不足時,釋放軟引用所引用的對象),在應用程式中需要顯示判斷對象是否為null,若為null,需要重建對象