jvm源码阅读笔记[3]:从内存分配到触发GC的细节
来源:互联网 发布:centos 输入法切换 编辑:程序博客网 时间:2024/06/10 19:48
从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨
个人源码地址:https://github.com/FlashLightNing/openjdk-notes
还有一个openjdk6,7,8,9的地址:https://github.com/dmlloyd/openjdk
jvm源码阅读笔记[1]:如何触发一次CMS回收
jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解
jvm源码阅读笔记[3]:从内存分配到触发GC的细节
jvm源码阅读笔记[4]:从GC说到vm operation
jvm源码阅读笔记[5]:内存分配失败触发的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步,
- 若开启了UseTLAB,则在tlab里面分配,分配成功则返回对象空间。若分配失败,则返回null
- 若第一步返回的是null,则在堆中进行分配
- 若仍分配失败,则抛出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上分配。
再来看看分配的几个步骤
- 若开启了UseTLAB,则在tlab里面分配,分配成功则返回对象空间。若分配失败,则返回null
- 若第一步返回的是null,则在堆中进行分配
- 若仍分配失败,则抛出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区分配对象:
- 要分配的大小>年轻代容量(eden+from总大小)
- 某些JNI方法正在被调用
- 最近发生过一次担保失败或者可能发生担保失败
总结起来,分配分为3个大步骤:
- 若开启了UseTLAB,则在tlab里面分配,分配成功则返回对象空间。若分配失败,则返回null
- 若第一步返回的是null,则在堆中进行分配
- 若仍分配失败,则抛出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”。
从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨
个人源码地址:https://github.com/FlashLightNing/openjdk-notes
还有一个openjdk6,7,8,9的地址:https://github.com/dmlloyd/openjdk
jvm源码阅读笔记[1]:如何触发一次CMS回收
jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解
jvm源码阅读笔记[3]:从内存分配到触发GC的细节
jvm源码阅读笔记[4]:从GC说到vm operation
jvm源码阅读笔记[5]:内存分配失败触发的GC究竟对内存做了什么?
- jvm源码阅读笔记[3]:从内存分配到触发GC的细节
- jvm源码阅读笔记[5]:内存分配失败触发的GC究竟对内存做了什么?
- jvm源码阅读笔记[4]:从GC说到vm operation
- jvm源码阅读笔记[7]-从jstat -gccause命令谈到jvm中都有哪些GC cause
- 触发Full GC执行的情况 JVM对象分配规则
- JVM之---GC内存分配
- JVM内存分配与GC
- jvm源码阅读笔记[1]:如何触发一次CMS回收
- JVM学习笔记-对象的内存分配
- 内存堆管理器GenCollectedHeap分配对象内存及Gc触发
- JVM的GC中对象的age以及JVM内存的分配策略
- 触发JVM进行Full GC的情况
- JVM中触发full gc的条件
- JVM(十四)GC的触发时间
- 【Java】深入理解JVM学习笔记(三) —— GC收集器和内存分配
- JVM的内存分配
- JVM的内存分配
- [jvm][面试] jvm full gc 的触发情况以及解决办法
- 【codevs 1036】商务旅行
- 可折叠按钮
- 2018计算机推免生东南大学面试笔试回忆录~
- 阿里云Centos下安装JDK+Tomcat+Mysql步骤详解
- mysql 字段类型保存小数和整数
- jvm源码阅读笔记[3]:从内存分配到触发GC的细节
- bzoj 2783: [JLOI2012]树
- hibernate泛型Dao
- Java数据加密(MD5,sha1,sha256)
- 机房个人重构——总结思考
- 三、动态SQL语句
- CodeForces
- rsync配置文件详解
- 发布npm包