1. 程式人生 > >jvm原始碼閱讀筆記[3]:從記憶體分配到觸發GC的細節

jvm原始碼閱讀筆記[3]:從記憶體分配到觸發GC的細節

    
    除了第一篇說到的,對於使用cms回收的應用,會有執行緒輪詢判斷老年代是否滿足GC的條件,若滿足,則會觸發一次cms老年代的回收。
    針對年輕代,更常見的是,執行緒優先在eden區分配物件的時候,若eden區空間不足,則會觸發一次young gc。若不允許擔保失敗,則還可能轉為一次full gc。那麼,今天就來看看這種記憶體分配不足觸發GC的過程。
    以下是collectedHeap.inline.hpp中的分配的程式碼。
    

oop CollectedHeap::obj_allocate(KlassHandle klass, int size, TRAPS) {
  debug_only(check_for_valid_allocation_state());
  assert
(!Universe::heap()->is_gc_active(), "Allocation during gc not allowed");//校驗在GC的時候不會進行記憶體分配 assert(size >= 0, "int won't convert to size_t"); HeapWord* obj = common_mem_allocate_init(klass, size, CHECK_NULL); post_allocation_setup_obj(klass, obj, size); NOT_PRODUCT(Universe::heap()->check_for_bad_heap_word_value(obj, size)); return
(oop)obj; }

    
    collectedHeap定義了Java堆的實現,定義了堆必須實現的功能,如建立TLAB,記憶體分配等基本功能。然後其他類通過繼承collectedHeap類,實現了幾種具體的堆型別,主要有ParallelScavengeHeap,G1CollectedHeap等。這些細節以後再寫。
    從原始碼中可以看到,首先是一些簡單的校驗,如當前堆不處於GC狀態,分配的大小>0。然後再呼叫common_mem_allocate_init方法進行記憶體分配,再是呼叫post_allocation_setup_obj做一些初始化的工作,如設定物件頭資訊等。
    來看看common_mem_allocate_init方法:
    

HeapWord* CollectedHeap::common_mem_allocate_init(KlassHandle klass, size_t size, TRAPS) {
  HeapWord* obj = common_mem_allocate_noinit(klass, size, CHECK_NULL);
  init_obj(obj, size);//位元組填充和對齊
  return obj;
}

    
    它先是呼叫common_mem_allocate_noinit方法申請了記憶體空間,然後呼叫init_obj方法進行初始化,這裡的初始化主要是為申請出來的這塊空間填充0位元組和位元組對齊。
    還是來看看common_mem_allocate_noinit方法吧。
    


HeapWord* CollectedHeap::common_mem_allocate_noinit(KlassHandle klass, size_t size, TRAPS) {

  CHECK_UNHANDLED_OOPS_ONLY(THREAD->clear_unhandled_oops();)

  if (HAS_PENDING_EXCEPTION) {
    NOT_PRODUCT(guarantee(false, "Should not allocate with exception pending"));
    return NULL;  // caller does a CHECK_0 too
  }

  HeapWord* result = NULL;
  if (UseTLAB) {//在tlab裡分配
    result = allocate_from_tlab(klass, THREAD, size);
    if (result != NULL) {
      assert(!HAS_PENDING_EXCEPTION,
             "Unexpected exception, will result in uninitialized storage");
      return result;
    }
  }
  bool gc_overhead_limit_was_exceeded = false;
   //在堆中分配
  result = Universe::heap()->mem_allocate(size,
                                          &gc_overhead_limit_was_exceeded);
  if (result != NULL) {
    NOT_PRODUCT(Universe::heap()->
      check_for_non_bad_heap_word_value(result, size));
    assert(!HAS_PENDING_EXCEPTION,
           "Unexpected exception, will result in uninitialized storage");
    THREAD->incr_allocated_bytes(size * HeapWordSize);

    AllocTracer::send_allocation_outside_tlab_event(klass, size * HeapWordSize);

    return result;
  }

    //丟擲OOM異常
  if (!gc_overhead_limit_was_exceeded) {
    // -XX:+HeapDumpOnOutOfMemoryError and -XX:OnOutOfMemoryError support
    report_java_out_of_memory("Java heap space");

    if (JvmtiExport::should_post_resource_exhausted()) {
      JvmtiExport::post_resource_exhausted(
        JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_JAVA_HEAP,
        "Java heap space");
    }

    THROW_OOP_0(Universe::out_of_memory_error_java_heap());
  } else {
    // -XX:+HeapDumpOnOutOfMemoryError and -XX:OnOutOfMemoryError support
    report_java_out_of_memory("GC overhead limit exceeded");

    if (JvmtiExport::should_post_resource_exhausted()) {
      JvmtiExport::post_resource_exhausted(
        JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_JAVA_HEAP,
        "GC overhead limit exceeded");
    }

    THROW_OOP_0(Universe::out_of_memory_error_gc_overhead_limit());
  }
}

    
    可以看到分配分為3步,

  1. 若開啟了UseTLAB,則在tlab裡面分配,分配成功則返回物件空間。若分配失敗,則返回null
  2. 若第一步返回的是null,則在堆中進行分配
  3. 若仍分配失敗,則丟擲OOM異常。

    
    先來看看第一個開啟了UseTLAB的情況:
    


