内存分配与回收策略

其实Java技术体系中的自动内存管理本质上就是自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存内存的回收我们已经讲过,现在看看内存的分配。

内存分配是在堆里面进行的,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中。规则不固定,取决于使用的垃圾收集器组合及相关的参数。接下来我们讲解几条比较普遍的内存分配规则,并用相应的代码实现佐证。

在这之前有两个名词需要清除:

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,当然速度也很快。
  • 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。
1. 对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(即只针对新生代进行的GC)。虚拟机提供了 -XX:+PrintGCDetails参数,会告诉虚拟机在发生垃圾收集行为时打印内存回收日志。首先看一下我们要测试的代码:

private static final int _1MB = 1024 * 1024;

/**
 *VM参数:-verbose:gc -Xmx20M -Xmx20M -Xmn10M -Xx:+PrintGCDetails -XX:SurvivorRatio=8
 */
public static void testAllocation() {
	byte[] allocation1, allocation2, allocation3, allocation4;
	allocation1 = new byte[3 * _1MB];
	allocation2 = new byte[3 * _1MB];
	allocation3 = new byte[3 * _1MB];
	allocation4 = new byte[5 * _1MB];	//出现一次Minor GC
}
//GC的细节
[GC (Allocation Failure) [PSYoungGen: 7292K->800K(9216K)] 7292K->6952K(19456K), 0.0057665 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 800K->0K(9216K)] [ParOldGen: 6152K->6775K(10240K)] 6952K->6775K(19456K), [Metaspace: 2582K->2582K(1056768K)], 0.0079977 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 3072K->0K(9216K)] 9847K->9847K(19456K), 0.0036659 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 0K->0K(9216K)] [ParOldGen: 9847K->9846K(10240K)] 9847K->9846K(19456K), [Metaspace: 2583K->2583K(1056768K)], 0.0090421 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
//最后内存分布情况
Heap
 PSYoungGen      total 9216K, used 5202K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 63% used [0x00000000ff600000,0x00000000ffb14930,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 9846K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 96% used [0x00000000fec00000,0x00000000ff59dbd8,0x00000000ff600000)
 Metaspace       used 2589K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 282K, capacity 386K, committed 512K, reserved 1048576K

通过VM参数我们指定了堆大小为20MB,不可扩展,10MB给新生代,剩下10MB给老年代。参数SurvivorRatio为0.8代表新生代中Eden区与一个Survivor区的空间比例是8:1,在上面的最后内存分布情况能看出“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代的总可用空间为9216K(Eden区+1个Survivor区的总容量)。

说实话上面的GC细节有一些我也没太看明白,但是看看下面那部分的最后内存分配情况。新生代用了5202K,几乎相当于5MB的大小,其实也就是数组allocation4;而老年代用了9846K,几乎相当于9MB,其实就是前三个数组。因为申请最后一个数组前,新生代均不满足分配要求,所以会通过分配担保机制提前将这三个数组依次转移到老年代中。

2. 大对象直接进入老年代

所谓的大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是很长的字符串和数组。虚拟机“最烦”大对象,更烦朝生夕灭的短命大对象。虚拟机提供了一个XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制

当然要注意的是,这个参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数。

3. 长期存活的对象将进入老年代

因为虚拟机采用了分代收集的思想,那就应该能识别哪些对象应放在新生代,哪些放在老年代。为了做到这一点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过一次Minor GC后仍然存活,且能被Survivor容纳,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTrnuringThreshold设置。在我们讲HotSpot虚拟机中对象的产生和访问时我们说过,对象的内存布局,其中一部分就是对象头,而GC分代年龄就放在对象头里,和这里是对应的。

4. 动态对象年龄判定

当然为了适应各种不同情况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

5. 空间分配担保

为什么会有空间分配担保,是因为每次针对新生代的Minor GC都有可能因为那个to空间不够用,而将剩余存活的对象放入老年代,因此我们要保证老年代能够放在Minor GC后所存活的对象。

这个过程有3个步骤:

  1. 发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果它成立,那么Minor GC是保证安全的。因为就算新生代的所有对象都会继续存活,那么Minor GC后将所有对象放入老年代也是够用的,所以是安全的。
  2. 如果不成立即小于,那么虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代的最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次Minor GC,但是有风险的
  3. 如果HandlePromotionFailure参数设置不允许,或者2中是小于,那么要改为一次Full GC

说一下2中的Minor GC为什么有风险,因为这里使用的是一个经验值,也就是虽然老年代空间小于新生代所有对象的空间总和,但是我把以往晋升到老年代对象容量的平均值大小作为经验值,认为这次从新生代晋升到老年代的所有对象大小和这个平均值差不多(或者说小于平均值),那么就继续Minor GC。当然如果大于这个平均值,那么就要进行Full GC从老年代里腾出空间,以便新生代中对象的晋升。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值