一、JVM 內存模型
本節來分析 Java 對象如何進行分配
和回收
。
JVM 運行時數據區
主要由線程私有區域
和線程共享
區域組成。
- 線程私有區域:
- 虛擬機棧
- 本地方法棧
- 程序計數器
2.線程共享區域:
- 堆
- 方法區
下面繪製一個草圖來描述 JVM 運行數據區的組成:
1.1、線程私有區域
線程私有區域組成爲:
- 程序計數器
- 虛擬機棧
- 本地方法棧
1.1.1、程序計數器
什麼是程序計數器呢?
因爲 Java 本身就是一個多線程的,每一個線程都有一個程序計數器, CPU 在切換線程時,會使用程序計數器記錄下當前線程正在執行的字節碼指令的地址(行號),這樣線程再次回來工作時,就知道執行到哪個位置了。
爲了更加深入的理解程序計數器,下面來看這樣一段代碼:
通過 javap -c -l MMDemo.class 得到對應字節碼:
這個 Code 對應的這些數就是程序計數器了。
1.1.2、虛擬機棧
虛擬棧屬於線程私有部分,在線程內部中一般會調用很多方法,而每一個方法使用一個棧幀
來描述。
下面用一個草圖來描述一下棧幀
與虛擬機棧
的關係:
虛擬機棧是由多個棧幀組成,每調用一個方法就相當於有一個棧幀入棧到虛擬機棧中。
1.1.3、棧幀的組成
在前面描述過,在線程中,一個方法被調用就會一個棧幀
被壓入虛擬機棧
中。棧幀就是用來描述這個方法,一個棧幀是由局部變量表
,操作數棧
,返回值地址
,動態鏈接
組成。
下面還是回到上面示例,結合草圖,看它們之間的關係:
局部變量表:
方法內部聲明的變量存放表
32位地址,尋址空間爲 4G 。如果需要存放64位的數據,需要使用高位和地位表示。
下面是 pay() 生成的局部變量表:
- this 表示當前對象
- i
- obj
操作數棧:
對局部變量表中的變量進行出棧入棧的操作。
返回值地址:
一個方法被執行之後,有一個返回值,返回給對應的調用處。
動態鏈接:
主要對應的多態,只有代碼執行時才知道具體的實現類是那個對象。
1.1.4、StackOverflowError
這個異常想必很多人都遇過,字面意思就是棧溢出。我們通過上面的分析我們知道,虛擬機棧如果不斷出現棧幀入棧,當虛擬機棧空間達到上限,那麼就會出現 StackOverflowError
。
下面來模擬這個錯誤的產生:
public class StackOverflowError {
private static int count = 0;
public static void main(String[] args) {
try {
recursion();
} catch (Throwable e) {
System.out.println("deep of calling = " + count);
e.printStackTrace();
}
}
public static void recursion() {
count++;
recursion();
}
}
如果是死循環出現這樣的錯誤StackOverflowError,那麼通過 -Xss
參數的設置也是沒有用。
當然,如果是因爲虛擬機棧空間比較少到導致頻繁出現這個錯誤,那麼是可以合理的調節這個參數的。
例如設置參數:-Xss164K
1.1.5、本地方法棧
虛擬機棧對應的方法是 Java 方法,而本地方法棧對應的是 native 方法。其他方面應該和虛擬機棧差不多。
1.2、線程共享區域
1.2.1、方法區
方法區所存放的數據爲類信息
,常量
,靜態變量
,即時編譯後的代碼
。
在 JDK1.8 以下的 JVM 中,方法區就是對應的永久代
,而 JDK1.8 之後方法區就變成了元空間(MetaSpace)
1.2.2、堆空間
在堆空間中主要存放的是通過 new 創建出來的對象或者數組。
##1.3、JVM 內存模型-堆
在 JVM 內存模型中,將線程共享部分分爲了堆空間和方法區,下面主要來看堆空間
是如何進一步劃分的。
在下面這張草圖中,JVM 堆空間按照分代思想
劃分爲新生代
和老年代
兩部分,它們兩者空間分別爲堆空間的1/3和2/3。
對於新生代這個區域又進一步進行劃分爲 Eden區
, from區
,to區
這三個區域。
- 對象或者數組的創建優先在 Eden 區分配內存空間。
- 如果新創建的大對象在新生代放不下,那麼會直接移入老年代空間。
- 在每一次進行 Minor GC 之後,Eden 區的垃圾對象就會被回收,並且存活的對象會進入 from區 或者 to區。在每次 Minor GC 之後存活的對象的年齡會累加,當多次 GC 之後,對象的年齡達到 15 ,那麼將進入老年代。
- 如果 Eden 區存活的對象太大,放不進 from 或者 to 中,那麼將進入老年代。
注意:在新生代中發生的 GC 爲 Minor GC 而老年代發生的 GC 爲 Full GC, Full GC 比 Minor GC 的效率要低。
1.4、垃圾回收算法
GC 是如何判斷一個對象是否需要被回收呢?
在 JVM 中主要有兩種判斷方法:
- 對象引用記數法
當一個對象被一個變量引用時,那麼引用計數累加。如果一個對象沒有被其他變量引用,那麼就是需要被 GC 回收的。但是這裏有一個弊端,那就是對象間相互引用導致無法被回收的問題。
- 可達性分析算法
從一個 GCRoot 開始遍歷,當一個對象到 GCRoot 沒有直達路徑時,就標記爲不可達,是需要被 GC 回收的。
那什麼對象可以作爲 GCRoot 呢?
- 局部變量表所引用的對象
- 靜態變量/常量引用的對象
- 本地方法引用的對象
JVM垃圾收集算法
- 標記清除法
標記清除算法分爲兩個階段:
標記階段負責將將可回收的對象進行標記出來,清除階段負責將這些標記出來的對象進行回收。從下面的圖可以看出,這種算法會造成內存碎片。
- 複製算法:
它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。
這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,持續複製長生存期的對象則導致效率降低。
- 標記整理法:
複製收集算法在對象存活率較高時就要執行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存
- 分代收集算法:
根據分代思想對堆區劃分位新生代和老年代,針對這個兩個區只用不同的回收算法。
新生代:使用複製算法
老年代:使用標記清理算法或者標記整理算法
總結
…
記錄於2019年3月27日