Gear with Code Java Engineer

JavaGC动态对象年龄判定

2019-11-23

题目借用了《深入理解Java虚拟机第二版》中的3.6.4节的题目。这个题目看上去让人比较迷惑,意思像是:有一种对象叫“动态对象”,现在我们要对它的年龄进行判定。实际上这节要说的不是这个意思,如果非要用这八个字,至少应该改成“对象年龄动态判定”。但是,这样描述也不够准确,准确的描述是:在MinorGC过程中,能够留在新生代的对象的年龄上限的动态判定。具体是怎么回事呢,我们一起来看看吧。

1 书中的说法

首先不得不提一下,书中实验是基于Serial/Serial Old收集器,而本文中使用的是Parallel Scavengee/Parallel Old,虽然使用的收集算法相同,新生代使用复制算法,老年代使用标记-整理算法,但可能由于不同的收集器策略不同,导致结果不同。因此并不能明说书中说的就一定不对。

书中这一节的原话是这样说的:

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

关键是这一句“如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代”

从这句话中可以得出这样的结论——这个“相同年龄”只能是1,因为如果年龄为2的对象之和大于Suivivor空间的一半的话,那么它们在上一次MinorGC中就应该已经被送到老年代了,否则就和此规则自相矛盾。

这句话中还有一个不清楚的地方,这个Survivor空间指的是FromSurvivor还是ToSurvivor?这两种情况的行为是不同的,主要表现在判定是否包括Eden区的对象

  • 推论1:

    如果是FromSurvior,表示判定发生在Eden区和FromSurvivor区对象进入ToSurvivor区之前,可以这样理解——在MinorGC开始时,首先对FromSurvivor区进行判定,如果其中年龄为1的对象之和大于等于FromSurvivor区空间的一半,就将FromSurvivor区中的所有对象(所有对象年龄都大于等于1)移到老年代,再处理Eden区。

  • 推论2:

    如果是ToSuivivor,表示判定发生在Eden区和FromSurvivor区对象进入ToSurvivor区之后,可以这样理解——在MinorGC时,若Eden区(只有Eden区的对象年龄为1)进入ToSurvivor区的对象之和大于等于ToSurvivor区空间的一半,就将新生代所有的对象(所有对象年龄都大于等于1)都移到老年代。

下面通过实验来验证一下吧。

2 书中案例


/**

 * VM参数 :

 * -verbose:gc

 * -Xms20M

 * -Xmx20M

 * -Xmn10M

 * -XX:SurvivorRatio=8

 * -XX:+PrintGCDetails

 * -XX:+UseSerialGC

 * -XX:MaxTenuringThreshold=15

 * -XX:+PrintTenuringDistribution

 */

public class Main {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {

        // 先进行两次Full GC,排除自动生成的其它对象的影响
        System.gc();
        System.gc();
        
        byte[] b1, b2, b3, b4;

        b1 = new byte[_1MB / 4];    //256K

        b2 = new byte[_1MB / 4];    //256K

        b3 = new byte[4 * _1MB];

        // 此处发生第一次MinorGC
        b4 = new byte[4 * _1MB];

        b4 = null;

        // 此处发生第二次MinorGC
        b4 = new byte[4 * _1MB];

    }
}

GC日志如下:

