遇到一个对象分配的问题

最近看了毕玄老师分享的的PPT,对其中一个案例进行了测试,参考代码如下:

/**
 * -Xmx20M -Xms20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails
 * -XX:PretenureSizeThreshold=4M -Xloggc:D:\gc.log
 * @author heyong
 */
public class SerialGCDemo {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws InterruptedException {
        ByteArray bytes = new ByteArray(2 * _1MB);
        ByteArray bytes2 = new ByteArray(2 * _1MB);
        ByteArray bytes3 = new ByteArray(2 * _1MB);
        System.out.println("step 1");
        ByteArray bytes4 = new ByteArray(2 * _1MB);
        Thread.sleep(3000);
        System.out.println("step 2");
        ByteArray bytes5 = new ByteArray(2 * _1MB);
        ByteArray bytes6 = new ByteArray(2 * _1MB);
        System.out.println("step 3");
        ByteArray bytes7 = new ByteArray(3 * _1MB);
        // Thread.sleep(1000 * 60 * 60);
        System.out.println("======");
    }

    private static class ByteArray {
        private byte[] bytes;
        public ByteArray(int size) {
            bytes = new byte[size];
        }
    }
}

运行的代码和参数如上,在复杂代码到机器里面运行之前,请大家思考下面2个问题:

  1. bytes7对象能否分配成功?
  2. 如果bytes7能够分配成功,应该分配在哪里呢?

如果大家对于对象的分配流程感兴趣,可以参考我的博文:

对象的创建以及YoungGC的触发:http://www.jianshu.com/p/941fe93d21c2
JVM之逃逸分析以及TLAB:http://www.jianshu.com/p/3835450d49d0

好了言归正传,回答上面的问题!

bytes7对象能否分配成功

上面的对象能够分配成功,在 eclipse 执行上面的程序,打印出来的GC日志如下:

0.341: [GC (Allocation Failure) 0.341: [DefNew: 7127K->526K(9216K), 0.0199660 secs] 7127K->6670K(19456K), 0.0202876 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
3.367: [GC (Allocation Failure) 3.368: [DefNew: 6830K->6830K(9216K), 0.0000683 secs]3.368: [Tenured: 6144K->8192K(10240K), 0.0581733 secs] 12974K->12812K(19456K), [Metaspace: 2655K->2655K(1056768K)], 0.0590199 secs] [Times: user=0.01 sys=0.00, real=0.06 secs] 

上面的日志中有两条GC日志,从第一条日志可以分析出,创建bytes4的时候,年轻代不足,触发了一次YoungGC,年轻代存活对象大小 = 6M,To Survivor 区大小 = 1M,To区空间不够,通过空间担保机制晋升到老年代,老年代的晋升规则如下:

bool TenuredGeneration::promotion_attempt_is_safe(size_t max_promotion_in_bytes) const {
  //老年代最大连续空间
  size_t available = max_contiguous_available();
  //平均晋升对象的大小
  size_t av_promo  = (size_t)gc_stats()->avg_promoted()->padded_average();
  //最大连续空间大小 > 平均大小或最大连续空间 > 晋升对象的大小
  bool   res = (available >= av_promo) || (available >= max_promotion_in_bytes);
  if (PrintGC && Verbose) {
    gclog_or_tty->print_cr(
      "Tenured: promo attempt is%s safe: available("SIZE_FORMAT") %s av_promo("SIZE_FORMAT"),"
      "max_promo("SIZE_FORMAT")",
      res? "":" not", available, res? ">=":"<",
      av_promo, max_promotion_in_bytes);
  }
  return res;
}

从上面的代码可以看出,如果最大连续空间大小 > 平均大小或最大连续空间 > 晋升对象的大小,就运行年轻代对象晋升到老年代,该程序中老年代空间 = 10m,因此可以将年轻代对象晋升到老年代。
下面继续看第二条日志,发现年轻代在分配bytes7对象的时候,年轻代不足触发YoungGC,但是老年代空间不足,无法满足晋升规则,因此触发了一次FullGC,部分年轻代对象被晋升到老年代。从源码角度分析一下为什么会触发FullGC,源码如下:

//表示有jni在操作内存,此时不能进行GC避免改变对象在内存的位置
if (GC_locker::is_active_and_needs_gc()) {
    if (!gch->is_maximal_no_gc()) {
      result = expand_heap_and_allocate(size, is_tlab);//扩堆
    }
    return result;   // could be null if we are out of space
    //consult_young=true的时候,表示调用该方法时,判断此时晋升是否的安全的。
    //若=false,表示只取上次young gc时设置的参数,此次不再进行额外的判断。
  } else if (!gch->incremental_collection_will_fail(false /* don't consult_young */)) {
    // 对年轻代进行GC
    gch->do_collection(false            /* full */,
                       false            /* clear_all_soft_refs */,
                       size             /* size */,
                       is_tlab          /* is_tlab */,
                       number_of_generations() - 1 /* max_level */);
  } else {
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print(" :: Trying full because partial may fail :: ");
    }
    //对老年代进行GC
    gch->do_collection(true             /* full */,
                       false            /* clear_all_soft_refs */,
                       size             /* size */,
                       is_tlab          /* is_tlab */,
                       number_of_generations() - 1 /* max_level */);
  }
  //尝试重新分配
  result = gch->attempt_allocation(size, is_tlab, false /*first_only*/);
  if (result != NULL) {
    assert(gch->is_in_reserved(result), "result not in heap");
    return result;
  }

  // 如果上面分配失败,尝试先阔堆,然后分配
  result = expand_heap_and_allocate(size, is_tlab);
  if (result != NULL) {
    return result;
  }
  //如果对象还是没有分配成功,进行一次FullGC
  {
    IntFlagSetting flag_change(MarkSweepAlwaysCompactCount, 1); // Make sure the heap is fully compacted

    gch->do_collection(true             /* full */,
                       true             /* clear_all_soft_refs */,
                       size             /* size */,
                       is_tlab          /* is_tlab */,
                       number_of_generations() - 1 /* max_level */);
  }
  //在FullGC以后尝试分配
  result = gch->attempt_allocation(size, is_tlab, false /* first_only */);

