這篇文章主要講Java內存的分配與回收機制,主要包括Java運行時的數據區域、對象的建立、垃圾收集算法與回收策略java
一.運行時數據區域
下圖是Java虛擬機運行時的內存示意圖:面試
從圖中咱們能夠看到Java內存總共分爲6個部分:算法
- 程序計數器:每條線程都有一個獨立的程序計數器,計數器能夠看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工做時,就是經過改變這個計數器的值來選取下一條所需執行的字節碼指令、分支、循環、跳轉、異常處理,線程恢復等基礎功能都須要依賴這個計數器完成。
- Java虛擬機棧:虛擬機棧是線程私有的,生命週期與線程相同。虛擬機棧爲Java方法執行描述內存模型,每一個方法在執行的同時會建立一個棧幀用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應一個棧幀在虛擬機棧中入棧到出棧的過程。
- 本地方法棧:與虛擬機棧發揮的做用類似。區別是虛擬機棧爲執行Java方法服務,本地方法棧爲Native方法服務。
- 堆:全部線程共享的區域。在虛擬機啓動時建立,全部的對象實例幾乎都在堆上分配。Java堆還能夠細分爲:新生代和老年代,再細緻一點有Eden空間、From Survivor空間、To Survivor空間。不過不管如何劃分,存儲的都是對象實例,進一步劃分的目的是爲了更好的回收內存,或者更快的分配內存。
- 方法區:方法區是各個線程共享的內存區域,主要用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據。這塊區域與Java堆同樣不須要連續的內存和能夠選擇固定大小或可擴展外,還能夠選擇不實現垃圾收集。這區域的內存回收目標主要是針對常量池的回收和對類型的卸載,垃圾收集行爲在這個區域較少出現。
- 運行時常量池:運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各類字面符和符號引用,這部份內容在類加載後進入方法區的運行時常量池中存放。
- 直接內存:直接內存也稱堆外內存,它不是虛擬機運行時數據區的一部分。JDK1.4後引入NIO類,是一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可使用Native函數庫直接在堆外分配內存,而後經過存儲在Java堆中的DirectByteBuffer對象做爲引用對這塊內存進行操做。這樣可以顯著提升性能,避免Java堆和Native堆中來回複製數據。
因此經過表格的形式歸納以下:數組
數據區域 歸納 線程共享 程序計數器 當前線程所執行的字節碼的行號指示器 否 虛擬機棧 爲Java方法執行建立棧幀存儲局部變量、操做數棧、動態連接、方法出口等信息 否 本地方法棧 與虛擬機棧相似,爲Native方法服務 否 堆 存放對象實例 是 方法區 存儲虛擬機已加載的類信息、常量、靜態變量、即時編譯後的代碼等數據 是 運行時常量池 方法區的一部分,存放編譯期生成的字面量和符號引用 是 直接內存 被分配在堆外的內存,性能高,不受Java堆的大小限制 是 二.對象的建立與內存佈局安全
1.對象的建立性能優化
Java對象的建立架構
上圖是對象建立的完整流程圖,接下來作詳細說明。併發
- 當虛擬機收到new指令後,檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用所表明的類是否已被加載、解析和初始化過。若是沒有,必須先執行類加載過程。
- 在類加載完成後能夠肯定對象分配所須要的空間。若是Java堆中內存是絕對規整的,用過的內存放一邊,空閒的內存放另外一邊,中間放着一個指針做爲分界點的指示器,那分配內存就只是把指針向空閒空間方向挪動一段與對象大小相等的距離,這種分配方式稱爲"指針碰撞"。若是Java堆中內存不是規整的,空閒內存與使用過的內存是相互交錯的,虛擬機必須維護一個列表,記錄哪些內存塊是可用的,在分配的時候從列表中找出足夠的空間分配給對象實例,並更新列表上的記錄,這種分配方式稱爲"空閒列表"。採用哪一種分配方式一般由虛擬機的垃圾收集器是否帶有壓縮整理功能決定。
- 劃分可用空間時,還需考慮爲對象實例分配空間時是不是線程安全的。要保證線程安全,有兩種方案。一種是對分配內存空間的動做進行同步處理,實際上虛擬機採用CAS配上失敗重試的方式保證更新操做的原子性。另外一種是把內存分配的動做按照線程劃分在不一樣空間中進行,每一個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer , TLAB)。哪一個線程要分配內存,就在哪一個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時,才須要同步鎖定。
- 內存分配完成後,虛擬機對分配到的內存空間都初始化爲零值(不包括對象頭),保證對象的實例字段在Java代碼中能夠不賦初始值就能夠直接使用。
- 虛擬機將對象的信息放入對象的對象頭中。
- 執行構造函數
2.對象的內存佈局分佈式
對象的內存佈局總共分爲三個部分:函數
- 對象頭中主要包括兩部分信息:
- 一部分用於存儲對象自身的運行時數據,如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。
- 另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。若是對象是Java數組,那在對象頭中還必須有一塊記錄數組長的數據。
- 實例數據部分是對象真正存儲的有效信息,也是程序代碼中定義的各類類型的字段內容。從父類繼承下來的,在子類中定義的都須要記錄下來。
- 對齊填充僅僅起到佔位符的做用。HotSpot VM的自動內存管理系統要求對象起始地址是8字節的整數倍,因此對象大小必須是8字節的整數倍。當對象實例數據部分沒有對齊時,須要經過對齊填充來補
在此我向你們推薦一個Java高級羣 :725633148 裏面會分享一些資深架構師錄製的視頻錄像:(有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構、面試資料)等這些成爲架構師必備的知識體系 進羣立刻免費領取,目前受益良多!
三.內存的回收
1.對象存活斷定
Java虛擬機經過可達性分析來斷定對象是否存活。這個算法的基本思想是經過一系列稱爲"GC Roots"的對象做爲起始點,從這些節點向下搜索,搜索走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有與任何引用鏈相連時,則該對象是不可用的。
如圖,object5,object6,object7雖然互有關聯,可是GC Roots是不可達的,因此它們被斷定是可回收的對象。
另外值得一提的是引用計數算法,引用計數法是經過給對象一個引用計數器,每當有一個地方引用它時,計數器值就加一;引用失效時,計數器值就減一;任什麼時候刻計數器爲0的對象就是不可能再被使用的。引用計數器效率高、實現簡單。可是很難解決對象間相互循環引用的問題,主流Java虛擬機幾乎都再也不使用引用計數法來管理內存。
可達性分析示意圖
即便在可達性分析算法中不可達的對象,也不必定會當即被回收。一個對象被回收,至少要經歷兩次標記過程。
若是對象在進行可達性分析後沒有與GC Roots相連的引用鏈,那它將會被第一次標記並進行一次篩選。篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或finalize()方法已被虛擬機調用過,虛擬機將這兩種狀況視爲"沒有必要執行"。
若是這個對象斷定爲有必要執行finalize()方法,那麼這個對象會放置在F-Queue隊列中,稍後由虛擬機自動創建、低優先級的Finalizer線程去執行finalize()方法。GC對F-Queue中的對象進行第二次小規模標記,若是對象從新與引用鏈上的任何一個對象創建關聯,那麼第二次標記時它將被移除"即將回收"的集合。不然對象就真的要被回收了。
Finalize方法
2.方法區回收斷定
方法區的回收主要包括兩部份內容:廢棄常量和無用的類。
- 廢棄常量的回收與回收Java堆中的對象相似。
- 判斷無用的類的條件必須知足三個條件:
- 該類全部實例已經被回收。
- 加載該類的ClassLoader已被回收。
- 該類對應的java.lang.Class對象沒有在任何地方被引用,也沒法經過反射訪問該類。
3.垃圾收集算法
- 標記-清除算法(Mark-Sweep):
- 算法分爲"標記"和"清除"兩個階段:首先標記出須要回收的對象,在標記完成後統一回收被標記的對象。它主要不足有兩個:一是效率問題,標記和清除兩個過程效率都不高。二是空間問題,標記清除後會產生大量不連續內存碎片,碎片太多可能致使要分配較大對象時,沒法找到足夠的內存空間不得不提早觸發一次垃圾收集動做。
-
- 標記-清除
- 複製算法:
- 複製算法將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中一塊。當一塊內存用完了,將存活的對象複製到另外一塊上面,而後把已使用的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法將內存縮小爲原來的一半,代價較高。
-
- 複製算法
- 標記-整理算法(Mark-Compact):
- 標記過程與"標記-清除"算法同樣,但後續不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存
-
- 標記-整理算法
4.分代收集算法
商業虛擬機的垃圾收集都採用分代收集算法,根據對象存活週期將內存劃分爲幾塊。Java堆分爲新生代和老年代,這樣能夠根據年代特色採用適當的收集算法。新生代中每次垃圾收集都有大批對象死去,那就選用複製算法。老年代對象存活率高,沒有額外空間進行分配擔保,適合使用"標記-清理"或"標記-整理"算法來回收。
4.內存分配與回收策略
- 對象優先在Eden分區:
- 大多數狀況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間分配時,虛擬機發起一次Minor GC。GC後對象嘗試放入Survivor空間,若是Survivor空間沒法放入對象時,只能經過空間分配擔保機制提早轉移到老年代。
- 大對象直接進入老年代:
- 大對象指須要大量連續內存空間的Java對象。虛擬機提供-XX:PretenureSizeThreshold參數,若是大於這個設置值對象則直接分配在老年代。這樣能夠避免新生代中的Eden區及兩個Survivor區發生大量內存複製。
- 長期存活的對象進入老年代:
- 虛擬機會給每一個對象定義一個對象年齡計數器。若是對象在Eden出生而且通過一次Minor GC後任然存活,且可以被Survivor容納,將被移動到Survivor空間中,而且對象年齡設爲1.每次Minor GC後對象任然存活在Survivor區中,年齡就加一,當年齡到達-XX:MaxTenuringThreshold參數設定的值時,將會移動到老年代。
- 動態年齡判斷:
- 虛擬機不是永遠要求對象的年齡必須達到-XX:MaxTenuringThreshold設定的值纔會將對象移動到老年代去。若是Survivor中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象能夠直接進入老年代。
- 空間分配擔保:
- 在Minor GC前,虛擬機會檢查老年代最大可用連續空間是否大於新生代全部對象總空間,若是條件成立,那麼Minor GC是成立的。若是不成立,虛擬機查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用連續空間是否大於歷次移動到老年代對象的平均大小,若是大於,將嘗試一次Minor GC。若是小於,或者HandlePromotionFailure設置值不容許冒險,那將進行一次Full GC。
新生代GC(Minor GC):發生在新生代的垃圾收集動做,由於Java對象大多朝生夕死,因此Minor GC很是頻繁,回收速度也較快。
老年代GC(Major GC/Full GC):發生在老年代的垃圾收集動做。出現Major GC,常常會伴隨至少一次Minor GC。Major GC的速度通常比Minor GC慢10倍以上。
在此我向你們推薦一個Java高級羣 :725633148 裏面會分享一些資深架構師錄製的視頻錄像:(有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構、面試資料)等這些成爲架構師必備的知識體系 進羣立刻免費領取,目前受益良多!