[Full GC (System.gc()) [Tenured: 0K->520K(10240K), 0.0018223 secs] 984K->520K(19456K), [Metaspace: 2639K->2639K(1056768K)], 0.0018733 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [Tenured: 520K->520K(10240K), 0.0020463 secs] 520K->520K(19456K), [Metaspace: 2639K->2639K(1056768K)], 0.0020832 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

[GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15)

age 1: 524320 bytes, 524320 total
4771K->512K(9216K), 0.0020692 secs] 5292K->5128K(19456K), 0.0020896 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
4608K->0K(9216K), 0.0003543 secs] 9224K->5128K(19456K), 0.0003693 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] Heap def new generation total 9216K, used 4260K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 52% used [0x00000000fec00000, 0x00000000ff0290e0, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 5128K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 50% used [0x00000000ff600000, 0x00000000ffb022e0, 0x00000000ffb02400, 0x0000000100000000) Metaspace used 2645K, capacity 4486K, committed 4864K, reserved 1056768K class space used 282K, capacity 386K, committed 512K, reserved 1048576K

观察字体加粗的几个地方:

  1. 在两次Full GC之后

    新生代:0

    老年代:520k,是系统自动生成的对象

    总共:520k

  2. 第一次MinorGC之后

    新生代:512k,是两个256k的对象

    老年代:4616k = 4m + 520k

    总共:5128k = 512k + 4616k

    这里新生代的512k是从Eden区进入ToSurvivor区的,并且等于ToSurvivor区的一半了,但是并没有进入老年代,可见推论2是错误的

    这里的512k恰好等于ToSurvivor区的一半,如果是大于呢?我们将两个256k的对象其中的一个稍微调大一点验证一下:

    b1 = new byte[_1MB / 4 + _1MB / 8];    //256K + 128k
    

    修改后的第一次MinorGC:

    [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15)

    • age 1: 655392 bytes, 655392 total
      4899K->640K(9216K), 0.0024489 secs] 5420K->5256K(19456K), 0.0024721 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    640k的对象还是进入了ToSurvivor区,由此可以完全证明推论2是错误的

  3. 第二次MinorGC之后

    新生代:0

    老年代:5128k = 512k + 4096k + 520k

    总共:5128k

    这说明新生代Survivor区中的512k对象被移到了老年代,而这512k对象的年龄并没有超过15,这符合推论1的猜想:第二次MinorGC开始时,这512k对象年龄为1,总和大于FromSurvivor区的一半,直接被移动到了老年代。但是仅靠这样一次实验不能就确定推论1是正确的,还需要继续实验。

3 验证推论1

我们知道,验真是困难的,验伪是方便的,所以我们以验伪为目的设计实验,若验伪成功,则推论1不正确,若验伪失败,则推论1可能是正确的。

  1. 实验设计

    触发三次MinorGC:

    • 第一次放入256k对象到Survivor区

    • 第二次再放入256k对象到Survivor区

    • 第三次什么也不放,观察GC日志,此时年龄为1的对象只有256k,不满足推论1的条件,若此时仍有对象进入老年代,则推论1不正确,否则,推论1可能正确。

    • 为了防止老年代空间不足触发FullGC,我们将老年代空间设大一些

      -Xms50M

      -Xmx50M

      -Xmn10M

  2. 代码验证

    public class JVMtest {
        private static final int _1MB = 1024 * 1024;
       
        public static void main(String[] args) {
       
            // 先进行两次Full GC,排除自动生成的其它对象的影响
            System.gc();
            System.gc();
       
            byte[] trigger = new byte[_1MB * 4]; // 用于触发MinorGC
       
            byte[] b1 = new byte[_1MB * 1 / 4]; // 256k
       
            // MinorGC1
            trigger = new byte[_1MB * 4];
       
            byte[] b2 = new byte[_1MB * 1 / 4]; // 256k
       
            // MinorGC2
            trigger = new byte[_1MB * 4];
               
            // MinorGC3
            trigger = new byte[_1MB * 4];
        }
    }
    

    GC日志:

    [Full GC (System.gc()) [Tenured: 0K->520K(40960K), 0.0019991 secs] 984K->520K(50176K), [Metaspace: 2638K->2638K(1056768K)], 0.0020526 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [Tenured: 520K->520K(40960K), 0.0014560 secs] 520K->520K(50176K), [Metaspace: 2638K->2638K(1056768K)], 0.0014887 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15)

    age 1: 262160 bytes, 262160 total
    4515K->256K(9216K), 0.0023849 secs] 5036K->4872K(50176K), 0.0024091 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 2 (max 15)

    age 1: 262160 bytes, 262160 total

    age 2: 262160 bytes, 524320 total
    4694K->512K(9216K), 0.0024963 secs] 9311K->9224K(50176K), 0.0025229 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]

    [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15)

    age 2: 262160 bytes, 262160 total
    4608K->256K(9216K), 0.0024041 secs] 13320K->13320K(50176K), 0.0024231 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4516K [0x00000000fce00000, 0x00000000fd800000, 0x00000000fd800000) eden space 8192K, 52% used [0x00000000fce00000, 0x00000000fd2290e0, 0x00000000fd600000) from space 1024K, 25% used [0x00000000fd700000, 0x00000000fd740010, 0x00000000fd800000) to space 1024K, 0% used [0x00000000fd600000, 0x00000000fd600000, 0x00000000fd700000) tenured generation total 40960K, used 13064K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000) the space 40960K, 31% used [0x00000000fd800000, 0x00000000fe4c22f0, 0x00000000fe4c2400, 0x0000000100000000) Metaspace used 2644K, capacity 4486K, committed 4864K, reserved 1056768K class space used 282K, capacity 386K, committed 512K, reserved 1048576K
  3. 结论

    可以看到,在第三次MinorGC之后,年龄为1的对象只有256k,但仍然有256k的对象进入了老年代,这证明推论1是错误的

    继续观察此次的GC日志,有以下几个值得关注的地方:

    • 在第三次GC之后,留下了age为2(在第二次GC中为1)的对象,也就是说,将age为3的对象放入了老年代。
    • 三次GC日志中,有一个new threshold,这个值从15变到2,再变到15。而虚拟机参数中有一个MaxTenuringThreshold,这个参数表示对象经过多少次GC之后就进入老年代,准确的说是如果对象年龄大于MaxTenuringThreshold就进入老年代

    根据以上两个地方可以提出一个猜想:

    猜想1:

    在每次MinorGC之后会重新计算一次MaxTenuringThreshold,计算规则是:将Survivor区中的对象按照年龄从小到大的顺序累加,当累加到某个年龄X的时候,若累加值大于等于Sruvivor空间的一半,就将MaxTenuringThreshold设为X。在下一次MinorGC时(对象年龄+1之后),年龄大于MaxTenuringThreshold的对象就进入老年代。

