這篇文章是我本身回顧和再學習 Java 內存管理相關知識的過程當中整理出來的。html
整理的目的是讓我本身能對 Java 內存管理相關的知識的認識更全面一些,分享的目的是但願你們也能從這些知識中獲得一些啓發。算法
內存是計算機中重要的部件之一,是與 CPU 進行溝通的橋樑,是 CPU 能直接尋址的存儲空間,由半導體器件製成。緩存
若是說數據是商品,那硬盤就是商店的倉庫,內存就是商店的貨架,倉庫裏的商品你是不能直接買的,你只能買貨架上的商品。數據結構
每個程序中使用的內存區域至關因而不一樣的貨架,當一個貨架上須要擺放的商品超過這個貨架所能容納的最大值,就會出現放不下的狀況,也就是內存溢出。架構
JVM(Java 虛擬機)是 Java Virtual Machine 的縮寫,它是一個虛構出來的計算機,經過在實際的計算機上仿真模擬各類計算機功能來實現的。函數
JVM 有本身的硬件架構,如處理器、堆棧、寄存器等,還有對應分指令系統。性能
假如一個程序使用的內存區域是一個貨架,那 JVM 就至關因而一個淘寶店鋪,它不是真實存在的貨架,但它和真實貨架同樣能夠上架和下架商品,並且上架的商品數量也是有限的。學習
假如貨架是在深圳,那 JVM 的平臺無關性就至關因而客人能夠在各個地方購買你在淘寶上發佈的商品,不是隻有在深圳才能購買貨架上的商品。優化
Java 內存模型的主要目標是定義程序中各個變量的訪問規則,也就是在虛擬機中將變量存儲到內存,以及從內存中取出變量這樣的底層細節。atom
下面咱們就來看下 Java 內存模型的具體介紹。
Java 內存模型規定了全部的變量都存儲在主內存(Main Memory)中,每條線程有本身的工做內存(Working Memory),線程的工做內存中保存了線程使用到的變量的內存副本。
線程對變量副本的全部操做都必須在工做內存中進行,不能直接讀寫主內存中的變量。
不一樣線程之間沒法直接訪問其餘線程工做內存中的變量,線程間變量值的傳遞都要經過主內存來完成。
所謂執行引擎,就是一個運算器,可以識別輸入的指令,並根據輸入的指令執行一套特定的邏輯,最終輸出特定的結果 執行引擎對於 JVM 的做用就像是 CPU 對於實體機器的做用,均可以識別指令,而且根據指令完成特定的運算。
Java 內存模型中定義了 8 種操做來完成主內存與工做內存之間具體的交互協議,虛擬機實現時必須保證每一種操做都是原子、不可再分的。
這 8 種操做又可分爲做用於主內存的和做用於工做內存的操做。
lock(鎖定)
做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。
unlock(解鎖)
做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能被其餘線程鎖定。
read(讀取) 做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便 load 時使用。
write(寫入)
做用於主內存的變量,它把 store 操做從工做內存中獲得的變量值放入主內存的變量中。
load(載入)
做用於工做內存的變量,它把 read 操做從主內存中獲得的變量值放入工做內存的變量副本中。
use(使用 ) 做用於工做內存的變量,它把一個工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用的變量的值的字節碼執行時會執行這個操做。
assign(賦值) 做用於工做內存的變量,它把一個執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼執行時執行這個操做。
store(存儲) 做用於工做內存的變量,它把工做內存中的一個變量的值傳送到主內存中,以便隨後的 write 操做使用。
JVM 在執行 Java 程序的過程當中會把它管理的內存分爲若干個數據區域,而這些區域又能夠分爲線程私有的數據區域和線程共享的數據區域。
程序計數器(Program Counter Register)有下面三個特色。
較小
程序計數器是一塊較小的內存空間,它能夠看做是當前線程執行的字節碼的行號指示器。
線程私有
爲了線程切換後能恢復到正確的執行位置,每條線程都有一個私有的程序計數器。
無異常
程序計數器是惟一一個在 Java 虛擬機規範中沒有規定任何 OOM 狀況的區域。
虛擬機棧能夠說是 Java 方法棧,它有下面三個特色。
描述方法執行
虛擬機棧描述的是 Java 方法執行的內存模型,每一個方法在執行時都會建立一個棧幀(Stack Frame),棧幀用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。
一個方法從調用到執行完成的過程,對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
關於棧幀在第 5 大節會有一個更多的介紹。
線程私有
與程序計數器同樣,Java 虛擬機棧也是線程私有的,它的生命週期與線程相同。
異常
在 Java 虛擬機規範中,對虛擬機棧規定了下面兩種異常。
StackOverflowError
當執行 Java 方法時會進行壓棧的操做,在棧中會保存局部變量、操做數棧和方法出口等信息。
JVM 規定了棧的最大深度,若是線程請求執行方法時棧的深度大於規定的深度,就會拋出棧溢出異常 StackOverflowError。
OutOfMemoryError
若是虛擬機在擴展時沒法申請到足夠的內存,就會拋出內存溢出異常 OutOfMemoryError。
本地方法棧(Native Method Stack)的做用與虛擬機棧很是類似,它有下面兩個特色。
爲 Native 方法服務
本地方法棧與虛擬機棧的區別是虛擬機棧爲 Java 方法服務,而本地方法棧爲 Native 方法服務。
異常
與虛擬機棧同樣,本地方法棧也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
Java 堆(Java Heap)也就是實例堆,它有下面四個特色。
最大
對於大多數應用來講,Java 堆是 JVM 管理的內存中最大的一塊內存區域。
線程共享
Java 堆是全部線程共享的一塊內存區域,在虛擬機啓動時建立。
存放實例
堆的惟一做用就是存放對象實例,幾乎全部的對象實例都是在這裏分配內存。
GC
堆是垃圾收集器管理的主要區域,因此有時也叫 GC 堆。
方法區(Method Area)存儲的是已被虛擬機加載的數據,它有下面三個特色。
線程共享
方法區和堆同樣,是全部線程共享的內存區域。
存儲的數據類型
異常
方法區的大小決定了系統能夠保存多少個類,若是系統定義了太多的類,致使方法區溢出,虛擬機一樣會拋出內存溢出異常 OutOfMemoryError。
方法區又可分爲運行時常量池和直接內存兩部分。
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分。
Class 文件中除了有類的版本、字段、方法和接口等描述信息,還有一項信息就是常量池(Constant Pool Table)。
常量池用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
運行時常量池受到方法區內存的限制,當常量池沒法再申請到內存時會拋出 OutOfMemoryError 異常。
直接內存
直接內存(Direct Memory)有下面四個特色。
在虛擬機數據區外
直接內存不是從虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。
直接分配
在 JDK 1.4 中新加入了 NIO(New Input/Output)類,引入了一種基於通道與緩衝區的 I/O 方式,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做,這樣能避免在 Java 堆和 Native 堆中來回複製數據。
受設備內存大小限制
直接內存的分配不會受到 Java 堆大小的限制,可是會受到設備總內存(RAM 以及 SWAP 區)大小以及處理器尋址空間的限制。
異常
直接內存的容量默認與 Java 堆的最大值同樣,若是超額申請內存,也有可能致使 OOM 異常出現。
當 Java 程序出現異常時,程序會打印出對應的異常堆棧,經過這個堆棧咱們能夠知道方法的調用鏈路,而這個調用鏈路就是由一個個 Java 方法棧幀組成的。
咱們來看下棧幀中包含的局部變量表、操做數棧、動態鏈接和返回地址分別有着什麼做用。
局部變量表(Local Variable Table)中的變量只在當前函數調用中有效,當函數調用結束後,隨着函數棧幀的銷燬,局部變量表也會隨之銷燬。
局部變量表中存放的編譯期可知的各類數據有以下三種。
基本數據類型
如 boolean、char、int 等
對象引用
reference 類型,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置
returnAddress 類型
指向了一條字節碼指令的地址。
操做數棧(Operand Stack)也叫操做棧,它主要用於保存計算過程的中間結果,同時做爲計算過程當中臨時變量的存儲空間。
操做數棧也是一個先進後出的數據結構,只支持入棧和出棧兩種操做。
當一個方法剛開始執行時,操做數棧是空的,在方法執行的過程當中,會有各類字節碼執行往操做數棧中寫入和提取內容,也就是出棧/入棧操做。
好比下面的這張圖中,當調用了虛擬機的 iadd 指令後,它就會在操做數棧中彈出兩個整數並進行加法計算,並將計算結果入棧。
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接(Dynamic Linking)。
當一個方法開始執行後,只有兩種方式能夠退出這個方法,一種是正常完成出口,另外一種是異常完成出口。
正常完成出口
執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者。
是否有返回值和返回值的類型將根據遇到哪一種方法返回指令來決定,這種退出方法的方式稱爲正常完成出口(Normal Method Invocation Completion)。
異常完成出口
在方法執行過程當中遇到異常,而且這個異常沒有在方法體內獲得處理,就會致使方法退出,這種退出方式稱爲異常完成出口(Abrupt Method Invocation Completion)。
一個方法使用異常完成出口的方式退出,任何值都不會返回給它的調用者。
不管採用哪一種退出方式,在方法退出後,都須要返回到方法被調用的位置,程序才能繼續執行。
在主流的商用程序語言(Java、C# 和 Lisp 等)的主流實現中,都是經過可達性分析(Reachability Analysis)斷定對象是否存活的。
這個算法的基本思路就是經過一系列「GC Roots」對象做爲起始點,從這些節點開始向下搜索,搜索走過的路徑就叫引用鏈。
當一個對象到 GC Roots 沒有任何引用鏈相連時,則證實此對象是不可用的。
好比下圖中的 object五、object六、object7,雖然它們互有關聯,可是它們到 GC Roots 是不可達的,因此它們會被斷定爲可回收對象。
在 Java 中,不一樣內存區域中可做爲 GC Roots 的對象包括下面幾種。
虛擬機棧
虛擬機棧的棧幀中的局部變量表中引用的對象,好比某個方法正在使用的類字段。
方法區
本地方法棧
本地方法棧中 Native 方法引用的對象。
不管是經過引用計數算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象的引用鏈是否可達,斷定對象是否存活都與引用有關。
在 JDK 1.2 以後,Java 對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用和虛引用四種,這四種引用強度按順序依次減弱。
強引用有下面幾個特色。
廣泛存在
強引用是指代碼中廣泛存在的,好比 "Object obj = new Object()" 這類引用。
直接訪問
強引用能夠直接訪問目標對象。
不會回收
強引用指向的對象在任什麼時候候都不會被系統回收,虛擬機即便拋出 OOM 異常,也不會回收強引用指向的對象。
使用 obj = null 不會觸發 GC,可是在下次 GC 的時候這個強引用對象就能夠被回收了。
OOM 隱患
強引用可能致使內存泄漏。
軟引用有下面幾個特色。
有用但非必需
軟引用用於描述一些還有用但非必需的對象。
二次回收
對於軟引用關聯的對象,在系統即將發生內存溢出前,會把這些對象列入回收範圍中進行二次回收。
OOM 隱患
若是二次回收後尚未足夠的內存,就會拋出內存溢出異常。
SoftReference
在 JDK 1.2 後,Java 提供了 SoftReference 類來實現軟引用。
弱引用有下面幾個特色。
比軟引用弱
弱引用的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次 GC 前。
發現即回收
在 GC 時,只要發現弱引用,無論系統堆空間使用狀況如何,都會將對象進行回收。
無關緊要
軟引用、弱引用適合保存無關緊要的緩存數據。
WeakReference
JDK 1.2 後,提供了 WeakReference 類來實現弱引用。
虛引用是最弱的一種引用關係,它有如下幾個特色。
沒法獲取
一個對象是否有虛引用的存在,都不會對其生存時間構成影響,也沒法經過虛引用取得一個對象實例。
收到通知
爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。
PhatomReference
在 JDK 1.2 後,提供了 PhantomReference 類來實現虛引用。
地上有髒東西是不可避免的,可是每天都要掃地又太麻煩了,有沒有什麼辦法可讓咱們不用掃地呢?
掃地機器人就能夠幫咱們作這件事,而垃圾回收器 GC(Garbage Collector)就至關因而掃地機器人。
咱們 Java 開發者不用像 C++ 開發者那樣關心內存釋放的問題,可是咱們也不能擋着掃地機器人的路。
當咱們操做不當致使某塊內存泄漏時,GC 就不能對這塊內存進行回收。
GC 可不是個好伺候的主,若是你讓「GC 很忙」,那它就會讓你「應用很卡」。
拿 Android 來講,進行 GC 時,全部線程都要暫停,包括主線程,16ms 是 Android 要求的每幀繪製時間,而當 GC 的時間超過 16ms,就會形成丟幀的狀況,也就是界面卡頓。
垃圾回收器回收資源的方式就是垃圾回收算法,下面咱們來看下四個主要的垃圾回收算法。
標記-清除算法(Mark-Sweep)至關因而先把貨架上有人買的、沒人買的、空着的商品和位置都記錄下來,而後再把沒人買的商品統一進行下架。
工做原理
第一步:標記全部須要回收的對象
第二步:標記完成後,統一回收全部被標記的對象
缺點
效率低
標記和清除的效率都不高
內存碎片
標記清除後會產生大量不連續的內存碎片,內存碎片太多會致使當程序須要分配較大的對象時,沒法找到足夠的連續內存而不得不提早觸發 GC
爲了解決效率問題,複製(Copying)收集算法出現了。
工做原理
複製算法把可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。
當使用中的這塊內存用完了,就把存活的對象複製到另外一塊內存上,而後把已使用的空間一次清理掉。
這樣每次都是對半個內存區域進行回收,內存分配時也不用考慮內存碎片等複雜問題。
優勢
複製算法的優勢是每次只對半個內存區域進行內存回收,分配內存時也不用考慮內存碎片等複雜狀況,只要一動堆頂指針,按順序分配內存便可。
缺點
浪費空間
把內存縮小一半來使用太浪費空間。
有時效率較低
在對象存活率高時,要進行較多的複製操做,這時效率就變低了
在複製算法中,若是不想浪費 50% 的空間,就須要有額外的空間進行分配擔保,以應對被使用內存中全部對象都存活的低端狀況,因此養老區不能用這種算法。
根據養老區的特色,有人提出了一種標記-整理(Mark-Compact)算法。
工做原理
標記-整理算法的標記過程與標記-清除算法同樣,但後續步驟是讓全部存活的對象向一端移動,而後直接清理掉邊界外的內存。
現代商業虛擬機的垃圾回收都採用分代收集(Generational Collection)算法,這種算法會根據對象存活週期的不一樣將內存劃分爲幾塊,這樣就能夠根據各個區域的特色採用最適當的收集算法。
在新生區,每次垃圾收集都有大批對象死去,只有少許存活,因此能夠用複製算法。
養老區中由於對象存活率高、沒有額外空間對它進行擔保,就必須使用標記-清理或標記-整理算法進行回收。
堆內存可分爲新生區、養老區和永久存儲區三個區域。
新生區
新生區(Young Generation Space)是類的誕生、成長和消亡的區域。
新生區又分爲伊甸區(Eden space)、倖存者區(Survivor space)兩部分。
伊甸區
大多數狀況下,對象都是在伊甸區中分配的,當伊甸區沒有足夠的空間進行分配時,虛擬機將發起一次 Minor GC。
Minor GC 是指發生在新生區的垃圾收集動做,Minor GC 很是頻繁,回收速度也比較快。
當伊甸區的空間用完時,GC 會對伊甸區進行垃圾回收,而後把伊甸區剩下的對象移動到倖存 0 區。
倖存 0 區
若是倖存 0 區滿了,GC 會對該區域進行垃圾回收,而後再把該區剩下的對象移動到倖存 1 區。
倖存 1 區
若是倖存 1 區滿了,GC 會對該區域進行垃圾回收,而後把倖存 1 區中的對象移動到養老區。
養老區
養老區(Tenure Generation Space)用於保存重新生區篩選出來的 Java 對象。
當倖存 1 區移動嘗試對象到養老區,可是發現空間不足時,虛擬機會發起一次 Major GC。
Major GC 的速度通常比 Minor GC 慢 10 倍以上。
大對象會直接進入養老區,好比很大的數字和很長的字符串。
永久存儲區
永久存儲區(Permanent Space)是一個常駐內存區域,用於存放 JDK 自身攜帶的 Class Interface 元數據。
永久存儲區存儲的是運行環境必需的類信息,被裝載進該區域的數據是不會被垃圾回收器回收掉的,只有 JVM 關閉時纔會釋放此區域的內存。