JVM筆記4:Java記憶體分配策略及配置引數
簡單來說,物件記憶體分配主要是在堆中分配。但是分配的規則並不是固定的,取決於使用的收集器組合以及JVM記憶體相關引數的設定
以下介紹幾條基本規則(使用的ParNew+Serial Old收集器組合):
一,物件優先在新生代Eden區分配
//-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintHeapAtGC -XX:+PrintGCDetails public class test { static int mb = 1024*1024; public static void main(String[] args) { byte[] b1 = new byte[2*mb]; System.out.println("b1 over"); byte[] b2 = new byte[2*mb]; System.out.println("b2 over"); byte[] b3 = new byte[2*mb]; System.out.println("b3 over");//GC byte[] b4 = new byte[4*mb]; System.out.println("b4 over"); } }
堆記憶體大小為20M,不可自動擴充套件,新生代記憶體大小為10M,根據預設值,Eden區:Survivor區為8:1,Eden區大小應為:10M*8/10=8129KB,Survivor區大小應為1024KB,新生代總可用記憶體應為9216KB
當b3分配完成後,新生代將使用6M記憶體(6144KB,b1+b2+b3),同時申請b4的4M=4096KB記憶體,此時新生代的可用記憶體為9216-6144=3072KB,不足以分配b4的空間,則觸發一次Minor GC回收新生代記憶體空間,由於b1、b2以及b3都為存活狀態,並且剩餘的一個Survivor區無法裝下b1、b2和b3,則新生代會租借老年代的區域,並將b1、b2和b3移動至租借區域,然後新生代完成Minor GC。由於此時新生代已經沒有物件存放其中,剩餘大量記憶體,則b4將在新生代中分配
b1 over b2 over b3 over {Heap before GC invocations=0 (full 0): par new generation total 9216K, used 6487K [0x03b30000, 0x04530000, 0x04530000)//b1+b2+b3,佔6M eden space 8192K, 79% used [0x03b30000, 0x04185f60, 0x04330000) from space 1024K, 0% used [0x04330000, 0x04330000, 0x04430000) to space 1024K, 0% used [0x04430000, 0x04430000, 0x04530000) tenured generation total 10240K, used 0K [0x04530000, 0x04f30000, 0x04f30000)//老年代為空 the space 10240K, 0% used [0x04530000, 0x04530000, 0x04530200, 0x04f30000) compacting perm gen total 12288K, used 2105K [0x04f30000, 0x05b30000, 0x08f30000) the space 12288K, 17% used [0x04f30000, 0x0513e478, 0x0513e600, 0x05b30000) No shared spaces configured. [GC [ParNew: 6487K->150K(9216K), 0.0092952 secs] 6487K->6294K(19456K), 0.0093314 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]//物件仍處於存活狀態,新生代無足夠的空間完成Minor GC,只能租借老年代的空間,將b1、b2和b3移動至老年代 Heap after GC invocations=1 (full 0): par new generation total 9216K, used 150K [0x03b30000, 0x04530000, 0x04530000)//新生代幾乎被清空 eden space 8192K, 0% used [0x03b30000, 0x03b30000, 0x04330000) from space 1024K, 14% used [0x04430000, 0x04455a10, 0x04530000) to space 1024K, 0% used [0x04330000, 0x04330000, 0x04430000) tenured generation total 10240K, used 6144K [0x04530000, 0x04f30000, 0x04f30000)//b1+b2+b3 the space 10240K, 60% used [0x04530000, 0x04b30030, 0x04b30200, 0x04f30000) compacting perm gen total 12288K, used 2105K [0x04f30000, 0x05b30000, 0x08f30000) the space 12288K, 17% used [0x04f30000, 0x0513e478, 0x0513e600, 0x05b30000) No shared spaces configured. } b4 over Heap par new generation total 9216K, used 4410K [0x03b30000, 0x04530000, 0x04530000)//b4 eden space 8192K, 54% used [0x03b30000, 0x03f82008, 0x04330000) from space 1024K, 14% used [0x04430000, 0x04455a10, 0x04530000) to space 1024K, 0% used [0x04330000, 0x04330000, 0x04430000) tenured generation total 10240K, used 6144K [0x04530000, 0x04f30000, 0x04f30000)//b1+b2+b3 the space 10240K, 60% used [0x04530000, 0x04b30030, 0x04b30200, 0x04f30000) compacting perm gen total 12288K, used 2116K [0x04f30000, 0x05b30000, 0x08f30000) the space 12288K, 17% used [0x04f30000, 0x051413c8, 0x05141400, 0x05b30000) No shared spaces configured.
二,大物件直接進入老年代
為了避免記憶體回收時大物件在Eden區和2個Survivor區之間的拷貝(ParNew收集器使用複製演算法),同時為了避免為了提供足夠的記憶體空間而提前觸發的GC,虛擬機器提供了-XX:PretenureSizeThreshold(該設定只對Serial和ParNew收集器生效)引數,大於該引數設定值的物件將直接在老年代分配
//-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintHeapAtGC -XX:+PrintGCDetails
//-XX:PretenureSizeThreshold=2097152
public class test {
static int mb = 1024*1024;
public static void main(String[] args) {
byte[] b1 = new byte[3*mb];
System.out.println("b1 over");
}
}
由於設定超過2M(2*1024*1024=2097152B)的物件直接在老年代分配,故b1將分配在老年代上
b1 over
Heap
par new generation total 9216K, used 507K [0x03b50000, 0x04550000, 0x04550000)//新生代幾乎為空
eden space 8192K, 6% used [0x03b50000, 0x03bcef00, 0x04350000)
from space 1024K, 0% used [0x04350000, 0x04350000, 0x04450000)
to space 1024K, 0% used [0x04450000, 0x04450000, 0x04550000)
tenured generation total 10240K, used 3072K [0x04550000, 0x04f50000, 0x04f50000)//老年代使用了3*1024K記憶體
the space 10240K, 30% used [0x04550000, 0x04850010, 0x04850200, 0x04f50000)
compacting perm gen total 12288K, used 2110K [0x04f50000, 0x05b50000, 0x08f50000)
the space 12288K, 17% used [0x04f50000, 0x0515f8c8, 0x0515fa00, 0x05b50000)
No shared spaces configured.
三,長期存活物件將進入老年代
由於虛擬機器垃圾收集是基於“分代演算法”的,故虛擬機器必須能夠識別哪些物件存放在新生代,哪些物件應該存放在老年代
虛擬機器設計了一個物件年齡計數器,如果物件在Eden區出生並且經過第一次Minor GC後依然存活,並且可以被Survivor區容納,就會被複制至Survivor區並將物件年齡設定為1。以後物件每熬過一次Minor GC,物件年齡便+1。當物件年齡超過物件晉升老年代的年齡閥值(該閥值預設為15)時,便會晉升至老年代,何時晉升,我們接下來研究
虛擬機器提供了-XX:MaxTenuringThreshold引數設定晉升閥值
//-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintHeapAtGC -XX:+PrintGCDetails
//-XX:MaxTenuringThreshold=1
public class test {
static int mb = 1024*1024;
public static void main(String[] args) {
System.out.println("step 1");
byte[] b1 = new byte[1*mb/4];
System.out.println("step 2");
byte[] b2 = new byte[4*mb];
System.out.println("step 3");
byte[] b3 = new byte[4*mb];//GC
System.out.println("step 4");
b3 = null;
System.out.println("step 5");
b3 = new byte[4*mb];//GC
}
}
b1、b2正常分配。在step3,新生代將沒有足夠的記憶體分配b3所需的4M空間,故引發一次Minor GC。b1只有256KB,可以放置在Survivor區中,故複製b1到Survivor區中,b2為4M,無法放置到Survivor區中,故租借老年代4M記憶體放置b2,回收新生代記憶體空間,b1經歷了一次Minor GC後依然存活,故年齡變為1。
在step4,分配給b3物件的記憶體空間依然被佔用,只是將b3物件的引用置為空,由於不涉及到記憶體分配,故而不涉及到GC,因此物件的年齡也不會發生變化
在step5,重新給b3物件分配4M空間,由於新生代沒有足夠記憶體,故引發Minor GC,step3分配給b3的4M記憶體空間由於不再與存活物件相關聯,將被回收,同時,由於b1的年齡到達物件晉升老年代的年齡設定,b1將被移動至老年代
step 1
step 2
step 3
{Heap before GC invocations=0 (full 0):
par new generation total 9216K, used 4695K [0x03b80000, 0x04580000, 0x04580000)//b1+b2
eden space 8192K, 57% used [0x03b80000, 0x04015f50, 0x04380000)
from space 1024K, 0% used [0x04380000, 0x04380000, 0x04480000)
to space 1024K, 0% used [0x04480000, 0x04480000, 0x04580000)
tenured generation total 10240K, used 0K [0x04580000, 0x04f80000, 0x04f80000)//此時老年代為空
the space 10240K, 0% used [0x04580000, 0x04580000, 0x04580200, 0x04f80000)
compacting perm gen total 12288K, used 2105K [0x04f80000, 0x05b80000, 0x08f80000)
the space 12288K, 17% used [0x04f80000, 0x0518e450, 0x0518e600, 0x05b80000)
No shared spaces configured.
[GC [ParNew: 4695K->409K(9216K), 0.0049519 secs] 4695K->4505K(19456K), 0.0049944 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap after GC invocations=1 (full 0):
par new generation total 9216K, used 409K [0x03b80000, 0x04580000, 0x04580000)//b1
eden space 8192K, 0% used [0x03b80000, 0x03b80000, 0x04380000)
from space 1024K, 39% used [0x04480000, 0x044e6610, 0x04580000)
to space 1024K, 0% used [0x04380000, 0x04380000, 0x04480000)
tenured generation total 10240K, used 4096K [0x04580000, 0x04f80000, 0x04f80000)//b2
the space 10240K, 40% used [0x04580000, 0x04980010, 0x04980200, 0x04f80000)
compacting perm gen total 12288K, used 2105K [0x04f80000, 0x05b80000, 0x08f80000)
the space 12288K, 17% used [0x04f80000, 0x0518e450, 0x0518e600, 0x05b80000)
No shared spaces configured.
}
step 4
step 5
{Heap before GC invocations=1 (full 0):
par new generation total 9216K, used 4669K [0x03b80000, 0x04580000, 0x04580000)//b1+b3(step3)
eden space 8192K, 52% used [0x03b80000, 0x03fa9098, 0x04380000)
from space 1024K, 39% used [0x04480000, 0x044e6610, 0x04580000)
to space 1024K, 0% used [0x04380000, 0x04380000, 0x04480000)
tenured generation total 10240K, used 4096K [0x04580000, 0x04f80000, 0x04f80000)//b2
the space 10240K, 40% used [0x04580000, 0x04980010, 0x04980200, 0x04f80000)
compacting perm gen total 12288K, used 2111K [0x04f80000, 0x05b80000, 0x08f80000)
the space 12288K, 17% used [0x04f80000, 0x0518fe08, 0x05190000, 0x05b80000)
No shared spaces configured.
[GC [ParNew: 4669K->43K(9216K), 0.0008256 secs] 8765K->4548K(19456K), 0.0008701 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap after GC invocations=2 (full 0):
par new generation total 9216K, used 43K [0x03b80000, 0x04580000, 0x04580000)//step3分配的b3物件空間被回收
eden space 8192K, 0% used [0x03b80000, 0x03b80000, 0x04380000)
from space 1024K, 4% used [0x04380000, 0x0438ad90, 0x04480000)
to space 1024K, 0% used [0x04480000, 0x04480000, 0x04580000)
tenured generation total 10240K, used 4505K [0x04580000, 0x04f80000, 0x04f80000)//b1+b2
the space 10240K, 43% used [0x04580000, 0x049e6590, 0x049e6600, 0x04f80000)
compacting perm gen total 12288K, used 2111K [0x04f80000, 0x05b80000, 0x08f80000)
the space 12288K, 17% used [0x04f80000, 0x0518fe08, 0x05190000, 0x05b80000)
No shared spaces configured.
}
Heap
par new generation total 9216K, used 4303K [0x03b80000, 0x04580000, 0x04580000)//b3(step5)
eden space 8192K, 52% used [0x03b80000, 0x03fa8fe0, 0x04380000)
from space 1024K, 4% used [0x04380000, 0x0438ad90, 0x04480000)
to space 1024K, 0% used [0x04480000, 0x04480000, 0x04580000)
tenured generation total 10240K, used 4505K [0x04580000, 0x04f80000, 0x04f80000)//b1+b2
the space 10240K, 43% used [0x04580000, 0x049e6590, 0x049e6600, 0x04f80000)
compacting perm gen total 12288K, used 2116K [0x04f80000, 0x05b80000, 0x08f80000)
the space 12288K, 17% used [0x04f80000, 0x051913c8, 0x05191400, 0x05b80000)
No shared spaces configured.
如果修改MaxTenuringThreshold的值為2,從列印日誌中可以發現,最終老年代的記憶體使用量為4096KB=4M,也就是說b1沒有晉升至老年代
上面是Minor GC的執行狀況,如果是Full GC呢:
//-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintHeapAtGC -XX:+PrintGCDetails
//-XX:MaxTenuringThreshold=1
public class test {
static int mb = 1024*1024;
public static void main(String[] args) {
byte[] b1 = new byte[1*mb/4];
System.gc();
}
}
這裡我們使用的是Full GC。
Full GC通常至少伴隨著一次Minor GC(並非絕對),看下面日誌,這裡的Minor GC應該至少發生了2次,一次Minor GC是不會把b1移動至老年代的
{Heap before GC invocations=0 (full 0):
par new generation total 9216K, used 599K [0x03b80000, 0x04580000, 0x04580000)//b1
eden space 8192K, 7% used [0x03b80000, 0x03c15f40, 0x04380000)
from space 1024K, 0% used [0x04380000, 0x04380000, 0x04480000)
to space 1024K, 0% used [0x04480000, 0x04480000, 0x04580000)
tenured generation total 10240K, used 0K [0x04580000, 0x04f80000, 0x04f80000)//老年代為空
the space 10240K, 0% used [0x04580000, 0x04580000, 0x04580200, 0x04f80000)
compacting perm gen total 12288K, used 2104K [0x04f80000, 0x05b80000, 0x08f80000)
the space 12288K, 17% used [0x04f80000, 0x0518e278, 0x0518e400, 0x05b80000)
No shared spaces configured.
[Full GC (System) [Tenured: 0K->404K(10240K), 0.0069434 secs] 599K->404K(19456K), [Perm : 2104K->2104K(12288K)], 0.0069992 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap after GC invocations=1 (full 1):
par new generation total 9216K, used 0K [0x03b80000, 0x04580000, 0x04580000)//新生代為空
eden space 8192K, 0% used [0x03b80000, 0x03b80000, 0x04380000)
from space 1024K, 0% used [0x04380000, 0x04380000, 0x04480000)
to space 1024K, 0% used [0x04480000, 0x04480000, 0x04580000)
tenured generation total 10240K, used 404K [0x04580000, 0x04f80000, 0x04f80000)//b1
the space 10240K, 3% used [0x04580000, 0x045e5130, 0x045e5200, 0x04f80000)
compacting perm gen total 12288K, used 2104K [0x04f80000, 0x05b80000, 0x08f80000)
the space 12288K, 17% used [0x04f80000, 0x0518e278, 0x0518e400, 0x05b80000)
No shared spaces configured.
}
Heap
par new generation total 9216K, used 327K [0x03b80000, 0x04580000, 0x04580000)
eden space 8192K, 4% used [0x03b80000, 0x03bd1f98, 0x04380000)
from space 1024K, 0% used [0x04380000, 0x04380000, 0x04480000)
to space 1024K, 0% used [0x04480000, 0x04480000, 0x04580000)
tenured generation total 10240K, used 404K [0x04580000, 0x04f80000, 0x04f80000)
the space 10240K, 3% used [0x04580000, 0x045e5130, 0x045e5200, 0x04f80000)
compacting perm gen total 12288K, used 2116K [0x04f80000, 0x05b80000, 0x08f80000)
the space 12288K, 17% used [0x04f80000, 0x05191190, 0x05191200, 0x05b80000)
No shared spaces configured.
四:動態物件年齡判定
為了使記憶體分配更加靈活,虛擬機器並不要求物件年齡達到MaxTenuringThreshold才晉升老年代
如果Survivor區中相同年齡所有物件大小的總和大於Survivor區空間的一半,年齡大於或等於該年齡的物件在Minor GC時將複製至老年代
//-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:MaxTenuringThreshold=10
//-XX:+PrintTenuringDistribution
public class Test {
static int mb = 1024*1024;
public static void main(String[] args) {
System.out.println("step 1");
byte[] b1 = new byte[1*mb/4];
byte[] b3 = new byte[4*mb];
byte[] b4 = new byte[4*mb];//GC
System.out.println("step 2");
byte[] b2 = new byte[1*mb/4];//可以嘗試1*mb/2,然後觀察日誌
b4 = null;
System.out.println("step 3");
b4 = new byte[4*mb];//GC
System.out.println("step 4");
b4 = null;
b4 = new byte[4*mb];//GC
}
}
先來介紹一個設定-XX:+PrintTenuringDistribution,這個引數很有意思,會在Minor GC時列印Survivor區記憶體容量的一半,晉升老年代年齡閥值,Survivor區中的物件大小以及物件年齡
根據啟動引數的設定,Survivor大小的一半是524288B,也就是512KB。第一次GC後,b1依然存活,故年齡變為1。第二次GC後,b1和b2依然存活,故b1的年齡變為2,b2的年齡為1。b1+b2的大小加起來超過了Survivor區容量的一半,此時會修改Survivor區晉升老年代年齡閥值為2(如果移動年齡為2的物件可以使Survivor去的記憶體使用降至512KB以內,則只移動年齡為2的物件,否則將會同時移動年齡為1的物件)。第三次GC時,將年齡等於晉升閥值的物件移動至老年代,執行GC,GC結束後,b1依然在Survivor區(當然可能從Survivor from區拷貝至了Survivor to區),此時b1的年齡變為2。這時Survivor區的使用記憶體沒有達到512M,修改Survivor區晉升老年代年齡閥值為引數設定的10。
step 1
Desired survivor size 524288 bytes, new threshold 10 (max 10)
- age 1: 412800 bytes, 412800 total
step 2
step 3
Desired survivor size 524288 bytes, new threshold 2 (max 10)
- age 1: 262160 bytes, 262160 total
- age 2: 412800 bytes, 674960 total
step 4
Desired survivor size 524288 bytes, new threshold 10 (max 10)
- age 1: 136 bytes, 136 total
- age 2: 262160 bytes, 262296 total
最後,為什麼在第三次GC後,Survivor區還存在一個大小為136B,年齡為1的被使用記憶體空間?
我猜測,雖然Minor GC時Survivor區沒有足夠的空間完成GC時會租借老年代的記憶體,但是在Survivor區依然儲存了一個指向老年代租借記憶體起始地址的引用
五:空間分配擔保
這個前面已經出現過多次了,由於新生代使用複製演算法,當Minor GC時如果存活物件過多,無法完全放入Survivor區,就會向老年代借用記憶體存放物件,以完成Minor GC
在觸發Minor GC時,虛擬機器會先檢測之前GC時租借的老年代記憶體的平均大小是否大於老年代的剩餘記憶體,如果大於,則將Minor GC變為一次Full GC,如果小於,則檢視虛擬機器是否允許擔保失敗(-XX:+/-HandlePromotionFailure。從jdk6.0開始,允許擔保失敗已變為HotSpot虛擬機器所有收集器預設設定,虛擬機器將不再識別該引數設定,詳見JDK-6990095 : Deprecate and eliminate -XX:-HandlePromotionFailure),如果允許擔保失敗,則只執行一次Minor GC,否則也要將Minor GC變為一次Full GC(直到GC結束時才能確定到底有多少物件需要被移動至老年代,所以在GC前,只能使用粗略的平均值進行判斷)