GC part 4

part 4

JVM可以帮我们管理内存,这是一件非常有意义的事情,我们再也不用担心allocate出来的内存没有在适当的时候free掉了,这个comment希望能去探索一下JVM是如何处理内容申请的,因为垃圾收集的发生就是因为申请了太多的内存,需要清理或者整理哪些已经没有价值的对象来释放空间,以满足新的内存分配申请;下面将以一个具体的内存申请问题出发,从源码角度去分析一下JVM的内存分配处理链路;

JVM是如何为一个对象在堆上申请一块空间的?

在java语言中我们通过使用new关键字来创建一个新的对象,在虚拟机中对应着new指令,当然本文并不打算从new指令说起;instanceOopDesc对应java语言中的对象实例,所以创建一个新对象就是在JVM里面创建一个新的instanceOopDesc实例,InstanceKlass::allocate_instance用于创建一个新的instanceOopDesc实例,下面就从allocate_instance函数开始说起。

instanceOop InstanceKlass::allocate_instance(TRAPS) {
  bool has_finalizer_flag = has_finalizer(); // Query before possible GC
  int size = size_helper();  // Query before forming handle.

  KlassHandle h_k(THREAD, this);

  instanceOop i;

  i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

整个函数大概分三个步骤执行,首先取到实例所需要的空间大小size,然后使用CollectedHeap::obj_allocate去堆上申请一块大小为size的空间,最后判断实例是否实现了finalizer,如果有的话,那么就要使用register_finalizer注册finalizer;这里主要关心内存申请的部分,也就是CollectedHeap::obj_allocate函数;

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");
  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;
}

这个函数做了一些校验,然后调用common_mem_allocate_init去申请内存,下面看看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初始化这块内存;,依然只关系内存申请相关函数common_mem_allocate_noinit:

image

申请分两组情况,如果使用TLAB(Thread-Local Allocation Buffer),那么就使用allocate_from_tlab来分配内存,否则使用Universe::heap()->mem_allocate来分配内存;先来看看从TLAB分配内存的情况:

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

  HeapWord* obj = thread->tlab().allocate(size);
  if (obj != NULL) {
    return obj;
  }
  // Otherwise...
  return allocate_from_tlab_slow(klass, thread, size);
}

依然还是分两种情况,首先通过thread->tlab().allocate来分配内存,如果无法满足要求,那么就通过allocate_from_tlab_slow来进行内存分配,还是先来看thread->tlab().allocate;

inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) {
  invariants();
  HeapWord* obj = top();
  if (pointer_delta(end(), obj) >= size) {
    // successful thread-local allocation
#ifdef ASSERT
    // Skip mangling the space corresponding to the object header to
    // ensure that the returned space is not considered parsable by
    // any concurrent GC thread.
    size_t hdr_size = oopDesc::header_size();
    Copy::fill_to_words(obj + hdr_size, size - hdr_size, badHeapWordVal);
#endif // ASSERT
    // This addition is safe because we know that top is
    // at least size below end, so the add can't wrap.
    set_top(obj + size);

    invariants();
    return obj;
  }
  return NULL;
}

这个函数还是比较简单明了的,top指针指向空闲内存开始处,判断tlab里面剩下的内存是否可以满足要求,如果可以,那么就分配size大小的空间,并且移动空闲指针到合适的地方;否则就代表无法成功在TLAB上分配到足够的内存;

如果thread->tlab().allocate分配失败,那么allocate_from_tlab_slow就要开始工作了,首先,如果JVM认为TLAB空闲的内存足够大,那么就不能抛弃这部分空闲的内存,那就得去堆中去分配了;

  // Retain tlab and allocate object in shared space if
  // the amount free in the tlab is too large to discard.
  if (thread->tlab().free() > thread->tlab().refill_waste_limit()) {
    thread->tlab().record_slow_allocation(size);
    return NULL;
  }
image

接着,就说明TLAB里面已经没有空闲的空间了,或者TLAB里面空闲的空间可以忍受浪费,那么就新申请一块TLAB,首先需要计算新的TLAB的大小,thread->tlab().compute_size将承担这个工作:

image

首先将申请的对象大小规整为aligned_obj_size,然后计算出目前可申请的空间大小available_size,这个大小的值可能是新生代中Eden的空闲空间;new_tlab_size是最终确定的申请的TLAB的大小;接着判断是否满足要求,如果new_tlab_size的大小还不足以满足申请的对象实例,那么就放弃神奇这次TLAB;
回到allocate_from_tlab_slow,如果发现计算出来的TLAB的大小为0,那么就直接返回NULL告诉上层无法完成在TLAB上进行分配;如果计算出来的新的TLAB的大小不为0,那么就通过Universe::heap()->allocate_new_tlab函数来申请一块新的TLAB;这个工作将由GenCollectedHeap::allocate_new_tlab来完成:

HeapWord* GenCollectedHeap::allocate_new_tlab(size_t size) {
  bool gc_overhead_limit_was_exceeded;
  return gen_policy()->mem_allocate_work(size /* size */,
                                         true /* is_tlab */,
                                         &gc_overhead_limit_was_exceeded);
}

这个函数较为复杂,下面按几个关键步骤来分析一下该函数的实现;

  • (1)、首先判断是否可以在Young区分配空间,如果可以,那么就在Young区域分配,如果分配成功了,那么就可以结束这次内存分配之旅了,否则就得继续往下尝试。
    if (young->should_allocate(size, is_tlab)) {
      result = young->par_allocate(size, is_tlab);
      if (result != NULL) {
        assert(gch->is_in_reserved(result), "result not in heap");
        return result;
      }
    }

should_allocate函数在DefNew里面的实现如下:

  // Allocation support
  virtual bool should_allocate(size_t word_size, bool is_tlab) {
    assert(UseTLAB || !is_tlab, "Should not allocate tlab");

    size_t overflow_limit    = (size_t)1 << (BitsPerSize_t - LogHeapWordSize);

    const bool non_zero      = word_size > 0;
    const bool overflows     = word_size >= overflow_limit;
    const bool check_too_big = _pretenure_size_threshold_words > 0;
    const bool not_too_big   = word_size < _pretenure_size_threshold_words;
    const bool size_ok       = is_tlab || !check_too_big || not_too_big;

    bool result = !overflows &&
                  non_zero   &&
                  size_ok;

    return result;
  }

如果申请的内存在可控的范围之内,那么就可以在该DefNew里面进行分配,否则就不行;如果判断可以在Young里面分配内存,那么就通过young->par_allocate函数来执行内存分配的工作:

HeapWord* DefNewGeneration::par_allocate(size_t word_size,
                                         bool is_tlab) {
  HeapWord* res = eden()->par_allocate(word_size);
  if (CMSEdenChunksRecordAlways && _old_gen != NULL) {
    _old_gen->sample_eden_chunk();
  }
  return res;
}

eden()->par_allocate是关键,最后将由par_allocate_impl来实现具体的内存分配工作:

// This version is lock-free.
inline HeapWord* ContiguousSpace::par_allocate_impl(size_t size) {
  do {
    HeapWord* obj = top();
    if (pointer_delta(end(), obj) >= size) {
      HeapWord* new_top = obj + size;
      HeapWord* result = (HeapWord*)Atomic::cmpxchg_ptr(new_top, top_addr(), obj);
      // result can be one of two:
      //  the old top value: the exchange succeeded
      //  otherwise: the new value of the top is returned.
      if (result == obj) {
        assert(is_aligned(obj) && is_aligned(new_top), "checking alignment");
        return obj;
      }
    } else {
      return NULL;
    }
  } while (true);
}

这个函数还是比较容易理解的,通过CAS技术来循环尝试分配内存,top指向空闲内存的起始地址,尝试分配内存就是将top指针向前移动size长度即可,当然,如果申请的内存大小大于Eden的空闲内存,那么直接就会返回NULL以代表内存分配失败;

image

如果无法从Young区域成功申请到内存,那么就要使用attempt_allocation来从其他的分代尝试获取足够的内存了;

HeapWord* GenCollectedHeap::attempt_allocation(size_t size,
                                               bool is_tlab,
                                               bool first_only) {
  HeapWord* res = NULL;
  if (_young_gen->should_allocate(size, is_tlab)) {
    res = _young_gen->allocate(size, is_tlab);
    if (res != NULL || first_only) {
      return res;
    }
  }
  if (_old_gen->should_allocate(size, is_tlab)) {
    res = _old_gen->allocate(size, is_tlab);
  }
  return res;
}

依然是用各个分代的should_allocate来判断是否可以在某个分代进行内存分配,首先尝试在Young区域进行分配,然后在尝试从Old区域分配内存,从Young区分配内存的过程已经在上面分析过,就不再赘述了;下面来分析如何判断是否可以在old区域进行内存分配,以及具体是如何进行内存分配的;

  // Returns "true" iff this generation should be used to allocate an
  // object of the given size.  Young generations might
  // wish to exclude very large objects, for example, since, if allocated
  // often, they would greatly increase the frequency of young-gen
  // collection.
  virtual bool should_allocate(size_t word_size, bool is_tlab) {
    bool result = false;
    size_t overflow_limit = (size_t)1 << (BitsPerSize_t - LogHeapWordSize);
    if (!is_tlab || supports_tlab_allocation()) {
      result = (word_size > 0) && (word_size < overflow_limit);
    }
    return result;
  }