HeapWord* CollectedHeap::allocate_from_tlab(KlassHandle klass, Thread* thread, size_t size) {
  assert(UseTLAB, "should use UseTLAB");

  //第一步:直接線上程的tlab上分配,若分配失敗,則走相對慢的分配:allocate_from_tlab_slow
  HeapWord* obj = thread->tlab().allocate(size);
  if (obj != NULL) {
    return obj;
  }
  /* 
  是指當直接線上程的tlab上分配不下的時候,執行緒重新申請一塊tlab,然後在這塊tlab上分配,並返回分配完的地址。
  但如果剩餘的空間>配置的可浪費的空間,則就不在tlab分配,而是去eden區分配
  */
  return allocate_from_tlab_slow(klass, thread, size);
}

    
    首先線上程的tlab上通過指標碰撞分配,若剩餘空間大於要分配的size,則進行分配。若空間不足,則呼叫allocate_from_tlab_slow進行分配。該方法中執行緒重新申請了一塊tlab然後在該tlab上分配。來看看這個方法:
    

HeapWord* CollectedHeap::allocate_from_tlab_slow(KlassHandle klass, Thread* thread, size_t size) {

  /* 
  如果在tlab上的空閒空間大於設定的能忽略的大小,那就不在tlab上分配,而是在堆的共享區域,保留該tlab,用於下次分配使用。
  */
   if (thread->tlab().free() > thread->tlab().refill_waste_limit()) {
    thread->tlab().record_slow_allocation(size);
    return NULL;
  }

  /* 
   忽略此tlab然後重新申請一塊。為了最小化記憶體碎片,最後一個tlab可能比其他的都小
  */
  size_t new_tlab_size = thread->tlab().compute_size(size);

  thread->tlab().clear_before_allocation();

  if (new_tlab_size == 0) {
    return NULL;
  }

  // 申請一個新的tlab
  HeapWord* obj = Universe::heap()->allocate_new_tlab(new_tlab_size);
  if (obj == NULL) {
    return NULL;
  }

  AllocTracer::send_allocation_in_new_tlab_event(klass, new_tlab_size * HeapWordSize, size * HeapWordSize);

  if (ZeroTLAB) {
    Copy::zero_to_words(obj, new_tlab_size);
  } else {
#ifdef ASSERT
    size_t hdr_size = oopDesc::header_size();
    Copy::fill_to_words(obj + hdr_size, new_tlab_size - hdr_size, badHeapWordVal);
#endif // ASSERT
  }
  thread->tlab().fill(obj, obj + size, new_tlab_size);
  return obj;
}

    
    可以看到,當tlab中剩餘空間>設定的可忽略大小以及申請一塊新的tlab失敗時返回null,然後走上面的第二步,也就是在堆的共享區域分配。當tlab剩餘空間可以忽略,則申請一塊新的tlab,若申請成功,則在此tlab上分配。
    再來看看分配的幾個步驟

  1. 若開啟了UseTLAB,則在tlab裡面分配,分配成功則返回物件空間。若分配失敗,則返回null
  2. 若第一步返回的是null,則在堆中進行分配
  3. 若仍分配失敗,則丟擲OOM異常。

    總結起來第一步就是:若開啟了tlab,則先通過指標碰撞線上程的tlab分配。若在當前執行緒的tlab分配不下,則判斷tlab剩餘空間能否忽略。若能忽略,則忽略此tlab然後重新申請一塊tlab。若不能忽略,或者申請tlab失敗,則返回null。若申請了tlab後分配成功,則返回分配完的空間。若返回的是null,則接下來需要在堆的共享區域內分配(tlab雖然也在堆中,但是執行緒各自的,並不是共享的)。

    接下來我們看看第二步:在堆中進行分配。主要是mem_allocate方法。
    


