一個JVM實例只存在一個堆內存,堆也是java內存管理的核心區域
Java堆區在jvm啓動的時候被建立,其空間大小也就肯定了。是jvm管理的最大一塊內存空間。(堆內存的大小能夠調節)
《java虛擬機規範》規定,堆能夠處於物理上不連續的內存空間中,但在邏輯上它應該被視爲連續的
全部的線程共享java堆,在這裏還能夠劃分線程私有的緩衝區
《Java虛擬機規範》中對Java堆的描述是:全部對象實例以及數組都應該運行時分配在堆上
數組和對象可能永遠不會存儲在棧上,由於棧幀中保存引用,這個引用指向對象或數組在對中的位置
在方法結束後,堆中對象不會立刻移除,僅僅在垃圾收集的時候纔會被移除
堆是GC(Garbage Collection)執行垃圾回收的重點區域java
現代垃圾收集器大部分都基於分代收集理論設計,堆空間分爲:算法
java7以前堆內存邏輯上分爲三部分:新生區+養老區+永久區
java8以後堆內存邏輯上分爲三部分:新生區+養老區+元空間數組
設置堆空間大小的參數
-Xms 用來設置堆空間(年輕代+老年代)的初始內存大小
-X 是jvm的運行參數
ms 是memory start
-Xmx 用來設置堆空間(年輕代+老年代)的最大內存大小
默認堆空間的大小
初始內存大小:物理電腦內存大小 / 64
最大內存大小:物理電腦內存大小 / 4
手動設置:-Xms600m -Xmx600m
開發中建議將初始堆內存和最大的堆內存設置成相同的值。
查看設置的參數:方式一: jps / jstat -gc 進程id
方式二:-XX:+PrintGCDetails安全
public class OOMTest { public static void main(String[] args) { ArrayList<Picture> list = new ArrayList<>(); while(true){ try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } list.add(new Picture(new Random().nextInt(1024 * 1024))); } } } class Picture{ private byte[] pixels; public Picture(int length) { this.pixels = new byte[length]; } } //Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
存儲在JVM中的java對象能夠被劃分爲兩類:多線程
一類是生命週期較短的瞬時對象,這類對象的建立和消亡都很是迅速
另一類對象的生命週期卻很是短,在某些極端的狀況下還可以與JVM的生命週期保持一致
java堆區進一步細分的話,能夠分爲年輕代和老年代併發
其中年輕代又能夠劃分爲Eden空間,Survivor0和Survivor1空間(也叫from,to區)
dom
配置新生代與老年代在堆結構的佔比jvm
默認-XX:NewRation=2,表示新生代佔1,老年代佔2,新生代佔整個堆的1/3
在HotSpot中,Eden空間和另外兩個Survivor空間大小所佔比例爲8:1:1ide
能夠經過-XX:SurvivorRatio調整空間比例函數
幾乎全部的Java對象都是在Eden區被new出來的
絕大部分的Java對象的銷燬都在新生代進行了
能夠經過-Xmn設置新生代的最大內存大小
爲新對象分配內存是一件很是嚴謹和複雜的任務,JVM的設計者們不只須要考慮內存如何分配,在哪裏分配等問題,而且因爲內存分配算法與內存回收算法密切相關,因此還須要考慮GC執行完內存回收後是否會在內存空間中產生內存碎片
new的對象先伊甸園(Eden)。此區有大小限制
當伊甸園的空間填滿時,程序有須要建立對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中的不在被其餘對象所引用的對象進行銷燬。在加載新的對象放到伊甸園區
而後將伊甸園中的剩餘對象移動到倖存者0區
若是再次觸發垃圾回收,此時會從新放回倖存者0區,接着再去倖存者1區
何時進入養老區?能夠設置次數。默認爲15
能夠設置參數:-XX:MaxTenuringThreshold=進行設置
survivor區滿了不會進行垃圾回收,而是在伊甸園區滿了以後垃圾回收算法對伊甸園區進行回收的同時,survivor區會被動的進行垃圾回收
針對倖存者S0,S1區的總結:複製以後有交換,誰空誰是to
關於垃圾回收:頻繁在新生區收集,不多在養老區收集,幾乎不在永久區/元空間收集
JVM在進行GC時,並不是每次都對上面三個內存(新生代,老年代,方法區)區域一塊兒回收的,大部分的時候回收都是指新生代
針對Hotspot VM的實現,它裏面的GC按照回收區域又分爲兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(FULL GC)
部分收集:不是完整收集java堆的垃圾收集。其中又分爲:
新生代收集(Minor GC/Young GC):只是新生代(Eden/S0,S1)的垃圾收集
老年代收集(Major GC/Old GC):只是老年代的垃圾收集
只有CMS GC會有單獨收集老年代的行爲
注意:不少時候Major GC會和FULL GC混淆使用,須要具體分辨是老年代回收,仍是整堆回收
混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集
目前只有G1 GC會有這種行爲
整堆收集(FULL GC):收集整個java堆和方法區的垃圾收集
年輕代GC(Minor GC)觸發機制:
當年輕代空間不足時,就會觸發Minor GC,這裏的年輕代滿指的是Eden區滿,Survivor滿不會引起GC(每次Minor GC會清理年輕代的內存)
由於Java對象大多都是朝生熄滅的特徵,因此Minor GC很是頻繁,通常回收速度比較快。
Minor GC會引起STW,暫停其餘用戶的線程,等垃圾回收結束,用戶線程才恢復運行
老年代GC (Major GC/Full GC)觸發機制:
指發生在老 年代的GC,對象從老年代消失時,咱們說「Major GC」 或「Fu1l GC」發生了。
出現了Major GC,常常會伴隨至少一.次的Minor GC (但非絕對的,在ParallelScavenge收集器的收集策略裏就有直接進行MajorGC的策略選擇過程)
也就是在老年代空間不足時,會先嚐試觸發Minor GC。 若是以後空間還不足,則觸發Major GC
Major GC的速度通常會比Minor Gc慢10倍以上,STW的時間更長。
若是Major GC後,內存還不足,就報00M了。
Fu11 GC觸發機制
觸發Fu1l GC執行的狀況有以下五種:
調用System. gc()時,系統建議執行Full GC,可是沒必要然執行
老年代空間不足
方法區空間不足
經過Minor GC後進入老年代的平均大小大於老年代的可用內存
由Eden區、survivor space0 (From Space) 區向survivor space1 (ToSpace) 區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
說明: full gc是開發或調優中儘可能要避免的。這樣暫時時間會短一些。
爲何要把java堆分代?
經研究代表:不一樣對象的生命週期不一樣。70%-99%的對象都是臨時對象
新生代:有Eden,兩塊大小相同的Survivor(from/to或S0/S1)構成,其中to總爲空
不分代能正常工做嗎?
其實不分代徹底能夠,分代的惟一理由就是優化GC性能。若是沒有分代,那全部的對象都在一塊,就如同把一個學校的人都關在一個教室。GC的時候要找到哪些對象沒用這樣就會對堆的全部區域進行掃描。而不少對象都是朝生夕死的,若是分代的話,把新建立的對象放到某一地方,當GC的時候先把這塊存儲「朝生夕死」對象的區域進行回收,這樣就會騰出很大的空間出來。
若是對象在Eden出生並通過第一次MinorGC 後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,並將對象年齡設爲1。對象在Survivor區中每熬過一次MinorGC ,年齡就增長1 歲,當它的年齡增長到必定程度(默認爲15歲,其實每一個JVM、每一個GC都有所不一樣)時,就會被晉升到老年代中。對象晉升老年代的年齡閾值,能夠經過選項-XX : MaxTenuringThreshold來設置
針對不一樣年齡段的對象分配原則以下所示:
優先分配到Eden
大對象直接分配到老年代
儘可能避免程序中出現過多的大對象
長期存活的對象分配到老年代
動態對象年齡判斷
若是Survivor 區中相同年齡的全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象能夠直接進入老年代,無須等到MaxTenur ingThreshold中要求的年齡。
空間分配擔保
-XX:HandlePromotionFailure
堆區是線程共享的區域,任何線程均可以訪問到堆區中的共享數據
因爲對象實例的建立在JVM中很是頻繁,所以在併發環境下從堆區中劃份內存空間是線程不安全的
爲避免多個線程操做同一地址,須要使用加鎖等機制,進而影響分配速度
從內存模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM爲每一個線程分配了一個私有緩衝區,它包含在Eden空間內
多線程同時分配內存時,使用TLAB能夠避免一系列的非線程安全問題,同時還可以提高內存分配的吞吐量,所以咱們能夠將這種內存分配方式稱之爲快速分配策略
儘管不是全部的對象實例都可以在TLAB中成功分配內存,但JVM確實是將TLAB做爲內存分配的首選。
在程序中,開發人員能夠經過選項「-XX :UseTLAB」設置是否開啓TLAB空間。
默認狀況下,TLAB空間的內存很是小,佔有整個Eden空間的1%,固然咱們能夠經過選項「-XX:TLABWasteTargetPercent」設置TLAB空間所佔用Eden空間的百分比大小。
一旦對象在TLAB空間分配內存失敗時,JVM就會嘗試着經過使用加鎖機制確保數據操做的原子性,從而直接在Eden空間中分配內存。
測試堆空間經常使用的jvm參數:
-XX:+PrintFlagsInitial : 查看全部的參數的默認初始值
-XX:+PrintFlagsFinal :查看全部的參數的最終值(可能會存在修改,再也不是初始值)
具體查看某個參數的指令: jps:查看當前運行中的進程
jinfo -flag SurvivorRatio 進程id
-Xms:初始堆空間內存 (默認爲物理內存的1/64)
-Xmx:最大堆空間內存(默認爲物理內存的1/4)
-Xmn:設置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代與老年代在堆結構的佔比
-XX:SurvivorRatio:設置新生代中Eden和S0/S1空間的比例
-XX:MaxTenuringThreshold:設置新生代垃圾的最大年齡
-XX:+PrintGCDetails:輸出詳細的GC處理日誌
打印gc簡要信息:① -XX:+PrintGC ② -verbose:gc
-XX:HandlePromotionFailure:是否設置空間分配擔保
在發生MinorGC以前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代全部對象的總空間。
若是大於,則這次Minor GC是安全的
若是小於,則虛擬機會查看-XX: HandlePromotionFailure設置值是否容許擔保失敗。
若是HandlePromotionFailure=true, 那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小。
若是大於,則嘗試進行一次Minor GC,但此次Minor GC依然是有風險的;
若是小於,則改成進行一-次Full GC。
若是HandlePromotionFailure=false, 則改成進行一次Full GC。
在JDK6 Update24之 後,HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察OpenJDK中的源碼變化,雖然源碼中還定義了
HandlePromotionFailure參數,可是在代碼中已經不會再使用它。JDK6 Update24以後的規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。
在《深刻理解Java虛擬機》中關於Java堆內存有這樣一段描述:隨着JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配,標量替換優化技術將會致使一些微妙的變化,全部的對象都分配到堆上也漸漸變得不那麼絕對了
在JVM中,對象是在java堆中分配內存的,這是一個廣泛的常識,可是,有一種特殊的狀況,那就是若是通過逃逸分析(Escape Analysis)後發現,一個對象沒有逃逸出方法的話,那麼久可能被優化成棧上分配。這樣就無需在堆上分配內存,也無需進行;垃圾回收了。這也是最多見的堆外存儲技術。
此外,在基於OpenJdk深度指定的TaoBaoVm,其中創新的GCIH(GC invisible heap)技術實現off-heap,將生命週期較長的Java對象從heap中移至heap外,而且GC不能管理GCIH內部的java對象,以此達到下降GC的回收頻率和提高GC的回收率的目的
如何將堆上的對象分配到棧,須要使用逃逸分析手段
這是一種有效減小Java程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法
經過逃逸分析,Java HotSpot編譯器可以分析出一個新的對象引用的使用範圍從而決定是否要將這個對象分配到堆上
逃逸分析的基本行爲就是分析對象動態做用域
當一個對象在方法中被定義後,對象只在方法內部使用,則認爲沒有發生逃逸
當一個對象在方法中被定義後,被外部方法所引用,則認爲發生逃逸。例如做爲調用參數專遞到其餘地方中
在JDK 6u23版本以後,HotSpot中默認就已經開啓了逃逸分析。
若是使用的是較早的版本,開發人員則能夠經過:
選項「-XX: fDoEscapeAnalysis"顯式開啓逃逸分析
經過選項「-XX: +PrintEscapeAnalysis" 查看逃逸分析的篩選結果。
棧上分配。將堆分配轉化爲棧分配,若是一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象多是棧分配的候選,而不是堆分配
同步省略。若是一個對象被發現只能從一個線程被訪問到,那麼對於這個對象的操做能夠不考慮同步
分離對象或標量替換。有的對象可能不須要做爲一個連續的內存結構存在也能夠被訪問到,那麼對象的部分或所有能夠不存儲在內存,而是存儲在CPU的寄存器中
JIT編譯器在編譯期間根據逃逸分析的結果,發現若是一個對象並無逃逸出方法的話,就可能被優化成棧上分配。分配完成以後,繼續在調用棧內執行,最後線程結束,棧空間被回收,局部變量對象也被回收,這樣就無需進行垃圾回收了。
常見的棧上分配場景:給成員變量賦值,方法返回值,實例引用傳遞
線程同步的代價是至關高的,同步的後果是下降併發性和性能。
在動態編譯同步塊的時候,JIT編譯器能夠藉助逃逸分析來判斷同步塊所使用的鎖對象是否可以被同一個線程訪問而沒有被髮布到其餘線程。若是沒有,那麼JIT編譯器在編譯這個同步塊的時候就去取消對這部分代碼的同步功能,這樣就能大大提升併發性和性能,這個取消同步的過程就叫同步省略,也叫鎖消除
public void f(){ Object hollis = new Object(); synchronized(hollis){ System.out.print(hollis) } }
代碼中hollis這個對象進行加鎖,可是hollis對象的生命週期只在f()方法中,並不會被其餘線程所訪問,因此在JIT編譯階段就會被優化掉。優化成:
public void f(){ Object hollis = new Object(); System.out.print(hollis) }
標量(Scalar)是指一個沒法在分解成更小的數據的數據。Java中的原始數據類型就是標重.
相對的,那些還能夠分解的數據叫作聚合量(Aggregate),Java中的對象就是聚合量,由於他能夠分解成其餘聚合量和標量。
在JIT階段,若是通過逃逸分析,發現一個對象不會被外界訪問的話,那麼通過JIT優化,就會把這個對象拆解成若干個其中包含的若干個成員變量來代替。這個過程就是標量替換。
public static void main(String[] args) { alloc(); } private static void alloc(){ Point point = new Point(1,2); } class Point{ private int x; private int y; public Point(int x,int y){ this.x = x; this.y = y; } }
以上代碼通過標量替換後就會變成
private static void alloc(){ int x = 1; int y = 2; }
能夠看到,Point這個聚合量通過逃逸分析後,發現他並無逃逸,就被替換成兩個聚合量了。
標量替換的好處:能夠大大減小堆內存的佔用,由於一旦不須要建立了,那麼就不須要分配堆內存了
##標量替換參數
-XX:+EliminateAllocations:開啓了標量替換(默認打開),容許對象打散分配在棧上