对象的内存分配主要是在新生带的Eden上,如果启动了本地线程分配缓冲,就按线程优先级分配在TLAB上,还会有少数情况直接分配在老年代。内存分配的规则不是固定了,细节还是取决于垃圾收集器组合。
下面是一些普遍的内存分配规则:
- 对象优先在Eden分配
- 大对象直接进入老年代(PretenureSizeThreshold)
- 长期存活的对象将进入老年代(MaxTenuringThreshold)
- 动态对象年龄判断(相等年龄对象占Survivor空间一半)
- 空间分配担保流程
但是这些规则并不是固定的,还是会根据具体情况而定,程序测试会将JVM参数卸载方法前的注释中。
一、对象优先在Eden分配
一般情况下对象优先在Eden上分配,如果Eden空间不足,就进行一次MinorGC。
Minor GC指发生在新生代的垃圾回收,Minor GC比较频繁,一般回收速度也比较快。Major GC/Full GC指发生在老年代的GC,经常会伴随至少一次的Minor GC(非绝对),Major GC的速度一般比Minor GC慢十倍以上。
设置注释中的参数,此时新生代老年代分别有10m的空间,其中新生代的Eden和Survivor比为8:1,所以此时Eden只有8m。按照上面的观点,在分配a4时,Eden剩余空间已经不足容下a4,所以会进行一次Minor GC。
在我的测试环境下,程序中什么都不写时,堆的Eden中也会有2.5546875m的空间被使用,并且这2点多m在GC时能回收掉一部分。
public class EdenTest {
// -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
// -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] a1, a2, a3, a4;
a1 = new byte[_1MB];
a2 = new byte[_1MB];
a3 = new byte[_1MB];
a4 = new byte[5 * _1MB];
}
}
运行程序,查看GC日志。
// 发生了Minor GC,新生代使用空间大小从5238k变成了655k
[GC (Allocation Failure) [DefNew: 5238K->655K(9216K), 0.0029147 secs] 5238K->3727K(19456K), 0.0029604 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 5857K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
// eden中存放着a4
eden space 8192K, 63% used [0x00000000fec00000, 0x00000000ff114930, 0x00000000ff400000)
from space 1024K, 64% used [0x00000000ff500000, 0x00000000ff5a3e00, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
// 老生代使用的空间正好为3m,也就是a1、a2、a3移入了老年代
tenured generation total 10240K, used 3072K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 30% used [0x00000000ff600000, 0x00000000ff900030, 0x00000000ff900200, 0x0000000100000000)
Metaspace used 3494K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
GC日志表现得和推论差不多。程序运行中的内存分配主要是这样一个步骤:
- 一些未知的东西在eden中分配占用了2m多。
- a1分配进入eden。
- a2分配进入eden。
- a3分配进入eden。
- eden剩余空间不足安置a4,发起一次Minor GC。
- 未知的东西一部分被回收,一部分被移入Survivor(小于1m的部分)。
- a1存在引用,不会被回收,但是Survivor装不下,通过分配担保机制直接进入老年代。
- a2存在引用,不会被回收,但是Survivor装不下,通过分配担保机制直接进入老年代。
- a3存在引用,不会被回收,但是Survivor装不下,通过分配担保机制直接进入老年代。
二、大对象直接进入老年代
大对象就是分配一段连续大大空间。大对象的经常出现容易导致内存中还有不少空间时提前出发GC,来保证能够获取足够的连续空间,写程序时应该避免。
Serial和ParNew提供了PretenureSizeThreshold参数,当分配大于等于这个参数值的对象时,就会直接将对象分配在老年代中。
设置PretenureSizeThreshold为3m,运行下面程序,a1应该被直接分配在老年代,哪怕此时新生代的空间足够存放a1。
public class ThresholdTest {
// -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
// -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
// -XX:PretenureSizeThreshold=3145728
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] a1 = new byte[3 * _1MB];
}
}
看输出的GC日志,确实a1被分配在了老年区。
Heap
def new generation total 9216K, used 2330K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 28% used [0x00000000fec00000, 0x00000000fee469f8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 3072K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
// 老年代有30%的空间被使用,正好是3m,也就是a1直接分配在这里。
the space 10240K, 30% used [0x00000000ff600000, 0x00000000ff900010, 0x00000000ff900200, 0x0000000100000000)
Metaspace used 3494K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
三、长期存活的对象将进入老年代
分代管理其实是虚拟机给每个对象都定义了一个对象年龄计数器,每经过一次Minor GC且没有被回收,这个计数器就会加一,当到达一定大小时,这个对象就会被移到老年代中,阈值通过-XX:MaxTenuringThreshold设置。
还是因为那未知的2m多,我调整了一下书上的测试程序。
设置MaxTenuringThreshold为1,所以下面经历了两次GC后,新生代区的所有内容都应该进入老年代。
public class MaxTenuringTest {
// -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
// -Xms40M -Xmx40M -Xmn20M -XX:SurvivorRatio=8
// -XX:MaxTenuringThreshold=1
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] a1, a2, a3;
a1 = new byte[_1MB / 512];
a2 = new byte[8 * _1MB];
a3 = new byte[8 * _1MB];
a3 = null;
a3 = new byte[8 * _1MB];
}
}
确实,在第二次GC后,新生代全部空了,因为已经达到阈值,晋升到老年代了。
[GC (Allocation Failure) [DefNew: 10749K->911K(18432K), 0.0052187 secs] 10749K->9103K(38912K), 0.0052600 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
// 新生代清空
[GC (Allocation Failure) [DefNew: 9103K->0K(18432K), 0.0011062 secs] 17295K->9099K(38912K), 0.0011249 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 18432K, used 8356K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
eden space 16384K, 51% used [0x00000000fd800000, 0x00000000fe0290e0, 0x00000000fe800000)
from space 2048K, 0% used [0x00000000fe800000, 0x00000000fe800000, 0x00000000fea00000)
to space 2048K, 0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000fec00000)
tenured generation total 20480K, used 9099K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
the space 20480K, 44% used [0x00000000fec00000, 0x00000000ff4e2e80, 0x00000000ff4e3000, 0x0000000100000000)
Metaspace used 3494K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
调整参数为10后日志如下,新生代仍有内容。
[GC (Allocation Failure) [DefNew: 10749K->911K(18432K), 0.0047404 secs] 10749K->9103K(38912K), 0.0048049 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
// 新生代内容保持着
[GC (Allocation Failure) [DefNew: 9103K->907K(18432K), 0.0010164 secs] 17295K->9099K(38912K), 0.0010356 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
Heap
def new generation total 18432K, used 9591K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
eden space 16384K, 53% used [0x00000000fd800000, 0x00000000fe07afb0, 0x00000000fe800000)
from space 2048K, 44% used [0x00000000fe800000, 0x00000000fe8e2e70, 0x00000000fea00000)
to space 2048K, 0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000fec00000)
tenured generation total 20480K, used 8192K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
the space 20480K, 40% used [0x00000000fec00000, 0x00000000ff400010, 0x00000000ff400200, 0x0000000100000000)
Metaspace used 3494K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
四、动态对象年龄判定
为了更好地适应不同程序的内存状况,虚拟机并不是永远需要对象年龄到达MaxTenuringThreshold大小时才能进入老年代。
如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半时,年龄大于等于该值的所有对象可以进入老年代。
下面程序中a1加未知的空间已经大于一个Survivor的一半空间了,所以在第二次GC时会判定这两部分进入老年代,哪怕MaxTenuringThreshold设置的是10。
public class DynamicTest {
// -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
// -Xms40M -Xmx40M -Xmn20M -XX:SurvivorRatio=8
// -XX:MaxTenuringThreshold=10
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] a1, a2, a3;
a1 = new byte[_1MB];
a2 = new byte[8 * _1MB];
a3 = new byte[8 * _1MB];
a3 = null;
a3 = new byte[8 * _1MB];
}
}
第二次GC后新生代为空,全部进入老年代了。
[GC (Allocation Failure) [DefNew: 11517K->1679K(18432K), 0.0052658 secs] 11517K->9871K(38912K), 0.0053102 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
// GC后新生代为空
[GC (Allocation Failure) [DefNew: 9871K->0K(18432K), 0.0014142 secs] 18063K->9867K(38912K), 0.0014360 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 18432K, used 8356K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
eden space 16384K, 51% used [0x00000000fd800000, 0x00000000fe0290e0, 0x00000000fe800000)
from space 2048K, 0% used [0x00000000fe800000, 0x00000000fe800000, 0x00000000fea00000)
to space 2048K, 0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000fec00000)
tenured generation total 20480K, used 9867K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
the space 20480K, 48% used [0x00000000fec00000, 0x00000000ff5a2e80, 0x00000000ff5a3000, 0x0000000100000000)
Metaspace used 3494K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
五、空间分配担保
在JDK1.6之后,发生Minor GC之前,虚拟机会检查老年代连续空间是否大于新生代对象总大小或历次晋升平均大小,如果连续空间大,则直接Minor GC,否则会先进行一次Full GC,来提高分配担保的成功率。
取晋升平均值其实是一种动态概率的手段,但是无法完全避免担保失败的,如果出现了某次Minor GC存活对象远大于均值,就失败了,失败后会重新发起一次Full GC。
在JDK1.6之前,有一个HandlePromotionFailure(是否允许分配担保失败)的参数,在判断完老年代连续空间小于新生代对象总大小后,会判断是否设置此参数。如果允许分配担保失败才会去检查老年代连续空间是否大于平均晋升大小。在JDK1.6之后,修改了HotSpot中空间分配检查的代码片段,这个参数不再起作用。