HeapWord* GenCollectorPolicy::mem_allocate_work(size_t size,
                                        bool is_tlab,
                                        bool* gc_overhead_limit_was_exceeded) {
  GenCollectedHeap *gch = GenCollectedHeap::heap();

  debug_only(gch->check_for_valid_allocation_state());
  assert(gch->no_gc_in_progress(), "Allocation during gc not allowed");

  /* 
   一般來說,gc_overhead_limit_was_exceeded=false。
   只有當gc 時間限制超過了下面的檢查時才會設定成true.
  */
   *gc_overhead_limit_was_exceeded = false;

  HeapWord* result = NULL;


   for (int try_count = 1, gclocker_stalled_count = 0; /* return or throw */; try_count += 1) {
    HandleMark hm; 

    // .第一次嘗試分配不需要獲取鎖,通過while+CAS來保障
    Generation *gen0 = gch->get_gen(0);//年輕代
    assert(gen0->supports_inline_contig_alloc(),
      "Otherwise, must do alloc within heap lock");
    if (gen0->should_allocate(size, is_tlab)) {//對大小進行判斷,比如是否超過eden區能分配的最大大小
      result = gen0->par_allocate(size, is_tlab);//while迴圈+指標碰撞+CAS分配,
      if (result != NULL) {
        assert(gch->is_in_reserved(result), "result not in heap");
        return result;
      }
    }

    //如果res=null,表示在eden區分配失敗了,因為沒有連續的空間。則繼續往下走
    unsigned int gc_count_before;  
    {
      MutexLocker ml(Heap_lock);//獲取鎖
      if (PrintGC && Verbose) {
        gclog_or_tty->print_cr("TwoGenerationCollectorPolicy::mem_allocate_work:"
                      " attempting locked slow path allocation");
      }
      //需要注意的是,只有大物件可以被分配在老年代。一般情況下都是false,所以first_only=true
      bool first_only = ! should_try_older_generation_allocation(size);

      result = gch->attempt_allocation(size, is_tlab, first_only);//在每個代嘗試分配,first_only=true時只會在年輕代分配
      if (result != NULL) {
        assert(gch->is_in_reserved(result), "result not in heap");
        return result;
      }
      /*Gc操作已被觸發但還無法被執行,一般不會出現這種情況,只有在jni中jni_GetStringCritical等方法被呼叫時出現is_active_and_needs_gc=TRUE,主要是為了避免GC導致物件地址改變。jni_GetStringCritical方法的作用參考文章:http://blog.csdn.net/xyang81/article/details/42066665
      */
      if (GC_locker::is_active_and_needs_gc()) {  
        if (is_tlab) {
          return NULL;  // Caller will retry allocating individual object
        }
        if (!gch->is_maximal_no_gc()) {//因為不能進行GC回收,所以只能嘗試通過擴堆

          result = expand_heap_and_allocate(size, is_tlab);
          if (result != NULL) {
            return result;
          }
        }

        /*
        Number of times to retry allocations when "                      \
          "blocked by the GC locker
         GCLockerRetryAllocationCount 預設值=2
        */
        if (gclocker_stalled_count > GCLockerRetryAllocationCount) {
          return NULL; // we didn't get to do a GC and we didn't get any memory
        }


        JavaThread* jthr = JavaThread::current();
        if (!jthr->in_critical()) {
          MutexUnlocker mul(Heap_lock);
          // Wait for JNI critical section to be exited
          GC_locker::stall_until_clear();
          gclocker_stalled_count += 1;
          continue;
        } else {
          if (CheckJNICalls) {
            fatal("Possible deadlock due to allocating while"
                  " in jni critical section");
          }
          return NULL;
        }
      }

      gc_count_before = Universe::heap()->total_collections();
    }

    VM_GenCollectForAllocation op(size, is_tlab, gc_count_before);//VM操作進行一次由分配失敗觸發的GC
    VMThread::execute(&op);
    if (op.prologue_succeeded()) { //一次GC操作已完成  
      result = op.result();
      if (op.gc_locked()) {//當前執行緒沒有成功觸發GC(可能剛被其它執行緒觸發了),則繼續重試分配  
         assert(result == NULL, "must be NULL if gc_locked() is true");
         continue;  // retry and/or stall as necessary 重試分配
      }

      /* 
      分配失敗且已經完成GC了,則判斷是否超時等資訊。
       */
      const bool limit_exceeded = size_policy()->gc_overhead_limit_exceeded();
      const bool softrefs_clear = all_soft_refs_clear();

      if (limit_exceeded && softrefs_clear) {
        *gc_overhead_limit_was_exceeded = true;
        size_policy()->set_gc_overhead_limit_exceeded(false);
        if (op.result() != NULL) {
          CollectedHeap::fill_with_object(op.result(), size);
        }
        return NULL;
      }
      assert(result == NULL || gch->is_in_reserved(result),
             "result not in heap");
      return result;
    }

    // Give a warning if we seem to be looping forever.
    if ((QueuedAllocationWarningCount > 0) &&
        (try_count % QueuedAllocationWarningCount == 0)) {
          warning("TwoGenerationCollectorPolicy::mem_allocate_work retries %d times \n\t"
                  " size=" SIZE_FORMAT " %s", try_count, size, is_tlab ? "(TLAB)" : "");
    }
  }
  //for迴圈結束
}

    
    該方法比較長,主要分為以下幾步:
    1.先判斷是否在年輕代設定了最大能分配的大小。若沒設定(預設沒設定)或者此處分配的大小<設定的最大能分配的大小,則通過while+CAS的方式無鎖在eden區分配。否則,進入第二步的分配
    2.先獲取堆鎖,然後判斷此次分配能否在old區分配。接下來,根據判斷的結果在堆的年輕代和老年代分配。
    3.如果第二步分配失敗,判斷此時有沒有jni_GetStringCritical等JNI方法被呼叫。若有JNI呼叫,因為無法GC(可參考http://blog.csdn.net/xyang81/article/details/42066665),所以只能判斷能否擴堆。若能擴堆則擴堆。若不能擴堆,因為最外層是for迴圈,則跳過此處迴圈進行下次迴圈。
    4.若3仍然失敗了,則通過VM觸發一次由分配失敗觸發的一次GC,也就是我們經常能在GC日誌裡面看到的“_allocation_failure”。具體VM觸發的GC的細節下篇文章再做具體的描述。

    來看看第2步中JVM是怎麼判斷物件能否在Old區分配的:
    

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();//eden大小+from大小
  return    (word_size > heap_word_size(gen0_capacity))
         || GC_locker::is_active_and_needs_gc()
         || gch->incremental_collection_failed();
}

    
    只要以下3個條件滿足一個,就可以在old區分配物件:

  1. 要分配的大小>年輕代容量(eden+from總大小)
  2. 某些JNI方法正在被呼叫
  3. 最近發生過一次擔保失敗或者可能發生擔保失敗

    總結起來,分配分為3個大步驟:

  1. 若開啟了UseTLAB,則在tlab裡面分配,分配成功則返回物件空間。若分配失敗,則返回null
  2. 若第一步返回的是null,則在堆中進行分配
  3. 若仍分配失敗,則丟擲OOM異常。

    具體細節來講,第一步做的是:若開啟了tlab,則先通過指標碰撞線上程的tlab分配。若在當前執行緒的tlab分配不下,則判斷tlab剩餘空間能否忽略。若能忽略,則忽略此tlab然後重新申請一塊tlab。若不能忽略,或者申請tlab失敗,則返回null。若申請了tlab後分配成功,則返回分配完的空間。若返回的是null,則接下來需要在堆的共享區域內分配(tlab雖然也在堆中,但是執行緒各自的,並不是共享的)。
    第二步具體的是:
    1.先判斷是否在年輕代設定了最大能分配的大小。若沒設定(預設沒設定)或者此處分配的大小<設定的最大能分配的大小,則通過while+CAS的方式無鎖在eden區分配。否則,進入第二步的分配
    2.先獲取堆鎖,然後判斷此次分配能否在old區分配。接下來,根據判斷的結果在堆的年輕代和老年代分配。
    3.如果第二步分配失敗,判斷此時有沒有jni_GetStringCritical等JNI方法被呼叫。若有JNI呼叫,因為無法GC,所以只能判斷能否擴堆。若能擴堆則擴堆。若不能擴堆,因為最外層是for迴圈,則跳過此處迴圈進行下次迴圈。
    4.若3仍然失敗了,則通過VM觸發一次由分配失敗觸發的一次GC,也就是我們經常能在GC日誌裡面看到的“_allocation_failure”。