自动内存管理解决了两个问题:给对象分配内存以及回收对象的内存;下面介绍给对象分配内存的细节。
JVM堆内存模型
对象内存的分配主要在堆上进行,下面介绍JVM堆内存模型
堆分为三个部分:
年轻代(Young):又分为Eden和两个Survivor区,存放新分配的对象
老年代(Tenured):存放生命周期较长的对象,生命周期较长指经过了GC后仍然还存活的年轻代对象,会被移到该区
永久代(Perm):又叫方法区,存放类基本信息,一般不会溢出
对象内存的分配主要在堆的新生代Eden区,少数情况会直接分配到老年代。
3.6.1 对象优先在Eden上分配
多数情况下,对象在新生代Eden上分配。Eden空间不够,虚拟机发起一次Minor GC。
-XX:+PrintGCDetail 打印内存回收日志,并且在进程退出时输出当前内存各区域的分配情况
-XX:+SurvivorRation=8 设置新生代中Eden区与一个Survivor区的空间比例
代码:
public class TestAllocation {
private static final int _1MB = 1024*1024;
public static void main(String[] args){
testAllocation();
}
public static void testAllocation(){
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1 = new byte[2*_1MB];
allocation2 = new byte[2*_1MB];
allocation3 = new byte[2*_1MB];
allocation4 = new byte[4*_1MB];
}
}
运行参数: -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M
参数解释: UseSerialGC 虚拟机使用Serial+SerialOld收集器进行回收
Xms堆初始值
Xmx堆最大值
Xmn新生代大小(老年代=Xms/Xmx-Xmn)
结果:
[GC[DefNew: 6980K->464K(9216K), 0.0048934 secs] 6980K->6608K(19456K), 0.0049317 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4890K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
eden space 8192K, 54% used [0x00000000f9a00000, 0x00000000f9e52798, 0x00000000fa200000)
from space 1024K, 45% used [0x00000000fa300000, 0x00000000fa3741f0, 0x00000000fa400000)
to space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
tenured generation total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
the space 10240K, 60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)
compacting perm gen total 21248K, used 2514K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 11% used [0x00000000fae00000, 0x00000000fb074888, 0x00000000fb074a00, 0x00000000fc2c0000)
No shared spaces configured.
解释:
发生了一次Minor GC,新生代内存从6980K变为464K,但是总内存大小基本没变6980K-6608K,那是因为allocation1,allocation2,allocation3三个对象都存活,没有回收对象。
发生回收的原因为新生代不够为allocation4分配内存,此时根据复制算法需要将Eden+Survivor from复制到Survivor to区域,但是Survivor to不够放置3个2M的对象。此时出发分配担保机制,将这3个2M的对象转移到老年代。
所以最终的内存分配情况是:allocation4放在了eden区,allocation1,allocation2,allocation3放在了老年代。
3.6.2 大对象直接进入老年代
大对象指需要大量连续内存的对象,如字符串、数组。大对象会导致内存还有不少空间(但还不够)就会触发垃圾回收。
-XX:PretenureSizeThreshold 大于该值的对象直接在老年代中分配,避免在eden和两个Survivor区间发生复制
代码:
public class TestPretenureThreshold {
private static final int _1MB = 1024*1024;
public static void testPretenureSizeThreshold(){
byte[] allocation;
allocation = new byte[4 * _1MB];
}
//-XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=2M -Xms20M -Xmx20M -Xmn10M
public static void main(String[] args){
testPretenureSizeThreshold();
}
}
Heap
def new generation total 9216K, used 999K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
eden space 8192K, 12% used [0x00000000f9a00000, 0x00000000f9af9f70, 0x00000000fa200000)
from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
tenured generation total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
the space 10240K, 40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
compacting perm gen total 21248K, used 2511K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 11% used [0x00000000fae00000, 0x00000000fb073d08, 0x00000000fb073e00, 0x00000000fc2c0000)
No shared spaces configured.
3.6.3 长期存在的对象将进入老年代
虚拟机采用分代的思想管理内存,每个对象都存在年龄(Age)计数器,如果对象在eden出生并经过第一次Minor GC后依然存活,并被Survivor容纳的话,其年龄就会被设置为1,然后在Survivor区中每熬过一次Minor GC,年龄就会加1,当年龄增加到一定程度(默认15),就会晋升为老年代。
-XX:MaxTenuringThreshold 设置晋升为老年代的年龄阀值。
-XX:+PrintTenuringDistribution 输出Survivor中对象的年龄分布。
代码:
public class TestTenuringThreshold {
private static final int _1MB = 1024*1024;
public static void testTenuringThreshold(){
byte[] allocation1,allocation2,allocation3;
allocation1 = new byte[_1MB/4];
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
//-XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -Xms20M -Xmx20M -Xmn10M
public static void main(String[] args){
testTenuringThreshold();
}
结果:
[GC[DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 737872 bytes, 737872 total
: 5188K->720K(9216K), 0.0049431 secs] 5188K->4816K(19456K), 0.0050418 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC[DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 232 bytes, 232 total
: 5065K->0K(9216K), 0.0018952 secs] 9161K->4816K(19456K), 0.0019256 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4178K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
eden space 8192K, 51% used [0x00000000f9a00000, 0x00000000f9e14820, 0x00000000fa200000)
from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa2000e8, 0x00000000fa300000)
to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
tenured generation total 10240K, used 4816K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
the space 10240K, 47% used [0x00000000fa400000, 0x00000000fa8b4020, 0x00000000fa8b4200, 0x00000000fae00000)
compacting perm gen total 21248K, used 2514K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 11% used [0x00000000fae00000, 0x00000000fb074878, 0x00000000fb074a00, 0x00000000fc2c0000)
No shared spaces configured.
解释:
allocation1,allocation2被分配到eden区中,allocation3申请内存不够,触发Minor GC,allocation1 被移入Survivor中,内存还不够allocation2移入老年代
allocation3再次申请内存,allocation1会被移入老年代
最终新生代为allocation3 老年代为allocation1,allocation2
3.6.4 动态对象年龄判定
虚拟机并不永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升为老年代,如果Survivor中相同年龄所有对象大小总和大于Survivor空间一半,年龄大于或者等于该年龄的对象就可以直接进入老年代。
3.6.5 空间分配担保
在Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总和,如果成立,那么Minor GC是安全的。不成立,虚拟机会查看HandlePromotionFailure设置是否允许担保失败。
如果允许,继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,大于则尝试一次Minor GC,如果小于或者不允许冒险则进行一次Full GC。