老年代是不支持TLAB分配的,只有DefNew是支持的,supports_tlab_allocation函数用于判断某个分代是否支持TLAB分配,除了DefNew,其他分代都是不支持的,当然,如果不是TLAB分配请求,那么如果申请分配的内存大于0并且小于最大极限,那么就支持在该分代内申请内存,否则就是不支持的。

接着看看具体如何在老年代进行内存分配(对于Serial Old);

inline HeapWord* OffsetTableContigSpace::allocate(size_t size) {
  HeapWord* res = ContiguousSpace::allocate(size);
  if (res != NULL) {
    _offsets.alloc_block(res, size);
  }
  return res;
}

接着去看ContiguousSpace的allocate_impl:

// This version requires locking.
inline HeapWord* ContiguousSpace::allocate_impl(size_t size) {
  assert(Heap_lock->owned_by_self() ||
         (SafepointSynchronize::is_at_safepoint() && Thread::current()->is_VM_thread()),
         "not locked");
  HeapWord* obj = top();
  if (pointer_delta(end(), obj) >= size) {
    HeapWord* new_top = obj + size;
    set_top(new_top);
    assert(is_aligned(obj) && is_aligned(new_top), "checking alignment");
    return obj;
  } else {
    return NULL;
  }
}

依然是一段比较清晰简单的代码,top依然是空闲内存的起始地址,申请一段内存就是将top向前移动一段距离;
回到mem_allocate_work,如果attempt_allocation依然无法满足要求(分配失败),那么就要走接下来的逻辑了,如果is_active_and_needs_gc是true,那么说明其他线程已经出发了GC,这个时候判断堆是否可以扩展,如果可以扩展那么就进行堆扩展,否则就得出发GC操作了;
如果gch->is_maximal_no_gc()是false的,那么就说明堆是可以尝试去扩展的,那么就使用expand_heap_and_allocate来扩展堆然后再尝试分配;下面来看看expand_heap_and_allocate函数的实现细节;

HeapWord* GenCollectorPolicy::expand_heap_and_allocate(size_t size,
                                                       bool   is_tlab) {
  GenCollectedHeap *gch = GenCollectedHeap::heap();
  HeapWord* result = NULL;
  Generation *old = gch->old_gen();
  if (old->should_allocate(size, is_tlab)) {
    result = old->expand_and_allocate(size, is_tlab);
  }
  if (result == NULL) {
    Generation *young = gch->young_gen();
    if (young->should_allocate(size, is_tlab)) {
      result = young->expand_and_allocate(size, is_tlab);
    }
  }
  assert(result == NULL || gch->is_in_reserved(result), "result not in heap");
  return result;
}

堆扩展的顺序是老年代到新生代,内存分配的顺序是从新生代到老年代,这个细节需要注意一下!expand_and_allocate函数永远做堆分代扩展及内存分配的具体工作,首先看Serial Old的expand_and_allocate是如何实现的:

HeapWord*
TenuredGeneration::expand_and_allocate(size_t word_size,
                                       bool is_tlab,
                                       bool parallel) {
  assert(!is_tlab, "TenuredGeneration does not support TLAB allocation");
  if (parallel) {
    MutexLocker x(ParGCRareEvent_lock);
    HeapWord* result = NULL;
    size_t byte_size = word_size * HeapWordSize;
    while (true) {
      expand(byte_size, _min_heap_delta_bytes);
      if (GCExpandToAllocateDelayMillis > 0) {
        os::sleep(Thread::current(), GCExpandToAllocateDelayMillis, false);
      }
      result = _the_space->par_allocate(word_size);
      if ( result != NULL) {
        return result;
      } else {
        // If there's not enough expansion space available, give up.
        if (_virtual_space.uncommitted_size() < byte_size) {
          return NULL;
        }
        // else try again
      }
    }
  } else {
    expand(word_size*HeapWordSize, _min_heap_delta_bytes);
    return _the_space->allocate(word_size);
  }
}

parallel代表是否是多线程版本的GC,Serial Old是单线程的,所以看else分支即可;expand函数实现堆扩展,allocate函数用于从堆中申请内存,先看看expand函数的实现;CardGeneration::expand是最终指向expand的实际函数:

image

通过不断尝试缩小扩展的大小来进行堆扩展,grow_by函数用于实际执行扩展工作,暂时就不深入了;
下面看看DefNew的expand_and_allocate函数是怎么实现的;

