概要
在Java技术体系中,自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。
本文主要探讨给对象分配内存这点事。对象的内存分配,往大的方向上讲,就是在堆上分配。但是也有一些情况会在栈上分配和在TLAB上分配等。所以分配的规则并不是百分百固定的,其细节取决于当前使用的是哪一种GC组合和虚拟机中与内存相关的参数的设置。
- 对象优先分配在Eden分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 动态对象年龄判定
现代商业虚拟机都将Java堆分为新生代和老年代,在新生代中,一般采用复制算法。在老年代中,一般采用标记-整理算法。
新生代的堆空间又划分为一块较大的Eden空间和两块较小的Survivor空间如上图所示,每次只使用Eden和其中的一块Survivor(因为采用复制算法,所以另外一块Survivor拿来做保留区域)。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,就是说每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存是会被“浪费”的。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)来进行分配担保,一般情况下的GC策略如下图:

废话少说,下面开始正文
对象优先分配在Eden分配
先运行下面代码,然后结合日志来分析。以下代码的开头都注释了执行时所需要设置的虚拟机启动参数,这些参数对实验结果有直接影响,请调试代码的时候不要忽略掉.本例子介绍的比较详细,后面几个例子中重复的内容就不在细讲。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
class
TestGC1 {
private
static
final
int
_1MB =
1024
*
1024
;
/**
* VM ARGS: -verbose:gc - Xms20m -Xmx20m -Xmn10m - XX:SurvivorRatio=8 -XX:+PrintGCDetails
*/
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];
}
public
static
void
main(String[] args) {
testAllocation();
}
}
|
查看输出日志
先解释一下各个参数的意义:
-verbose:gc 表示输出虚拟机中的GC的情况 (红色方框部分)
-Xms20m 表示初始堆初始值
-Xmx20m 表示初始堆最大值 (- Xms和-Xmx的值相等时,表明堆空间大小为20m且不可扩展)
-Xmn10m 表示老年代的堆大小
-XX:SurvivorRatio=8 表明新生代中Eden区和Survivor区的空间比例为8:1
-XX:+PrintGCDetails 打印GC详细情况 (蓝色方框部分)
在testAllocation()方法中,尝试分配3个2MB大小和1个4MB大小的对象。当分配allocation4对象时候发生一次Minor GC.详细情况看日志(红色方框部分),关于各个数字的解释参考http://blog.youkuaiyun.com/alivetime/article/details/6895537 。这次GC的结果是新生代从6487KB变为149KB,而总内存占用量则几乎没有减少(因为allocation1,allocation2,allocation3三个对象都依旧存活)。这次Minor GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩下的2MB不足已分配,因此发生Minor GC.
GC期间虚拟机又发现已有的3个2MB大小的对象无法存入Survior空间,所以只好通过分配担保机制提前转移到老年代中去。
此时,老年代的空间为10MB足够分配给3个2MB大小的对象,因此不会执行Full GC
接下来,虚拟机又会查看HandlePromotionFailure设置是否允许担保失败,默认设置为ture。所以最终执行Minor GC。执行过程如下图红色线头所示

这次GC结束后,4MB的allocation4对象被顺利分配在Eden中。因此程序执行完的结果是Eden占用4MB,Survivor空间,老年代被占用6MB。
大对象直接进入老年代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
TestGC2 {
private
static
final
int
_1MB =
1024
*
1024
;
/**
* VM ARGS: - verbose:gc - Xms20m -Xmx20m - Xmn10m - XX:SurvivorRatio=8 - XX:+PrintGCDetails
* - XX:PretenureSizeThreshold=3145728
*/
public
static
void
testPretenureSizeThreshold(){
byte
[] allocation;
allocation =
new
byte
[
4
* _1MB];
}
public
static
void
main(String[] args) {
testPretenureSizeThreshold();
}
}
|
虚拟机提供了一个- XX:PretenureSizeThreshold参数,令大于这个设置的对象直接分配到老年代中。所以在testPretenureSizeThreshold()方法中 4MB的allocation对象被之间分配到老年代中。日志红色部分证明这一点
长期存活的对象进入老年代
为了能够识别哪些对象应当放在新生代,哪些对象应该放在老年代,虚拟机给每个对象定义了一个对象年龄技术器。
如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
class
TestGC3 {
private
static
final
int
_1MB =
1024
*
1024
;
/**
* VM ARGS: - verbose:gc - Xms20m -Xmx20m - Xmn10m - XX:SurvivorRatio=8 - XX:+PrintTenuringDistribution
* - XX:MaxTenuringThreshold=1 - XX:+PrintGCDetails
*/
public
static
void
testMaxTenuringThreshold(){
byte
[] allocation1, allocation2,allocation3 ;
allocation1 =
new
byte
[ _1MB /
4
];
allocation2 =
new
byte
[ _1MB *
4
];
allocation3 =
new
byte
[ _1MB *
4
];
allocation3 =
null
;
allocation3 =
new
byte
[ _1MB *
4
];
}
public
static
void
main(String[] args) {
testMaxTenuringThreshold();
}
|
以MaxTenuringThreshold=1运行输出日志
以MaxTenuringThreshold=15运行输出日志
在方法testMaxTenuringThreshold()中,对象allocation1需要256KB的内存空间。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年,新生代的Survivor空间变为0KB。
当MaxTenuringThreshold=15时,allocation1对象在第二次GC发生时仍然留在新生代,新生代的Survivor空间变为404KB
动态对象年龄判定
为了能更好地适应不同程序的内存情况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold要求的年龄。
运行下面代码并输出GC日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public
class
TestGC4 {
private
static
final
int
_1MB =
1024
*
1024
;
/**
* VM ARGS: - verbose:gc - Xms20m -Xmx20m - Xmn10m - XX:SurvivorRatio=8 - XX:+PrintTenuringDistribution
* - XX:MaxTenuringThreshold=1 - XX:+PrintGCDetails
*/
public
static
void
testMaxTenuringThreshold(){
byte
[] allocation1, allocation2,allocation3 ,allocation4;
allocation1 =
new
byte
[ _1MB /
4
];
allocation2 =
new
byte
[ _1MB /
4
];
allocation3 =
new
byte
[ _1MB *
4
];
allocation4 =
new
byte
[ _1MB *
4
];
allocation4 =
null
;
allocation4 =
new
byte
[ _1MB *
4
];
}
public
static
void
main(String[] args) {
testMaxTenuringThreshold();
}
}
|
输出GC日志
在日志中,我们发现Survivor空间的使用率为0,也就是说allocation1 ,allocation2对象都进入到了老年代中,而没有等到15岁的年龄。
---------------------------------------------全文完----------------------------------------------------