@java
Java是面向對象的語言,所謂「萬事萬物皆對象」就是Java是基於對象來設計程序的,沒有對象程序就沒法運行(8大基本類型除外),那麼對象是如何建立的?在內存中又是怎麼分配的呢?算法
在Java中咱們有幾種方式能夠建立一個新的對象呢?總共有如下幾種方式:數組
爲了便於說明和理解,下文僅針對new出來的對象進行討論。緩存
Java中對象的建立過程就包含上圖中的5個步驟,首先須要驗證待建立對象的類是否已經被JVM記載,若是沒有則會先進行類的加載,若是已經加載則會在堆中(不徹底是堆,後文會講到)分配內存;分配完內存後則是對對象的成員變量設置初始值(0或null),這樣對象在堆中就建立好了。可是,這個對象是屬於哪一個類的還不知道,由於類信息存在於方法區,因此還須要設置對象的頭部(固然頭部中也不只僅只有類型指針信息,稍後也會詳細講到),這樣堆中才建立好了一個完整的對象,可是這個對象的成員變量還都是初始值,因此最後會調用init方法按照咱們本身的意願初始化對象,一個真正的對象就建立好了。
對象的整個建立過程是很是簡單的,可是其中還有不少細節,好比對象會在哪裏建立?分配內存有哪些方式?怎麼保證線程安全?對象頭中有哪些信息?下面一一講解。安全
基本上全部的對象都是在堆中,但並不是絕對,在JDK1.6版本引入了逃逸分析技術。逃逸分析就是指針對對象的做用域進行斷定,當一個對象在方法中被定義後,若是被其它方法或其它線程訪問到,就稱爲方法逃逸或線程逃逸。
該技術針對未逃逸的對象作了一個優化:棧上分配(除此以外還有同步消除、標量替換,這裏暫時不講)。這個優化是指當一個對象能被肯定不會在該方法以外被引用,那麼就能夠直接在虛擬機棧中建立該對象,那麼這個對象就能夠隨着線程的消亡而銷燬,再也不須要垃圾回收器進行回收。這個優化帶來的收益是明顯的,由於有至關一部分對象都只會在該方法內部被引用。逃逸分析默認是開啓的,能夠經過-XX:-DoEscapeAnalysis參數關閉。下面看一個實例:併發
public class EscapeAnalysisTest { public static void main(String[] args) throws Exception { long start = System.currentTimeMillis(); for (int i = 0; i < 50000000; i++) {//5000萬次---5000萬個對象 allocate(); } System.out.println((System.currentTimeMillis() - start) + " ms"); Thread.sleep(600000); } static void allocate() {//逃逸分析(不會逃逸出方法) //這個myObject引用沒有出去,也沒有其餘方法使用 MyObject myObject = new MyObject(2020, 2020.6); } static class MyObject { int a; double b; MyObject(int a, double b) { this.a = a; this.b = b; } } }
加上-XX:+PrintGC參數運行上面的方法,會看到控制檯只是打印了執行時間5ms,可是若再加上-XX:-DoEscapeAnalysis關閉逃逸分析就會出現下面的結果:框架
[GC (Allocation Failure) 66384K->848K(251392K), 0.0012528 secs] [GC (Allocation Failure) 66384K->816K(251392K), 0.0010461 secs] [GC (Allocation Failure) 66352K->848K(316928K), 0.0009666 secs] [GC (Allocation Failure) 131920K->784K(316928K), 0.0018284 secs] [GC (Allocation Failure) 131856K->816K(438272K), 0.0009315 secs] [GC (Allocation Failure) 262960K->684K(438272K), 0.0022738 secs] [GC (Allocation Failure) 262828K->684K(700928K), 0.0005052 secs] 308 ms
執行時間大大提高,主要是用在了GC回收上。佈局
在HotSpot虛擬機中,對象在內存中分爲三塊:對象頭、實例數據和對齊填充。以下圖:
性能
對象的內存佈局上面這張圖寫的很清楚了,其中自身運行時數據瞭解一下有哪些信息便可,類型指針則是指向對象所屬的類,若是對象是數組,則對象頭中還會包含數組的長度信息;實例數據就是指對象的字段信息;最後對齊填充則不是必須的,由於爲了方便處理和計算,HotSpot要求對象的大小必須是8字節的整數倍,所以當不滿8字節的整數倍時,就須要對齊填充來補全。學習
當對象建立完成後就存在於堆中,那麼棧中怎麼定位並引用到該對象呢?虛擬機規範中自己並無定義這一部分該如何實現,具體的實現取決於各個虛擬機廠商,而目前主流的定位方式有兩種:句柄和直接指針。
以上兩種方式在各個語言和框架都有使用,而本文所討論的HotSpot虛擬機使用的是直接指針方式,由於對象的訪問是很是頻繁的,這時效率就顯得格外重要。
JVM不須要咱們手動釋放內存,這是Java廣受歡迎的緣由之一,那麼它是如何作到自動管理內存,回收不須要的對象的呢?既然要回收對象,那麼就須要判斷哪些對象是能夠被回收的,即對象的死活斷定,哪些對象不會再被引用?有兩種實現方式:引用計數法和可達性分析。
以上4種很是好理解,是重點,須要熟記於心,由於上面4種對象是在方法運行時或常量引用的對象,在對應的生命週期是確定不能被GC回收的,做爲GC Roots天然再合適不過。另外還有下面幾種能夠做爲了解:
除了堆中對象須要回收,方法區中的class對象也是能夠被回收的,可是回收的條件很是苛刻:
能夠看到方法區的回收條件是多麼苛刻,因此方法區的回收率通常極低,所以能夠經過-Xnoclassgc關閉方法區的回收,提高GC效率,但須要注意,關閉後將會致使方法區的內存永久被佔用,致使OOM出現。
經過上文咱們能夠發現,對象的存活斷定都是基於引用,而Java中引用又分爲了4種:
虛擬機提供了一次自我拯救的機會給對象,即finalize方法。若是對象覆蓋了該方法,當通過可達性分析後,就會進行一次判斷,判斷該對象是否有必要執行finalize方法,若是對象沒有覆蓋該方法或者已經執行過一次該方法都會斷定爲該對象沒有必要執行finalize方法,在GC時被回收。不然就會將該對象放入到一個叫F-Queue的隊列中,以後GC會對該隊列的對象進行二次標記,即調用該方法,若是咱們要讓該對象復活,那麼就只須要在finalize方法中將該對象從新與GC Roots關聯上便可。
該方法是虛擬機提供給對象復活的惟一機會,可是該方法做用極小,由於使用不慎可能會致使系統崩潰,另外因爲它的運行優先級也很是低,經常須要主線程等待它的執行,致使系統性能大大下降,因此基本上能夠忘記該方法的存在了。
上文說到對象是在堆中分配內存的,可是堆中也是分爲新生代和老年代的,新生代中又分了Eden、from、survivor區,那麼對象具體會分配到哪一個區呢?這涉及到對象的分配規則,下面一一說明。
大多數狀況,對象直接在Eden區中分配內存,當Eden區內存不足時,就會進行一次MinorGC(新生代垃圾回收,能夠經過-XX:+PrintGCDetails這個參數打印GC日誌信息)。
什麼是大對象?虛擬機提供了一個參數:-XX:PretenureSizeThreshold,當對象大小大於該值時,該對象就會直接被分配到老年代中(該參數只對Serial和ParNew垃圾收集器有效)。爲何不分配到新生代中呢?由於在新生代中每一次MinroGC都會致使對象在Eden、from和sruvivor中複製,若是存在不少這樣的大對象,那麼新生代的GC和複製效率就會極低(關於垃GC的內容後面的文章會詳細講解)。
既然對象優先在新生代中分配,那麼何時會進入到老年代呢?這就和上文講解的對象頭中的分代年齡有關了,默認狀況下超過15歲就會進入老年代,能夠經過-XX:MaxTenuringThreshold參數進行設置。那歲數又是怎麼增加的呢?每當對象熬過一次MiniorGC後年齡都會增長1歲。
可是虛擬機並非要求對象年齡必須達到MaxTenuringThreshold才能晉升老年代,當Survivor空間中相同年齡的全部對象的大小總和大於Survivor空間一半時,年齡大於或等於該年齡的對象就會直接晉升到老年代。
在發生MiniorGC以前,虛擬機首先會檢查老年代中最大可用的連續空間是否大於新生代全部對象的總和,若是大於則進行一次MiniorGC;不然,則會檢查HandlePromotionFailure設置值是否容許擔保失敗。若是容許則會檢查老年代最大連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於則進行一次MiniorGC,不然則進行一次FullGC。
爲何要這麼設計呢?由於頻繁的FullGC會致使性能大大下降,而取歷次晉升老年代對象的平均大小確定也不是百分百有效,由於存在對象忽然大大增長的狀況,這個時候就會出現擔保失敗的狀況,也會致使FullGC。須要注意的是HandlePromotionFailure這個參數在JDK6Update24後就不會再影響到虛擬機的空間分配擔保策略了,即默認老年代的連續空間大於新生代對象的總大小或歷次晉升的平均大小就會進行MinorGC,不然進行FullGC。
本文概念性的東西很是多,這是學習JVM的難點和基礎,但這是繞不開的一道坎,讀者只有多看,多思考,寫代碼復現文中提到的概念,才能真正的理解這些基礎知識。另外還有垃圾是怎麼回收的?有哪些垃圾回收器?怎麼選擇?這些問題將在下一篇進行解答。