理解JVM記憶體分配策略 三大原則+擔保機制 JVM分配記憶體機制有三大原則和擔保機制 具體如下所示: 優先分配到eden區 大對象,直接進入到老年代 長期存活的對象分配到老年代 空間分配擔保 對象優先在Eden上分配 如何驗證對象優先在Eden上分配呢,我們進行如下實驗。 列印記憶體分配信息 首先代碼如 ...
理解JVM記憶體分配策略
三大原則+擔保機制
JVM分配記憶體機制有三大原則和擔保機制
具體如下所示:
- 優先分配到eden區
- 大對象,直接進入到老年代
- 長期存活的對象分配到老年代
- 空間分配擔保
對象優先在Eden上分配
如何驗證對象優先在Eden上分配呢,我們進行如下實驗。
列印記憶體分配信息
首先代碼如下所示:
public class A {
public static void main(String[] args) {
byte[] b1 = new byte[4*1024*1024];
}
}
代碼很簡單,就是創建一個Byte數組,大小為4mb。
然後我們在運行的時候加上虛擬機參數來列印垃圾回收的信息。
-verbose:gc -XX:+PrintGCDetails
在我們運行後,結果如下所示。
Heap
PSYoungGen total 37888K, used 6718K [0x00000000d6000000, 0x00000000d8a00000, 0x0000000100000000)
eden space 32768K, 20% used [0x00000000d6000000,0x00000000d668f810,0x00000000d8000000)
from space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000)
to space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000)
ParOldGen total 86016K, used 0K [0x0000000082000000, 0x0000000087400000, 0x00000000d6000000)
object space 86016K, 0% used [0x0000000082000000,0x0000000082000000,0x0000000087400000)
Metaspace used 2638K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K
手動指定收集器
我們可以看在新生代採用的是Parallel Scavenge收集器
其實我們可以指定虛擬機參數來選擇垃圾收集器。
比方說如下參數:
-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
運行結果如下:
Heap
def new generation total 38720K, used 6850K [0x0000000082000000, 0x0000000084a00000, 0x00000000ac000000)
eden space 34432K, 19% used [0x0000000082000000, 0x00000000826b0be8, 0x00000000841a0000)
from space 4288K, 0% used [0x00000000841a0000, 0x00000000841a0000, 0x00000000845d0000)
to space 4288K, 0% used [0x00000000845d0000, 0x00000000845d0000, 0x0000000084a00000)
tenured generation total 86016K, used 0K [0x00000000ac000000, 0x00000000b1400000, 0x0000000100000000)
the space 86016K, 0% used [0x00000000ac000000, 0x00000000ac000000, 0x00000000ac000200, 0x00000000b1400000)
Metaspace used 2637K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K
其實JDK預設的不是Parallel收集器,但是JDK會依照各種環境來調整採用的垃圾收集器。
查看環境的代碼如下:
java -version
因此JDK根據server的環境,採用了Paralled收集器。
而Serial收集器主要用在客戶端的。
eden分配的驗證
我們看到現在eden區域為34432K,使用了19%,那我們來擴大10倍是否eden就放不下了呢?
我們來驗證一下。
public class A {
public static void main(String[] args) {
byte[] b1 = new byte[40*1024*1024];
}
}
運行結果如下:
Heap
def new generation total 38720K, used 2754K [0x0000000082000000, 0x0000000084a00000, 0x00000000ac000000)
eden space 34432K, 8% used [0x0000000082000000, 0x00000000822b0bd8, 0x00000000841a0000)
from space 4288K, 0% used [0x00000000841a0000, 0x00000000841a0000, 0x00000000845d0000)
to space 4288K, 0% used [0x00000000845d0000, 0x00000000845d0000, 0x0000000084a00000)
tenured generation total 86016K, used 40960K [0x00000000ac000000, 0x00000000b1400000, 0x0000000100000000)
the space 86016K, 47% used [0x00000000ac000000, 0x00000000ae800010, 0x00000000ae800200, 0x00000000b1400000)
Metaspace used 2637K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K顯然,我們還是正常運行了,但是eden區域沒有增加,老年代區域卻增加了,符合大對象直接分配到老年代的特征。。
所以我們適當的縮小每次分配的大小。
我們在此限制下eden區域的大小
參數如下:
-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
這裡我們限制記憶體大小為20M
Eden大小為8M
然後我們運行我們的代碼:
代碼如下所示:
public class A {
public static void main(String[] args) {
byte[] b1 = new byte[2*1024*1024];
byte[] b2 = new byte[2*1024*1024];
byte[] b3 = new byte[2*1024*1024];
byte[] b4 = new byte[4*1024*1024];
System.gc();
}
}
運行結果如下:
[GC (Allocation Failure) [DefNew: 7129K->520K(9216K), 0.0053010 secs] 7129K->6664K(19456K), 0.0053739 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [Tenured: 6144K->6144K(10240K), 0.0459449 secs] 10920K->10759K(19456K), [Metaspace: 2632K->2632K(1056768K)], 0.0496885 secs] [Times: user=0.00 sys=0.00, real=0.04 secs]
Heap
def new generation total 9216K, used 4779K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 58% used [0x00000000fec00000, 0x00000000ff0aad38, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
Metaspace used 2638K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K我們可以發現在eden區域為8192K 約為8M
也就是我們的b4的大小
而原先的b1,b2,b3為6M,被分配到了tenured generation。
原先的Eden區域如下所示,在分配完,b1,b2,b3後如下所示。
這時候我們發現已經無法繼續分了。
而查看日誌的時候,我們發生了倆次GC。
[GC (Allocation Failure) [DefNew: 7129K->520K(9216K), 0.0053010 secs] 7129K->6664K(19456K), 0.0053739 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [Tenured: 6144K->6144K(10240K), 0.0459449 secs] 10920K->10759K(19456K), [Metaspace: 2632K->2632K(1056768K)], 0.0496885 secs] [Times: user=0.00 sys=0.00, real=0.04 secs]
而在
[DefNew: 7129K->520K(9216K), 0.0053010 secs] 7129K->6664K(19456K), 0.0053739 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
中我們會看到,剛分配的對象並沒有被回收。
上面的GC是針對新生代的。
而下麵的FullGC是針對老年代的。
如果我們這時候要再分配4m的記憶體,虛擬機預設將原先的eden區域放到可放的地方,也就是在老年代這裡
因此會發生我們這種情況。
這就是整個過程。驗證了對象有現在Eden區域回收
大對象直接進入到老年代
指定大對象的參數。
-XX:PretenureSizeThreshold
測試代碼:如下
-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
public class A {
private static int M = 1024*1024;
public static void main(String[] args) {
byte[] b1 = new byte[8*M];
}
}
運行結果如下:
Heap
def new generation total 9216K, used 1149K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 14% used [0x00000000fec00000, 0x00000000fed1f718, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
Metaspace used 2637K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K
我們可以看到,結果數直接把8M扔到了老年代裡面了。
而我們修改成7M的時候
被髮現7M全部扔到了eden裡面。
如果我們制定了參數後,會發現結果變了。
參數如下所示:
-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=6M
運行結果如下:
我們會發現7M進到了老年代。
長期存活對象進入老年代
參數如下:
-XX:MaxTenuringThreshold
每次進行回收的時候,如果沒被回收,那對象的年齡+1
如果對象年齡到達閾值,就會進入老年代。
具體測試和上面的Max一樣。就不占篇幅了。
空間分配擔保
參數如下:
-XX:+HandlePromotionFailure
步驟如下:
- 首先衡量有沒有這個能力,然後才能進行分配。
- 如果有這個能力放入,那麼這個參數是‘+’號證明開啟了記憶體擔保,否則是‘-’號就是沒開啟。
逃逸分析與棧上分配
如何將記憶體分配到棧上呢?
這時就需要我們使用逃逸分析,篩選出未發生逃逸對象。
逃逸分析的主要目標就是分析出對象的作用域。
public class A {
A obj;
//方法返回A對象,發生逃逸。
public A getInstance() {
return this.obj ==null?new A():obj;
}
//為成員屬性賦值,發生逃逸
public void setObj() {
this.obj = new A();
}
//沒有發生逃逸。對象的作用域盡在當前方法中有效,沒有發生逃逸。
public void useA() {
A s = new A();
}
//
public void useA2() {
A s = getInstance();
}
}
總結,只要定義在方體中,對象的作用域不發生逃逸,否則發生逃逸。
所以儘量把變數放在方法體內,這樣會提高效率。
總結:
JVM記憶體分配策略不是特別複雜,只要一步一步跟著虛擬機走,那麼就可以去理解JVM記憶體分配的機制。