HeapWord* DefNewGeneration::expand_and_allocate(size_t size,
                                                bool   is_tlab,
                                                bool   parallel) {
  // We don't attempt to expand the young generation (but perhaps we should.)
  return allocate(size, is_tlab);
}
HeapWord* DefNewGeneration::allocate(size_t word_size, bool is_tlab) {
  // This is the slow-path allocation for the DefNewGeneration.
  // Most allocations are fast-path in compiled code.
  // We try to allocate from the eden.  If that works, we are happy.
  // Note that since DefNewGeneration supports lock-free allocation, we
  // have to use it here, as well.
  HeapWord* result = eden()->par_allocate(word_size);
  if (result != NULL) {
    if (CMSEdenChunksRecordAlways && _old_gen != NULL) {
      _old_gen->sample_eden_chunk();
    }
  } else {
    // If the eden is full and the last collection bailed out, we are running
    // out of heap space, and we try to allocate the from-space, too.
    // allocate_from_space can't be inlined because that would introduce a
    // circular dependency at compile time.
    result = allocate_from_space(word_size);
  }
  return result;
}

eden()->par_allocate试图从Eden空闲区域中去申请内存,上文已经分析过这个函数的实现细节,不再赘述;如果从Eden区域分配失败,那么就尝试在From区域进行分配,allocate_from_space函数用于执行这个工作;

image

是否需要从From区域进行内存分配需要做一些判断,should_allocate_from_space用于判断是否应该在From区域进行内存分配,当然,如果已经有线程触发了GC,那么也是可以从From区域进行内存分配的;下面先来看看should_allocate_from_space的判断标准;

image

如果在做一次FullGC,并且collection_attempt_is_safe返回了false,并且Eden不是空的,那么就可以在From分配内存,collection_attempt_is_safe是做什么的?

bool DefNewGeneration::collection_attempt_is_safe() {
  if (!to()->is_empty()) {
    log_trace(gc)(":: to is not empty ::");
    return false;
  }
  if (_old_gen == NULL) {
    GenCollectedHeap* gch = GenCollectedHeap::heap();
    _old_gen = gch->old_gen();
  }
  return _old_gen->promotion_attempt_is_safe(used());
}

如果to区域不为空,那么说明发生了"Promotion fail",这种情况下是false,以及_old_gen->promotion_attempt_is_safe也是false;

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);

  log_trace(gc)("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;
}

如果老年代连续的可用内存空间大于新生代的对象大小或者大于新生代历史平均晋升大小,那么就是true,否则就是false;

回到GenCollectorPolicy::mem_allocate_work函数中来,如果尝试扩展堆之后还是无法申请到内存,那么就只能触发一次VM_GenCollectForAllocation类型的GC Operation了,完成之后再尝试申请内存;

现在回头看看CollectedHeap::common_mem_allocate_noinit,如果TLAB这个分支无法完成内存申请工作,那么就要交给Universe::heap()->mem_allocate来执行内存分配的工作;下面来分析一下Universe::heap()->mem_allocate的流程,这个流程分析完了整个对象实例分配的流程也就分析完了:

HeapWord* GenCollectedHeap::mem_allocate(size_t size,
                                         bool* gc_overhead_limit_was_exceeded) {
  return gen_policy()->mem_allocate_work(size,
                                         false /* is_tlab */,
                                         gc_overhead_limit_was_exceeded);
}

上面已经分析过mem_allocate_work这个函数的具体实现,和TLAB分支唯一的区别就是is_tlab是false,所以接下来的分析就不进行了;
在内存分配的时候有一个细节没有提到,JVM提供了一个参数-XX:PretenureSizeThreshold,该参数可以设置直接在老年代进行分配的对象大小阈值,如果对象的大小大于该阈值,则直接在老年代分配,这个逻辑是做在新生代的,比如DefNew里面should_allocate函数里面判断对象是否应该被防止在DefNew的时候就用到了_pretenure_size_threshold_words参数。

JVM对象内存申请流程总结如下:

  • (1)、优先使用TLAB策略进行分配,在线程的TLAB里面进行内分配,如果无法再TLAB里面分配成功,再去堆中进行直接分配
  • (2)、在TLAB模式下进行分配的时候,如果无法分配成功,那么就要尝试去分配新的TLAB,然后再尝试分配内存
  • (3)、无论是重新申请TLAB,还是直接在堆上为内存分配内存,都是一样的,都是在堆上分配内存,下面就不区分了
  • (4)、首先尝试在新生代Eden中申请内存,如果无法完成内存分配,那么尝试从From区域分配(需要判断),如果还是不行,那么再尝试从老年代中分配内存
  • (5)、如果内存申请还是无法得到满足,这个时候就要去扩展堆来满足要去了,当然还是要去判断是否允许进行堆扩展
  • (6)、堆扩展是从老年代到新生代的,而内存分配是从新生代到老年代的,对扩展完了之后再尝试去申请内存,如果还是无法完成内存分配工作,那么这个时候就要试图去From区域分配内存,当然还是要判断是否允许在From区域进行内存分配的
  • (7)、如果还是无法完成内存分配,那么就要出发一次GC来回收垃圾了,然后再去尝试
  • (8)、OOM
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容