JVM
運行時資料區域
根據《Java 虛擬機規范(Java SE 7 版)》規定,Java 虛擬機所管理的記憶體如下圖所示,

程式計數器
記憶體空間小,執行緒私有.
位元組碼解釋器作業時就是通過改變程式計數器的值來選取下一條需要執行指令的位元組碼指令(主要是取下一條指令的位元組碼檔案).
分支,回圈,跳轉,例外處理,執行緒恢復等基礎功能都依賴程式計數器來完成.
如果執行緒正在執行一個Java方法,這個計數器記錄的是正在執行的虛擬機位元組碼指令的地址 ;
如果正在執行的是Native方法, 這個計數器記錄的值為(Undefined).
此記憶體區域是唯一一個在Java虛擬機中沒有規定任何OutOfMemoryError情況的區域.
Java虛擬機堆疊
執行緒私有,生命周期和執行緒一致.
描述的是Java方法執行的記憶體模型:每個方法在執行時都會創建一個堆疊幀(Stack Frame)用于存盤區域變數表,運算元堆疊,動態鏈接,方法出口等資訊.
每一個方法從呼叫到執行結束,就對應著一個堆疊幀從虛擬機堆疊中入堆疊到出堆疊的程序.
區域變數表 : 存放了編譯期可知的各種基本型別(boolean, byte, char, short, int, float, long, double), 物件參考(reference型別) 和 returnAddress型別(指向了一條位元組碼指令的地址).
StackOverflowError:執行緒請求的堆疊深度大于虛擬機所允許的深度, OutOfMemoryError:如果虛擬機堆疊可以動態擴展,而擴展時無法申請到足夠的記憶體,
本地方法堆疊
區別于Java虛擬機堆疊的是,Java虛擬機堆疊為虛擬機執行java方法(也就是位元組碼)服務, 而本地方法堆疊則為虛擬機使用到的Native方法服務.
也會有StackOverflowError和OutOfMemoryError例外.
Java堆
執行緒共享
對于絕大部分應用來說,Java堆這塊區域是JVM所管理的記憶體中最大的一塊.
主要存放物件實體和陣列.
內部會劃分出多個執行緒私有的分配緩沖區(Thread Local Allocation Buffer,TLAB).
可以位于物理上不連續的空間,但是邏輯上要連續,
堆是在虛擬機啟動時創建的.
OutOfMemoryError:如果堆中沒有記憶體完成實體分配,并且堆也無法再擴展時(計算需要的堆數超過自動存盤管理系統可用的堆數)拋出該例外,
方法區
屬于共享記憶體區域, 存盤已被加載的類資訊,常量,靜態變數,即時編譯器編譯后的代碼等資料.
方法區域在虛擬機啟動時創建.
如果方法區中的記憶體無法滿足分配請求,Java 虛擬機將引發 OutOfMemoryError.
運行時常量池
屬于方法區的一部分,用于存放編譯期生成的各種字面量和符號參考.
編譯期和運行期(String 的 intern() )都可以將常量放入池中.
記憶體有限,無法申請時排除OutOfMemoryError
創建類或介面時,如果運行時常量池的構造需要的記憶體多于 Java 虛擬機的方法區中可用的記憶體,則 Java 虛擬機將引發一個 OutOfMemoryError.
直接記憶體
非虛擬機運行時資料區的部分
在 JDK 1.4 中新加入 NIO (New Input/Output) 類,引入了一種基于通道(Channel)和快取(Buffer)的 I/O 方式,它可以使用 Native 函式庫直接分配堆外記憶體,然后通過一個存盤在 Java 堆中的 DirectByteBuffer 物件作為這塊記憶體的參考進行操作,可以避免在 Java 堆和 Native 堆中來回的資料耗時操作, OutOfMemoryError:會受到本機記憶體限制,如果記憶體區域總和大于物理記憶體限制從而導致動態擴展時出現該例外,