4 验证猜想1

仍然通过验伪的方式区来证明。若验伪成功,则猜想1不正确,若验伪失败,则猜想1可能是正确的。

  1. 实验设计

    • 虚拟机参数与上次相同。
    • 触发多次MinorGC,每次向Survivor区放入一个128k的对象。
    • 观察GC日志,在第四次MinorGC之后,MaxTenuringThreshold应该被设置为4,第五次MinorGC的时候,age为5的对象会被放入老年代,age1到age4的对象会留在Survivor区,此后的现象与第五次相同。
  2. 代码验证

    public class JVMtest {
        private static final int _1MB = 1024 * 1024;
       
        public static void main(String[] args) {
       
            // 先进行两次Full GC,排除自动生成的其它对象的影响
            System.gc();
            System.gc();
       
            byte[] trigger = new byte[_1MB * 4]; // 用于触发MinorGC
       
            byte[] b1 = new byte[_1MB * 1 / 8]; // 128k
       
            // MinorGC1
            trigger = new byte[_1MB * 4];
       
            byte[] b2 = new byte[_1MB * 1 / 8]; // 128k
            // MinorGC2
            trigger = new byte[_1MB * 4];
               
            byte[] b3 = new byte[_1MB * 1 / 8]; // 128k
            // MinorGC3
            trigger = new byte[_1MB * 4];
               
            byte[] b4 = new byte[_1MB * 1 / 8]; // 128k
            // MinorGC4
            trigger = new byte[_1MB * 4];
               
            byte[] b5 = new byte[_1MB * 1 / 8]; // 128k
            // MinorGC5
            trigger = new byte[_1MB * 4];
               
            byte[] b6 = new byte[_1MB * 1 / 8]; // 128k
            // MinorGC6
            trigger = new byte[_1MB * 4];
               
            byte[] b7 = new byte[_1MB * 1 / 8]; // 128k
            // MinorGC7
            trigger = new byte[_1MB * 4];
        }
    }
       
    

    GC日志:

    [Full GC (System.gc()) [Tenured: 0K->520K(40960K), 0.0020383 secs] 984K->520K(50176K), [Metaspace: 2638K->2638K(1056768K)], 0.0020944 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [Tenured: 520K->520K(40960K), 0.0012953 secs] 520K->520K(50176K), [Metaspace: 2638K->2638K(1056768K)], 0.0013209 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15)

    age 1: 131088 bytes, 131088 total
    4259K->128K(9216K), 0.0020922 secs] 4780K->4744K(50176K), 0.0021162 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15)

    age 1: 131088 bytes, 131088 total

    age 2: 131088 bytes, 262176 total
    4433K->256K(9216K), 0.0023702 secs] 9050K->8968K(50176K), 0.0023925 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15)

    age 1: 131088 bytes, 131088 total

    age 2: 131088 bytes, 262176 total

    age 3: 131088 bytes, 393264 total
    4590K->384K(9216K), 0.0025596 secs] 13303K->13192K(50176K), 0.0025908 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]

    [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 4 (max 15)

    age 1: 131088 bytes, 131088 total

    age 2: 131088 bytes, 262176 total

    age 3: 131088 bytes, 393264 total

    age 4: 131088 bytes, 524352 total
    4735K->512K(9216K), 0.0023926 secs] 17544K->17416K(50176K), 0.0024205 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 4 (max 15)

    age 1: 131088 bytes, 131088 total

    age 2: 131088 bytes, 262176 total

    age 3: 131088 bytes, 393264 total

    age 4: 131088 bytes, 524352 total
    4874K->512K(9216K), 0.0023385 secs] 21779K->21640K(50176K), 0.0023622 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 4 (max 15)

    age 1: 131088 bytes, 131088 total

    age 2: 131088 bytes, 262176 total

    age 3: 131088 bytes, 393264 total

    age 4: 131088 bytes, 524352 total
    4881K->512K(9216K), 0.0027198 secs] 26010K->25864K(50176K), 0.0027429 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 4 (max 15)

    age 1: 131088 bytes, 131088 total

    age 2: 131088 bytes, 262176 total

    age 3: 131088 bytes, 393264 total

    age 4: 131088 bytes, 524352 total
    4886K->512K(9216K), 0.0022169 secs] 30239K->30088K(50176K), 0.0022344 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4772K [0x00000000fce00000, 0x00000000fd800000, 0x00000000fd800000) eden space 8192K, 52% used [0x00000000fce00000, 0x00000000fd2290e0, 0x00000000fd600000) from space 1024K, 50% used [0x00000000fd700000, 0x00000000fd780040, 0x00000000fd800000) to space 1024K, 0% used [0x00000000fd600000, 0x00000000fd600000, 0x00000000fd700000) tenured generation total 40960K, used 29576K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000) the space 40960K, 72% used [0x00000000fd800000, 0x00000000ff4e2350, 0x00000000ff4e2400, 0x0000000100000000) Metaspace used 2645K, capacity 4486K, committed 4864K, reserved 1056768K class space used 282K, capacity 386K, committed 512K, reserved 1048576K
  3. 结论