从上面的代码中可以看出如果进行YoungGC分配失败以后,会进行一次FullGC,进行对象的回收,从日志可以看出,在进行FullGC时,部分年轻代对象晋升到了老年代。

如果bytes7能够分配成功,应该分配在哪里呢?

重新第二条日志,老年代区内存使用量增加了2M,有人可能会问老年代区增加的 2M 也可能是 bytes7 在老年代分配了啊?这里提供 3 种方法证明 bytes7 对象在年轻代分配

First

在上面的程序中将 :

 byte[] bytes7 = new byte[2 * _1MB];

修改为:

 byte[] bytes7 = new byte[3 * _1MB];

执行上面的程序,查看日志:

Heap
 def new generation   total 9216K, used 8016K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  97% used [0x00000000fec00000, 0x00000000ff3d43f8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  80% used [0x00000000ff600000, 0x00000000ffe00158, 0x00000000ffe00200, 0x0000000100000000)
 Metaspace       used 2661K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

发现年轻代使用量 = 8016K = 7.8 M,老年代使用量 = 8192K = 8M,bytes7分配了3M的内存,因此可以证明bytes7 是在年轻代分配的。

Second

光从日志的角度还是不能证明,下面我们从源码角度去证实一下(最好是先看:《对象的创建以及YoungGC的触发》这篇文章):
在年轻代进行对象分配的时候,会调用 GenCollectorPolicy::should_try_older_generation_allocation 方法来判断对象是否在老年代分配,具体实现如下:

bool GenCollectorPolicy::should_try_older_generation_allocation(
        size_t word_size) const {
  GenCollectedHeap* gch = GenCollectedHeap::heap();
  size_t gen0_capacity = gch->get_gen(0)->capacity_before_gc();
  return    (word_size > heap_word_size(gen0_capacity))//要分配的大小>年轻代容量(eden+from总大小)
         || GC_locker::is_active_and_needs_gc()//某些JNI方法正在被调用
         || gch->incremental_collection_failed();//最近发生过一次担保失败
}

从上面的代码可以看出,一个对象要在老年代分配,需要满足下面3中情况中的一种(前提条件:对象大小 < PretenureSizeThreshold设置的阈值):

  1. 要分配的大小>年轻代容量(eden+from总大小)
  2. 某些JNI方法正在被调用
  3. 最近发生过一次担保失败

当分配bytes7时,不满足上面任何一种情况,因此bytes7不可能在老年代分配

Third

去掉注释掉的代码前面的斜杠,执行程序,使用HSDB查看不同分区的内存地址,查看bytes7对象的地址,确定bytes7对象所在的内存区域。
关于HSDB的使用可以参考R大的博客:http://rednaxelafx.iteye.com/blog/1847971

差不多了,写完赶快跑路。

自我介绍

我是何勇,现在重庆猪八戒,多学学!!!

你可能感兴趣的:(遇到一个对象分配的问题)