三分鐘帶你秒懂對象的內(nèi)存分配流程
01、背景介紹
本篇綜合之前的知識,重點介紹一下對象的內(nèi)存分配流程。
02、對象的內(nèi)存分配原則
在之前的 JVM 內(nèi)存結(jié)構(gòu)布局的文章中,我們介紹到了 Java 堆的內(nèi)存布局,由 年輕代 (Young Generation) 和老年代 (Old Generation) 組成,默認情況下按照1 : 2的比例來分配空間。
其中年輕代又被劃分為三個不同的區(qū)域:Eden 區(qū)、From Survivor 區(qū)、To Survivor 區(qū),默認情況下按照8 : 1 : 1的比例來分配空間。
Java 堆的內(nèi)存布局,可以用如下圖來概括。
圖片
當創(chuàng)建的對象不再被使用了是需要被回收掉的,以便騰出空間給新的對象使用,這就是對象的垃圾回收,也就是對象的 GC,我們會在后續(xù)的文章中再次介紹對象的垃圾回收算法以及垃圾收集器。
本次我們重點介紹下,創(chuàng)建不同大小的對象,在堆空間中發(fā)生的內(nèi)存分配變化,以便后續(xù)更好的理解 GC 調(diào)優(yōu)過程。
2.1、對象優(yōu)先分配在 Eden 區(qū)
默認情況下,創(chuàng)建的對象會優(yōu)先分配在年輕代的 Eden 區(qū),當 Eden 區(qū)不夠用的時候,會觸發(fā)一次 Minor GC。
什么是 Minor GC 呢?
Minor GC 指的是 JVM 發(fā)生在年輕代的垃圾回收動作,效率高、速度快,但是只清除年輕代的垃圾對象。
與之對應的還有 Major GC 和 Full GC,Major GC 指的是 JVM 發(fā)生在老年代的垃圾回收動作,Major GC 的速度一般要比 Minor GC 慢 10 倍以上;同時,許多 Major GC 是由 Minor GC 引起的,因此把這個過程也稱之為 Full GC,也就是對整個堆進行垃圾回收。
當 Eden 區(qū)滿了以后,會發(fā)生 Minor GC,存活下來的對象會被移動到 Survivor 區(qū),如果 Survivor 區(qū)裝不下這批對象,此時會直接移動到老年代。
通常年輕代的對象存活時間都很短,在 Minor GC 后,大部分的對象都會被回收掉,但是也不排除個例情況,存活下來的對象的年齡會進行 +1,當年齡達到 15歲時,也會被移動到老年代。
用戶可以通過-XX:MaxTenuringThreshold參數(shù)來調(diào)整年齡的閥值,默認是 15,最大值也是 15。
2.2、大對象直接進入老年代
所謂大對象,顧名思義就是占用內(nèi)存比較多的對象,大對象一般可以直接分配到老年代,這是 JVM 的一種優(yōu)化設計。
用戶可以手動通過-XX:PretenureSizeThreshold參數(shù)設置大對象的大小,默認是 0,意味著任何對象都會優(yōu)先在年輕代的 Eden 區(qū)分配內(nèi)存。
試想一下,假如大對象優(yōu)先在 Eden 區(qū)中分配,給其它的對象預留的空間就會變小,此時很容易觸發(fā) Minor GC,經(jīng)過多次 GC 之后,大對象可能會繼續(xù)存活,最終還是會被轉(zhuǎn)移到老年代。
與其如此,還不如直接分配到老年代。
2.3、對象動態(tài)年齡判斷機制
對象動態(tài)年齡判斷,簡單的說就是對 Survivor 區(qū)的對象年齡從小到大進行累加,當累加到 X 年齡(某個年齡)時占用空間的總和大于 50%,那么比 X 年齡大的對象都會移動到老年代。
這種機制是 JVM 的一個預測機制,虛擬機并不是完全要求對象年齡必須達到 15 才能移動到老年代。當 survivor 區(qū)快要滿了并且存在一批可能會長期存活的對象,那不如提前進入老年代,減少年輕代的壓力。
用戶可以使用-XX:TargetSurvivorRatio參數(shù)來設置保留多少空閑空間,默認值是 50。
2.4、逃逸分析
逃逸分析是一項比較前沿的優(yōu)化技術(shù),它并不是直接優(yōu)化代碼的手段,而是為其它優(yōu)化手段提供了分析技術(shù)。
什么是逃逸分析呢?
當一個對象在方法里面被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他方法中,這種稱為方法逃逸;甚至還有可能被外部線程訪問到,譬如賦值給可以在其他線程中訪問的實例變量,這種稱為線程逃逸。
如果能證明一個對象不會逃移到方法外或者線程之外,換句話說就是別的方法或線程無法通過任何途徑訪問到這個對象,虛擬機可以通過一些途徑為這個變量進行一些不同程度的優(yōu)化。
比如棧上分配、同步消除、標量替換等優(yōu)化操作。
2.4.1、棧上分配
在上文我們提及到,對象會優(yōu)先在堆上分配,垃圾收集器會定期回收堆內(nèi)存中不再使用的對象,但這塊的內(nèi)存回收很耗時。
如果確定一個對象不會逃逸出方法之外,讓這個對象在棧上分配,對象所占用的內(nèi)存空間就可以隨著棧幀出棧而銷毀,這樣垃圾收集器的壓力將會小很多。
2.4.2、同步消除
線程同步本身是一個相對耗時的操作,如果逃逸分析能夠確定一個變量不會逃逸出線程,無法被其他線程訪問,那么這個變量的讀寫肯定就不會有競爭,此時虛擬機會對這個變量,實施的同步措施進行消除,比如去掉同步鎖來運行方法。
2.4.3、標量替換
標量是指一個數(shù)據(jù)已經(jīng)無法再分解成更小的數(shù)據(jù)來表示了,比如 Java 虛擬機中的原始數(shù)據(jù)類型(int,long 等數(shù)值類型以及 reference 類型)等都不能進一步分解,它們可以稱為標量。相對的,如果一個數(shù)據(jù)可以繼續(xù)分解,那它稱為聚合量。
Java 中最典型的聚合量是對象,如果逃逸分析證明一個對象不會被外部訪問,并且這個對象是可分解的,那程序真正執(zhí)行的時候?qū)⒖赡懿粍?chuàng)建這個對象,而改為直接創(chuàng)建它的若干個被這個方法使用到的成員變量來代替,拆散后的變量便可以被單獨分析與優(yōu)化,可以各自分別在棧幀或寄存器上分配空間,原本的對象就無需整體分配空間了。
默認情況下逃逸分析是關閉的,用戶可以使用-XX:+DoEscapeAnalysis參數(shù)來手動開啟逃逸分析。
03、小結(jié)
綜合以上的內(nèi)容,對象的內(nèi)存分配流程,可以用如下圖來概括。
圖片
由此可知,對象在內(nèi)存分配的時候,會根據(jù)不同情況來判斷合理分配,以便 JVM 更快捷的進行回收資源。
04、參考
1.https://zhuanlan.zhihu.com/p/267223891
2.https://zhuanlan.zhihu.com/p/401057707
3.https://www.cnblogs.com/xrq730/p/4827590.html
4.https://www.jianshu.com/p/3d38cba67f8b
6.https://blog.csdn.net/clover_lily/article/details/80095580
7.https://blog.csdn.net/FIRE_TRAY/article/details/51275788
8.https://blog.csdn.net/yb970521/article/details/108015984