可以看到,第四次GC之后MaxTenuringThreshold被设为4,从第五次GC开始,每次将age大于4的对象放入老年代,保留age1-age4。验伪失败,说明猜想1可能是正确的

5 查看虚拟机源码

如果一开始就直接去查虚拟机源码,不就不用说上面这么多屁话了吗?但那样岂非少了许多乐趣。况且虚拟机源码中还夹杂着许多其它逻辑,要单把这一块拎出来看也十分麻烦。虚拟机源码确实有对这个MaxTenuringThreshold动态计算的部分,代码如下:


uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {

	//survivor_capacity是survivor空间的大小

  size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);

  size_t total = 0;

  uint age = 1;

  while (age < table_size) {

    total += sizes[age];//sizes数组是每个年龄段对象大小

    if (total > desired_survivor_size) break;

    age++;

  }

  uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;

	...

}

这也进一步证明了猜想1的正确性。那么代码中的TargetSurvivorRatio是什么呢?这个是对于“一半“这个比例的设定,默认值为50%,一般我们不去修改它。

6 总结

通过以上的论述,我们基本可以得出一个结论,动态对象年龄判定实际上是新生代能够保留的对象的最大年龄的动态判定,具体的描述是:

在每次MinorGC之后会重新计算一次MaxTenuringThreshold,计算规则是:将Survivor区中的对象按照年龄从小到大的顺序累加,当累加到某个年龄X的时候,若累加值大于等于Sruvivor空间的TargetSurvivorRatio%(默认为50%),就将MaxTenuringThreshold设为X。在下一次MinorGC时(对象年龄+1之后),年龄大于MaxTenuringThreshold的对象就进入老年代。

尽信书不如无书,当在书中看到某个地方感到很迷惑的时候,不妨深入了解一下,说不定有意外收获。


上一篇 编程-时钟

Comments

Content