03-JVM内存分配机制详解
文章目錄
- 一、對象的創(chuàng)建流程
- 1、類加載檢查
- 2、分配內存
- 3、初始化
- 4、設置對象頭
- 5、執(zhí)行<inti>方法
- 二、對象內存分配
- 1、棧上分配
- 2、對象在Eden區(qū)分配
- 3、大對象直接進入老年代
- 4、長期存活的對象進入老年代
- 5、對象動態(tài)年齡判斷
- 6、老年代空間分配擔保機制
- 三、對象內存回收
- 1、引用計數(shù)法
- 2、可達性分析算法
一、對象的創(chuàng)建流程
1、類加載檢查
??虛擬機遇到一條new指令時,首先去檢查這個指令的參數(shù)能否在常量池中找到一個符號引用,并且檢查這個符號引用代表的類是否已加載。若沒有加載,則先執(zhí)行類加載過程。
??new指令對應到語言層面上是指,new關鍵字、克隆對象、對象序列化等。
2、分配內存
??類加載檢查通過后,接下來就是為對象分配內存。對象所需內存的大小在類加載期間便可以確定。為對象分配空間就是在堆內存中劃分一塊確定大小的內存,這個過程會出現(xiàn)兩個問題。
??如何劃分內存?
??怎么保證并發(fā)安全?
劃分內存:
- “指針碰撞”(Bump the Pointer)(默認用指針碰撞)
如果Java內存是覺得規(guī)整的,所有使用過的內存放在一邊,未使用的在另一邊,中間放著一個指針作為分界點,那么分配內存就是把指針往未使用區(qū)域挪動待分配對象大小的距離。 - “空閑列表”(Free List)
如果Java內存不規(guī)整,已使用和未使用的內存相互交錯。這種情況下就不能使用指針碰撞,虛擬機就需要維護一份列表,記錄哪些內存是可以使用的,給對象分配內存的時候,從列表中取出一塊足夠大的空間。
保證并發(fā)安全:
- CAS
虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性來對分配內存空間的動作進行同步處理 - 本地線程分配緩存(Thread Local Allocation Buffer,TLAB)
把分配內存的動作按照線程劃分在不同的空間中進行,即每個線程在Java堆中預先分配一塊內存。通過-XX:+/- UseTLAB參數(shù)來設定虛擬機是否使用TLAB(JVM會默認開啟-XX:+UseTLAB),-XX:TLABSize 指定TLAB大小。
3、初始化
??內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),如果使用TLAB,這一過程也可以提前至TLAB分配時進行。這一步保證了對象的實例在Java代碼中可以不賦初值的情況下就可以直接使用,程序能訪問到這些字段的數(shù)據(jù)類型對應的零值。
4、設置對象頭
??初始化后,虛擬機要對對象進行必要的設置。例如,這個對象是哪個類的實例,怎么找到類元信息,對象的哈希碼、GC的分代年齡等信息,這些都在對象頭中。
??在HotSpot虛擬機中,對象在內存中的存儲布局可以分為3塊區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance Data)、對齊填充(Padding)
-
對象頭(Header)
??對象頭包括兩部分信息,第一部分Mark Word標記字段(32位占4字節(jié),64位占8字節(jié)),用于存儲對象自身的運行時數(shù)據(jù), 如哈 希碼(HashCode)、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID、偏向時 間戳等。
??對象頭的另外一部分 是類型指針Klass Pointer(開啟指針壓縮占4字節(jié),關閉占8字節(jié)),即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
??另外,數(shù)組對象還會有數(shù)組長度(占4字節(jié)),數(shù)組最大容量2^32-1。- 32位對象頭
- 64位對象頭
- 32位對象頭
5、執(zhí)行方法
??執(zhí)行方法,即對象按照程序員的意愿進行初始化。對應到語言層面上講,就是為屬性賦值(注意,這與上面的賦 零值不同,這是由程序員賦的值),和執(zhí)行構造方法。
二、對象內存分配
- 棧上分配
- Eden區(qū)分配
- 大對象直接進入老年代
- 長期存活的對象進入老年代
- 對象動態(tài)年齡判斷機制
- 老年代空間分配擔保機制
1、棧上分配
??一般來說Java對象都是在堆上分配的,當對象沒有引用時,需要GC進行回收,如果對象數(shù)量較多,回收會有一定的壓力,也間接影響性能。為了減少臨時對象在堆內存的分配數(shù)量,JVM通過逃逸分析確定該對象會不會被外部訪問。如果能確定對象不會逃逸,就可以將對象分配到棧空間上,這樣對象所占用的內存會隨著棧幀出棧而釋放,減輕了垃圾回收的壓力。
- 逃逸分析
分析對象動態(tài)作用域,當一個對象在方法中定義后,可能會被外部引用,例如作為參數(shù)傳遞到其他方法中,那么該對象的作用域范圍不確定。如果一個對象只在本方法內使用,當方法結束后,這個對象就是無效的了,這樣的對象可以將其分配到棧空間里,讓其在方法結束時跟隨棧內存一起被回收掉。
JVM對于這種情況可以通過開啟逃逸分析參數(shù)(-XX:+DoEscapeAnalysis)來優(yōu)化對象內存分配位置,使其通過標量替換優(yōu)先分配在棧上(棧上分配),JDK7之后默認開啟逃逸分析,如果要關閉使用參數(shù)(-XX:-DoEscapeAnalysis) - 標量替換
通過逃逸分析確定該對象不會被外部訪問,并且對象可以被進一步分解時,JVM不會創(chuàng)建該對象,而是將該 對象成員變量分解若干個被這個方法使用的成員變量所代替,這些代替的成員變量在棧幀或寄存器上分配空間,這樣就 不會因為沒有一大塊連續(xù)空間導致對象內存不夠分配。開啟標量替換參數(shù)(-XX:+EliminateAllocations),JDK7之后默認 開啟。 - 標量與聚合量
標量即不可被進一步分解的量,而JAVA的基本數(shù)據(jù)類型就是標量(如:int,long等基本數(shù)據(jù)類型以及 reference類型等),標量的對立就是可以被進一步分解的量,而這種量稱之為聚合量。而在JAVA中對象就是可以被進一 步分解的聚合量。
結論:棧上分配依賴于逃逸分析和標量替換
2、對象在Eden區(qū)分配
??大多數(shù)情況下,對象都是在Eden區(qū)分配的,當Eden區(qū)沒有足夠的空間時,會進行一次MinorGC。
- MinorGC/YoungGC:指發(fā)送在年輕代的垃圾回收動作,MinorGC回收次數(shù)頻繁,速度很快。
- MajorGC/FullGC:回收年輕代、方法區(qū)、老年代的垃圾,速度很慢,相比MInorGC,要慢上10倍左右。
??Eden區(qū)與Survivor區(qū)默認為8:1:1
??大量對象創(chuàng)建在Eden區(qū),等Eden區(qū)滿了后,會觸發(fā)Minor GC,其中99%的對象會被回收掉,剩余存活的對象會進入到一塊有空間的Survivor區(qū)。等到下次Eden區(qū)滿時,再次發(fā)生Minor GC,把Eden區(qū)和Survivor區(qū)中的垃圾對象回收,剩余存活的對象會一起進入另一塊Survivor區(qū)。
??年輕代中的大多數(shù)對象的存活時間很短,可以說是朝生夕死,所以JVM中默認8:1:1的比例還是比較合理的,實際應用中讓Eden區(qū)足夠大,Survivor區(qū)夠用即可。
??JVM中的參數(shù)-XX:+UseAdaptiveSizePolicy(默認開啟)會導致8:1:1比例自動變化,如果不想這個比例有變化可以設置參數(shù)-XX:-UseAdaptiveSizePolicy。
新建對象Eden區(qū)分配示例
/*** 添加運行JVM參數(shù): -XX:+PrintGCDetails*/ public class GCTest {public static void main(String[] args) {byte[] allocation1;// 60000Kallocation1 = new byte[60000*1024];} } 運行結果: HeapPSYoungGen total 76288K, used 65536K [0x000000076b100000, 0x0000000770600000, 0x00000007c0000000)eden space 65536K, 100% used [0x000000076b100000,0x000000076f100000,0x000000076f100000)from space 10752K, 0% used [0x000000076fb80000,0x000000076fb80000,0x0000000770600000)to space 10752K, 0% used [0x000000076f100000,0x000000076f100000,0x000000076fb80000)ParOldGen total 175104K, used 0K [0x00000006c1200000, 0x00000006cbd00000, 0x000000076b100000)object space 175104K, 0% used [0x00000006c1200000,0x00000006c1200000,0x00000006cbd00000)Metaspace used 3221K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 350K, capacity 388K, committed 512K, reserved 1048576K??此時可以看到Eden區(qū)已經被填滿(程序運行時,即使什么都不做,新生代也會有幾M內存使用),此時再分配一個對象時,內存會怎么樣?
Eden區(qū)滿時再分配
public class GCTest {public static void main(String[] args) {byte[] allocation1, allocation2;// 60000Kallocation1 = new byte[60000*1024];// 8000Kallocation2 = new byte[8000*1024];} } 運行結果: [GC (Allocation Failure) [PSYoungGen: 65244K->824K(76288K)] 65244K->60832K(251392K), 0.0293285 secs] [Times: user=0.17 sys=0.03, real=0.03 secs] HeapPSYoungGen total 76288K, used 9479K [0x000000076b100000, 0x0000000774600000, 0x00000007c0000000)eden space 65536K, 13% used [0x000000076b100000,0x000000076b973ef8,0x000000076f100000)from space 10752K, 7% used [0x000000076f100000,0x000000076f1ce030,0x000000076fb80000)to space 10752K, 0% used [0x0000000773b80000,0x0000000773b80000,0x0000000774600000)ParOldGen total 175104K, used 60008K [0x00000006c1200000, 0x00000006cbd00000, 0x000000076b100000)object space 175104K, 34% used [0x00000006c1200000,0x00000006c4c9a010,0x00000006cbd00000)Metaspace used 3221K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 350K, capacity 388K, committed 512K, reserved 1048576K??分配allocation1時,Eden區(qū)被填滿,此時分配allocation2,Eden區(qū)放不下,所以產生一次MinorGC。GC后allocation1還是存活對象,按理來說應該會進入Survivor區(qū),但是Survivor區(qū)放不下(space 10752K),所以allocation1提前進入老年代,老年代的空間足夠放下allocation1,所以不會產生Full GC。
??執(zhí)行玩MInor GC后,Eden區(qū)還有足夠的空間,后面產生的對象還是會繼續(xù)分配到Eden區(qū)。
Minor GC后Eden區(qū)繼續(xù)分配示例
public class GCTest {public static void main(String[] args) {byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6;// 60000Kallocation1 = new byte[60000*1024];// 8000Kallocation2 = new byte[8000*1024];// 4*1000kallocation3 = new byte[1000*1024];allocation4 = new byte[1000*1024];allocation5 = new byte[1000*1024];allocation6 = new byte[1000*1024];}} 運行結果: [GC (Allocation Failure) [PSYoungGen: 65244K->872K(76288K)] 65244K->60880K(251392K), 0.0347016 secs] [Times: user=0.02 sys=0.00, real=0.04 secs] HeapPSYoungGen total 76288K, used 13799K [0x000000076b100000, 0x0000000774600000, 0x00000007c0000000)eden space 65536K, 19% used [0x000000076b100000,0x000000076bd9fbe8,0x000000076f100000)from space 10752K, 8% used [0x000000076f100000,0x000000076f1da020,0x000000076fb80000)to space 10752K, 0% used [0x0000000773b80000,0x0000000773b80000,0x0000000774600000)ParOldGen total 175104K, used 60008K [0x00000006c1200000, 0x00000006cbd00000, 0x000000076b100000)object space 175104K, 34% used [0x00000006c1200000,0x00000006c4c9a010,0x00000006cbd00000)Metaspace used 3221K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 350K, capacity 388K, committed 512K, reserved 1048576KProcess finished with exit code 0??可以看到后續(xù)創(chuàng)建的對象繼續(xù)在Eden區(qū)分配。
運行結果解釋
- GC (Allocation Failure)
- 表示這是一次YGC,括號里面表示GC的原因,否則就是FGC
- PSYoungGen total 76288K, used 13799K
年輕代總共76288K大小,已使用13799K - eden space 65536K, 19% used
Eden區(qū)大小和已使用百分比 - from space 10752K, 8% used
survivor區(qū)空間大小和已使用百分比 - to space 10752K, 0% used
另一塊survivor區(qū) - ParOldGen total 175104K, used 60008K
老年代空間大小,和已使用空間
3、大對象直接進入老年代
??大對象就是需要連續(xù)使用內存空間的對象,如數(shù)組對象。JVM參數(shù) -XX:PretenureSizeThreshold=(單位字節(jié))可以設置大對象的大小,如果對象大小超過設置的參數(shù),則直接進入老年代。這個參數(shù)只在 Serial 和ParNew兩個收集器下有效。
??為了避免為大對象分配內存時的復制操作而降低效率。
4、長期存活的對象進入老年代
??JVM虛擬機采用了分代收集的思想來管理內存, 那么在進行內存回收時,就需要識別哪些對象放到年輕代,哪些放到老年代。為了做到這一點,虛擬機給每個對象設置了一個對象年齡(Age)計數(shù)器。
??如果一個對象經歷了Minor GC還能存活,且Survivor區(qū)能夠放得下它,那么該對象的年齡就記為1。在Survivor區(qū)中,每經歷一次Minor GC還能存活的話,年齡就會加1。當對象的年齡增加到一定程度時(默認為15歲,CMS收集器默認6歲,不同的垃圾收集器會略微有點不同),就會進入到老年代中。對象進入到老年代的年齡閾值,可以通過參數(shù) -XX:MaxTenuringThreshold 來設置。
??通過上文的對象頭可以了解到,一個對象的年齡占2位,所以一個對象的最大年齡位2^4-1=15。
5、對象動態(tài)年齡判斷
??當前的Survivor區(qū)域里,有一批不同年齡的對象,從最小年齡開始,年齡1+年齡2+年齡n,他們的大小總和超過該區(qū)域總大小的50%(-XX:TargetSurvivorRatio可以指定),那么年齡大于等于n的對象都要進入到老年代中區(qū)。
??這個規(guī)則的目的是希望可能長期存活的對象盡早進入老年代。
??對象動態(tài)年齡判斷一般是在Minor GC后觸發(fā)的
6、老年代空間分配擔保機制
??JVM有這么一個參數(shù):-XX:-HandlePromotionFailure(1.8默認設置)
??年輕代每次GC前都,JVM都會計算老年代剩余可用空間,如果這個剩余空間小于年輕代里所有對象大小之和(包括垃圾對象),那么JVM就會看是否設置前面這個參數(shù)。如果設置這個參數(shù),且老年代剩余空間是否小于之前每一次MInorGC后進入老年代對象的平均大小。
??如果沒設置參數(shù),或者小于平均大小,會先觸發(fā)一次FullGC,將老年代和年輕代的垃圾對象一起回收掉,如果回收后還是沒有空間存放對象,則會發(fā)生OOM。
三、對象內存回收
??堆中存放著幾乎所有對象的實例,對堆進行垃圾回收,首先就要判斷哪些對象已死亡。判斷對象是否為垃圾對象有兩種方法:引用計數(shù)法、可達性分析算法
1、引用計數(shù)法
??給對象添加一個計數(shù)器,每當有一個對象引用它時,計數(shù)器就加1,當引用失效,計數(shù)器減1,當計數(shù)器歸0時,就表示這個對象沒有任何引用,可以回收。
??這個方法在目前主流的虛擬機中并沒有使用,主要還是互相引用的問題不好解決。
2、可達性分析算法
??將GC Root對象作為起點,從這些起點開始鄉(xiāng)下搜索,找得到的對象都標記為非垃圾對象,其余未標記的對象都需要進行回收。
- GC Root根節(jié)點:線程棧的本地變量、靜態(tài)變量、本地方法棧的變量等
總結
以上是生活随笔為你收集整理的03-JVM内存分配机制详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用CSS画基本图形
- 下一篇: 【转】oracle数据库中varchar