垃圾回收器與記憶體分配策略
程式計數器,虛擬機堆疊和本地方法堆疊3個區域隨執行緒生滅,堆疊中的堆疊針隨著方法的進入和退出執行出堆疊和入堆疊的操作.
Java堆和方法區不一樣,一個介面中的多個實作類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體可能也不一樣,我們只有在程式處于運行期才知道哪些物件會創建,這部分記憶體的分配和回收都是動態的,垃圾回收所關注的就是這部分記憶體.
判斷物件是否需要回收
參考計數法
每個物件有一個參考計數器,當物件被參考一次則計數器加1,當對應參考失效1次則計數器減1,對于計數器為0的物件意味著是垃圾物件,可以被GC回收.
缺點: Java堆中保持相互參考的物件無法回收,難以解決回圈參考問題
可達性演算法
從GC Roots作為起點開始搜索,從這些節點出發所走過的路徑稱為參考鏈. 那么整個鏈中的物件就是活物件.
對于GC Roots無法到達的物件隨時可能被GC回收.
可作為 GC Roots 的物件:
- 虛擬機堆疊(堆疊幀中的本地變數表)中參考的物件
- 方法區中靜態變數和常量參考的物件
- 本地方法堆疊中 JNI(即一般說的 Native 方法) 參考的物件
參考
前面的演算法判斷存活都與’參考’有關.
下面4種參考,強度依次遞減.
強參考
類似于Object obj = new Object();創建的 obj指向Object實體所在的堆空間.
強參考是使用最普遍的參考,如果一個物件具有強參考,那么垃圾回收器絕不會回收它
強參考特點:
- 強參考可以直接訪問目標物件
- 只要有參考變數存在,垃圾回收器永遠不會回收,JVM即使拋出OOM例外,也不會回收強參考所指向的物件,
- 強參考可能導致記憶體泄漏問題
軟參考
可以通過java.lang.ref.SoftReference類實作軟參考.在系統要發生記憶體溢位之前,將會把這些物件列進回收范圍中進行二次回收.
一個持有軟參考的物件,不會被JVM很快回收,JVM會判斷當前堆的使用情況來判斷何時回收.
當堆的使用率臨近閾值時,才回去回收軟參考的物件.
軟參考主要用來實作類似快取的功能,在記憶體足夠的情況下直接通過軟參考取值,無需從繁忙的真實來源查詢資料,提升速度;
當記憶體不足時,自動洗掉這部分快取資料,從真正的來源查詢這些資料,使用軟參考能防止記憶體泄露,增強程式的健壯性,
弱參考
在java中,可以用java.lang.ref.WeakReference實體來保存對一個Java物件的弱參考,
在系統GC時,不管系統堆空間是否足夠,都會將物件進行回收.
虛參考
可以通過PhantomReference 類實作虛參考. 無法通過虛參考獲取一個物件的實體,為一個物件設定虛參考關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知,
一個持有虛參考的物件和沒有參考幾乎是一樣的,隨時可能被垃圾回收器回收.
它的作用在于檢測物件是否已經從記憶體中洗掉,跟蹤垃圾回收程序.
垃圾回收演算法
標記清除法
標記清除法是最基礎的收集演算法 ,它分為"標記"和"清除"兩個階段:
首先標記出需要回收的物件,在標記完成后統一回收掉被標記的物件,它的標記程序其實就是前面的可達性分析演算法中判定垃圾物件的標記程序.
- 回收前:

- 回收后:

缺點:
- 標記和清除的效率都不高
- 標記清除后會產生大量不連續的記憶體碎片,空間碎片太多可能會導致需要分配較大物件時無法找到足夠的連續記憶體而不得不觸發另一次垃圾回收操作.
復制演算法
它將記憶體按容量分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的記憶體用完了,就將還存貨的物件復制到另一塊記憶體上面 , 然后再把已使用的記憶體空間一次清理掉.
優點:
- 每次只對一塊記憶體進行回收,運行高效
- 只需移動堆疊頂的指標,按順序分配記憶體即可,實作簡單
- 記憶體回收時不用考慮記憶體碎片的出現
缺點:
- 可一次分配的記憶體縮小了一半
- 空間利用率下降
因為大多數新生代物件都不會熬過第一次 GC,所以沒必要 1 : 1 劃分空間,可以分一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor,當回收時,將 Eden 和 Survivor 中還存活的物件一次性復制到另一塊 Survivor 上,最后清理 Eden 和 Survivor 空間,大小比例一般是 8 : 1 : 1,每次浪費 10% 的 Survivor 空間,但是這里有一個問題就是如果存活的大于 10% 怎么辦?這里采用一種分配擔保策略:多出來的物件直接進入老年代,
- 回收前:

- 回收后:

標記整理法
復制演算法比較適合于新生代,在老年代中,物件存活率比較高,如果執行較多的復制操作,效率將會變低,所以老年代一般會選用其他演算法,如標記—整理演算法,
該演算法標記程序和標記清除法中的標記程序一樣,然后把存活物件移到記憶體的一端,然后直接清理掉端邊界以外的記憶體,
- 回收前:

- 回收后:

分代回收
根據存活物件劃分幾塊記憶體區,一般是分為新生代和老年代,然后根據各個年代的特點制定相應的回收演算法,
- 在新生代中,每次垃圾收集時都會發現有大量物件死去,只有少量存活,因此可選用復制演算法來完成收集
- 老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除演算法或標記—整理演算法來進行回收
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/295685.html
標籤:其他
下一篇:iOS多執行緒面試題匯總與決議
