2024年细节炸裂,阿里老师深入探究G1源码之YoungGC技术(1),业内“大师级Dubbo实战笔记”面世

总结

其他的内容都可以按照路线图里面整理出来的知识点逐一去熟悉,学习,消化,不建议你去看书学习,最好是多看一些视频,把不懂地方反复看,学习了一节视频内容第二天一定要去复习,并总结成思维导图,形成树状知识网络结构,方便日后复习。

这里还有一份很不错的《Java基础核心总结笔记》,特意跟大家分享出来

目录:

部分内容截图:

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

bool has_finalizer_flag = has_finalizer();

//返回实例大小

int size = size_helper();

//封装成KlassHandle句柄,可以简单理解为是Klass的封装类

KlassHandle h_k(THREAD, this);

instanceOop i;

//创建对象实例

i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);

//注册finalizer方法

if (has_finalizer_flag && !RegisterFinalizersAtInit) {

i = register_finalizer(i, CHECK_NULL);

}

return i;

}

复制

看到这里,小伙伴们可能会有点迷惑,搞不懂instanceOopDesc和InstanceKlass,这个地方涉及到jvm的oop-klass模型,我们来画张图简单说明下,通过这张图,我们就可以理解这两句话的原因:

1.加载.class到元空间时,会在堆中实例化出一个class对象

实际上是通过instanceMirrorKlass实例化出的

2.在java中利用反射传入的是java.lang.Class对象,创建出的是普通的java实例

因为java.lang.Class对象实际上是InstanceKlass的java镜像类,可以通过java.lang.Class对象获取InstanceKlass的信息,进而获取普通java实例的信息,从而创建普通实例

细节炸裂!阿里大佬深入探究G1源码之YoungGC技术

关于oop-klass模型也非常有意思,有兴趣的读者可以自行查看下源码,本篇还是着重讲述youngGC

言归正传,我们继续往下看:

oop CollectedHeap::obj_allocate(KlassHandle klass, int size, TRAPS) {

//申请内存并初始化对象

HeapWord* obj = common_mem_allocate_init(klass, size, CHECK_NULL);

post_allocation_setup_obj(klass, obj);

return (oop)obj;

}

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;

}

//最后进入这个方法

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

HeapWord* result = NULL;

//是否使用TLAB处理,这里先不论述TLAB我们直接跳过

if (UseTLAB) {

result = allocate_from_tlab(klass, THREAD, size);

if (result != NULL) {

return result;

}

}

bool gc_overhead_limit_was_exceeded = false;

//堆中申请内存,直接进入这个方法

result = Universe::heap()->mem_allocate(size,

&gc_overhead_limit_was_exceeded);

}

Universe::heap()这个方法返回的是当前jvm使用的堆类,由于我们是G1的垃圾回收器,所以这里返回的是g1CollectedHeap.cpp:

HeapWord*

G1CollectedHeap::mem_allocate(size_t word_size,

bool* gc_overhead_limit_was_exceeded) {

//循环直到申请成功或者GC后申请失败

for (int try_count = 1, gclocker_retry_count = 0; /* we’ll return */; try_count += 1) {

unsigned int gc_count_before;

HeapWord* result = NULL;

//判断是否是大对象

if (!isHumongous(word_size)) {

//申请大对象的情况相对来说比较复杂,我们这里先看下申请小对象的逻辑

result = attempt_allocation(word_size, &gc_count_before, &gclocker_retry_count);

} else {

result = attempt_allocation_humongous(word_size, &gc_count_before, &gclocker_retry_count);

}

if (result != NULL) {

return result;

}

// 这个是fullGC的操作任务类

VM_G1CollectForAllocation op(gc_count_before, word_size);

//这里申请失败则会执行fullGC(这里先不论述FullGC)

VMThread::execute(&op);

}

return NULL;

}

大对象的申请流程比较复杂,笔者这里简单画了张图表示下大对象申请的流程,小伙伴们可以参考了解下,当然其中具体的步骤肯定不止这么简单,有兴趣的小伙伴可以自行查看源码

细节炸裂!阿里大佬深入探究G1源码之YoungGC技术

从小对象方法申请入手我们继续看:

inline HeapWord*

G1CollectedHeap::attempt_allocation(size_t word_size,

unsigned int* gc_count_before_ret,

int* gclocker_retry_count_ret) {

//再去申请一次内存,_mutator_alloc_region内部有指向当前活跃的eden region,每次申请会从这里申请内存

//在每次gc之前会清空申请的内存

HeapWord* result = _mutator_alloc_region.attempt_allocation(word_size,

false /* bot_updates */);

if (result == NULL) {

//失败后进入这个方法

result = attempt_allocation_slow(word_size,

gc_count_before_ret,

gclocker_retry_count_ret);

}

if (result != NULL) {

dirty_young_block(result, word_size);

}

return result;

}

//进入这个方法

HeapWord* G1CollectedHeap::attempt_allocation_slow(size_t word_size,

unsigned int *gc_count_before_ret,

int* gclocker_retry_count_ret) {

//这里默认情况下会循环三次,根据这个参数GCLockerRetryAllocationCount

HeapWord* result = NULL;

for (int try_count = 1; /* we’ll return */; try_count += 1) {

bool should_try_gc;

unsigned int gc_count_before;

{

MutexLockerEx x(Heap_lock);

//还是先去申请内存这个方法申请内存失败会将_mutator_alloc_region中的活跃区域进行retire并填充

//retire即将现在活跃的eden区region填充后加入到增量cset(即将要被回收的集合)中

//之后再去申请一块新的region代替当前活跃区域

//如果申请新的region失败才会继续下面操作进行GC

result = _mutator_alloc_region.attempt_allocation_locked(word_size,

false /* bot_updates */);

if (result != NULL) {

return result;

}

//GC_locker涉及到线程临界区的概念,这个方法会判断三个参数

//needs_gc && (_lock_count > 0 || _jni_lock_count > 0)

//这方法如果程序中使用jni开发可能会是true

if (GC_locker::is_active_and_needs_gc()) {

//判断是否可以扩容年轻代,可以则再申请一次

if (g1_policy()->can_expand_young_list()) {

result = _mutator_alloc_region.attempt_allocation_force(word_size,

false /* bot_updates */);

if (result != NULL) {

return result;

}

}

should_try_gc = false;

} else {

//这里判断need_gc,默认是fasle

if (GC_locker::needs_gc()) {

should_try_gc = false;

} else {

gc_count_before = total_collections();

should_try_gc = true;

}

}

}

//有且只有_lock_count ==0 && _jni_lock_count == 0 && !need_gc 这里是should_try_gc=true

if (should_try_gc) {

bool succeeded;

//执行GC

result = do_collection_pause(word_size, gc_count_before, &succeeded,

GCCause::_g1_inc_collection_pause);

if (result != NULL) {

return result;

}

if (succeeded) {

MutexLockerEx x(Heap_lock);

*gc_count_before_ret = total_collections();

return NULL;

}

} else {

//其他情况的GC_locker会走这里

//先判断循环次数超出则返回

if (*gclocker_retry_count_ret > GCLockerRetryAllocationCount) {

MutexLockerEx x(Heap_lock);

*gc_count_before_ret = total_collections();

return NULL;

}

//这个方法会被JNICritical_lock锁住直到needs_gc == false

// while (needs_gc()) {

// JNICritical_lock->wait();

// }

GC_locker::stall_until_clear();

(*gclocker_retry_count_ret) += 1;

}

//再去申请一次内存

result = _mutator_alloc_region.attempt_allocation(word_size,

false /* bot_updates */);

if (result != NULL) {

return result;

}

}

return NULL;

}

这里简单介绍下GC_locker这个类,这个类中有三个主要参数

**int **_lock_count : 记录jvm启动时的锁,在jvm启动前会+1,在jvm成功启动后会-1

**int **_jni_lock_count : 记录当前处于jni线程临界区的锁的数量,表示有几个线程处于临界区

**bool **needs_gc : 是否需要gc, 默认为false ,当有线程处于临界区时,此时如果需要gc,则会先舍弃gc,并且将needs_gc变为true,当所有线程处理完临界区后,根据这个标记再进行gc,之后再将其设置为false

这里涉及到一个新的名词:线程的临界区(即 critical region)表示存在对共享资源的多线程读写操作的代码块。

如果程序中使用了jni开发这里可能会使得_jni_lock_count>0且当前gc暂时舍去,如果没有使用则正常情况会继续gc。

二、youngGC全局停顿

=============

到这里youngGC的全局停顿就要开始了,我们直接来看代码

HeapWord* G1CollectedHeap::do_collection_pause(size_t word_size,

unsigned int gc_count_before,

bool* succeeded,

GCCause::Cause gc_cause) {

//记录gc停顿

//看到这里读者可能会比较疑惑,已经开始记录停顿开始了,但是怎么没有找到停顿的方法

//真正的停顿方法在VMThread::execute(&op);中

g1_policy()->record_stop_world_start();

// gc操作任务类,第三个参数表示本次gc是不是老年代并发gc

VM_G1IncCollectionPause op(gc_count_before,

word_size,

false, /* should_initiate_conc_mark */

g1_policy()->max_pause_time_ms(),

gc_cause);

VMThread::execute(&op);

HeapWord* result = op.result();

bool ret_succeeded = op.prologue_succeeded() && op.pause_succeeded();

*succeeded = ret_succeeded;

return result;

}

//简单介绍下VMThread,其实是原生的vm线程

void VMThread::execute(VM_Operation* op) {

Thread* t = Thread::current();

//判断当前线程是否是vm线程,这里t->is_VM_thread()返回true

if (!t->is_VM_thread()) {

//跳过这里,其实这里的逻辑是当前不是vm线程是java线程或者watcher线程

//会先将任务放到一个queue中,之后再执行

} else {

//如果是vm线程则会进入这里

HandleMark hm(t);

_cur_vm_operation = op;

//判断任务是否需要再安全点执行且当前是否在安全点

if (op->evaluate_at_safepoint() && !SafepointSynchronize::is_at_safepoint()) {

//如果不是安全点,则等待所有线程进入安全点,然后把线程暂时挂起

//这个类中有个状态 _state,所有java线程转换线程状态时会去判断这个状态然后

//决定是否block

SafepointSynchronize::begin();

//开始任务, op是刚刚传入的VM_G1IncCollectionPause操作任务类

//evaluate()方法最后会调用gc操作任务类的doit()方法

op->evaluate();

//安全点结束

SafepointSynchronize::end();

} else {

//是安全点则直接执行

op->evaluate();

}

if (op->is_cheap_allocated()) delete op;

_cur_vm_operation = prev_vm_operation;

}

}

SafepointSynchronize::begin()方法内包括了准备进入安全点到所有java线程Block的过程,此时youngGC的全局停顿开始了,之前和一些小伙伴沟通的时候发现有的小伙伴完全不知道G1的youngGC过程会发生全局停顿,导致程序出现不能提供服务的时候只盲目地去查fullGC日志,实际上通过源码我们可以直到G1的youngGC过程也会产生全局停顿,这是我们平时容易忽视的一点。

三、youngGC之前的准备工作

================

youngGC并不是直接就开始的,再着之前还会有很多准备工作,我们先来看看doit方法:

void VM_G1IncCollectionPause::doit() {

G1CollectedHeap* g1h = G1CollectedHeap::heap();

if (_word_size > 0) {

//会再去申请一次内存

_result = g1h->attempt_allocation_at_safepoint(_word_size,

false /* expect_null_cur_alloc_region */);

if (_result != NULL) {

_pause_succeeded = true;

return;

}

}

GCCauseSetter x(g1h, _gc_cause);

// youngGC这里是之前创建VM_G1IncCollectionPause的第三个参数:false

// 表示此次youngGC不属于老年代并发gc的周期

if (_should_initiate_conc_mark) {

_old_marking_cycles_completed_before = g1h->old_marking_cycles_completed();

bool res = g1h->g1_policy()->force_initial_mark_if_outside_cycle(_gc_cause);

if (!res) {

if (_gc_cause != GCCause::_g1_humongous_allocation) {

_should_retry_gc = true;

}

return;

}

}

//gc方法

_pause_succeeded =

g1h->do_collection_pause_at_safepoint(_target_pause_time_ms);

if (_pause_succeeded && _word_size > 0) {

// 再去申请内存

_result = g1h->attempt_allocation_at_safepoint(_word_size,

true /* expect_null_cur_alloc_region */);

} else {

if (!_pause_succeeded) {

_should_retry_gc = true;

}

}

}

继续往下看:

//这个方法是gc中最长的方法,我们忽略掉一些记日志和统计数据的方法,直接看关键的几个方法

bool

G1CollectedHeap::do_collection_pause_at_safepoint(double target_pause_time_ms) {

//判断是否有线程再临界区,如果有则舍弃本次gc,并把need_gc参数设置为true

//这里是我们刚刚提到的gc_locker

if (GC_locker::check_active_before_gc()) {

return false;

}

//打印一些heap日志和统计数据

//这个方法将会判断此次youngGC是不是一次初次标记

//(即老年代并发垃圾回收时,会伴随一次youngGC,此时会返回true)

//纯youngGC阶段这里会返回false

g1_policy()->decide_on_conc_mark_initiation();

//是否处于初始标记停顿阶段(youngGC这里返回的是false)

bool should_start_conc_mark = g1_policy()->during_initial_mark_pause();

{

EvacuationInfo evacuation_info;

//youngGC这里是false我们直接跳过

if (g1_policy()->during_initial_mark_pause()) {

increment_old_marking_cycles_started();

register_concurrent_cycle_start(_gc_timer_stw->gc_start());

}

//打印GC开始日志和活跃线程数

TraceCollectorStats tcs(g1mm()->incremental_collection_counters());

TraceMemoryManagerStats tms(false /* fullGC */, gc_cause());

//将二级空闲region列表合并到主空闲列表中

//二级空闲region列表包括之前释放的region

if (!G1StressConcRegionFreeing) {

append_secondary_free_list_if_not_empty_with_lock();

}

{

IsGCActiveMark x;

//填充tlab,扫描所有java线程的tlab并用空对象填充,之后再重置

gc_prologue(false);

//启动在全局停顿时期的软引用发现执行器

ref_processor_stw()->enable_discovery(true /verify_disabled/,

true /verify_no_refs/);

{

//关闭并发扫描阶段的软引用发现执行器

NoRefDiscovery no_cm_discovery(ref_processor_cm());

// 重置_mutator_alloc_region即主动将其中的活跃区域retire但是这次不会填充还没使用的区域

// 因为马上要gc了,之后将活跃的region并将其加入到_inc_cset_head中(增量cset)

// _mutator_alloc_region前面我们提到过是申请伊甸区内存的类

// 此时增量cset中将包含所有的eden区region

release_mutator_alloc_region();

// 完成cset,这个方法会将增量cset设置成cset,并将youngRegion中的survivor区全部设置成单纯的young区

// 因为gc之后老的survivor区将变成新的eden区

// 此时cset中将包含所有的eden区

g1_policy()->finalize_cset(target_pause_time_ms, evacuation_info);

// 初始化GC要申请的region,包括要用的新的survivor区域和old区域

init_gc_alloc_regions(evacuation_info);

// 执行gc

evacuate_collection_set(evacuation_info);

//这个方法我们先看到这个里,后面是gc的收尾工作,我们之后再看

}

这里的代码比较复杂,其中cset就是我们需要回收的region集合,在youngGC中它包括所有eden区,这是youngGC中需要释放回收的区域

我们先看下执行gc的方法evacuate_collection_set(evacuation_info):

方法比较长,在看之前我们先简单介绍下几个即将涉及到的知识点:

dirty card : 脏卡,在g1中每个region被分成若干个card,card用于映射一块内存块,其中不止有一个对象,当card有一个及以上对象的字段存在跨代引用时就被标记为脏

Rset : 记忆集合,在g1中用于解决跨region引用问题,在g1中只有老年代引用新生代的对象会被记录到Rset中,这样在youngGC中就可以避免扫描整个比较大的老年代减少开销,而只用处理记忆集合就好。在g1中rset只有脏卡会被加入到rset中

dirty card queue : 脏卡队列,再执行引用赋值语句时再写屏障中会将判断当前赋值是否跨代,如果跨代则将其对应的card标记为脏并加入dirty card queue中,后续会由其他线程异步处理更新到rset中。

dirty card queue set : 脏卡队列集合,其中由所有脏卡队列,当一个脏卡队列满时会在其中记录,youngGC时会将脏卡集合队列中满的脏卡队列更新到rset中。

这里涉及到的就是g1的写屏障和记忆集合,卡表等知识点,以后有机会笔者再专门介绍下相关知识,这里我们先简单了解下。

void G1CollectedHeap::evacuate_collection_set(EvacuationInfo& evacuation_info) {

//这个方法会把dcqs中没有满的dcq加入满的集合

//因为之后要更新脏卡到rset中,所以这里会把所有没满的集合标记成满的集合,之后只需要处理被标记为满的queue就可以了

g1_rem_set()->prepare_for_oops_into_collection_set_do();

// 先关闭热卡缓存

G1HotCardCache* hot_card_cache = _cg1r->hot_card_cache();

hot_card_cache->reset_hot_cache_claimed_index();

hot_card_cache->set_use_cache(false);

// 获取gc工作线程

uint n_workers;

if (G1CollectedHeap::use_parallel_gc_threads()) {

n_workers =

AdaptiveSizePolicy::calc_active_workers(workers()->total_workers(),

workers()->active_workers(),

Threads::number_of_non_daemon_threads());

workers()->set_active_workers(n_workers);

set_par_threads(n_workers);

} else {

n_workers = 1;

}

//youngGC任务类

G1ParTask g1_par_task(this, _task_queues);

init_for_evac_failure(NULL);

rem_set()->prepare_for_younger_refs_iterate(true);

{

StrongRootsScope srs(this);

//使用gc工作线程执行gc任务

if (G1CollectedHeap::use_parallel_gc_threads()) {

// The individual threads will set their evac-failure closures.

if (ParallelGCVerbose) G1ParScanThreadState::print_termination_stats_hdr();

// These tasks use ShareHeap::_process_strong_tasks

workers()->run_task(&g1_par_task);

} else {

g1_par_task.set_for_termination(n_workers);

g1_par_task.work(0);

}

end_par_time_sec = os::elapsedTime();

}

set_par_threads(0);

//在之前我们开启了软引用执行器,这里会处理软引用

process_discovered_references(n_workers);

//处理弱引用

{

G1STWIsAliveClosure is_alive(this);

G1KeepAliveClosure keep_alive(this);

JNIHandles::weak_oops_do(&is_alive, &keep_alive);

}

release_gc_alloc_regions(n_workers, evacuation_info);

g1_rem_set()->cleanup_after_oops_into_collection_set_do();

//重新启动热卡缓存

hot_card_cache->reset_hot_cache();

hot_card_cache->set_use_cache(true);

}

三、扫描根节点

=======

下面我们终于可以看到我们所熟知的扫描根节点的方法,网上许多文章都是从这一步开始的,但其实youngGC在这之前做的准备还是很多的,而且这些准备都是包括在停顿范围内的,所以youngGC的停顿时间不止是从扫描根节点开始(虽然大部分时候准备时间可以忽略不计),这些只有亲自看了源码才能理解到。

//G1ParTask的work方法,youngGC入口

void work(uint worker_id) {

{

ResourceMark rm;

HandleMark hm;

//这边声明了许多闭包

ReferenceProcessor* rp = _g1h->ref_processor_stw();

//线程扫描状态闭包,里面有一个rset引用queue用于作为rset的缓冲

//这里我们先关注下这个pss后面更新rset时会用到

G1ParScanThreadState pss(_g1h, worker_id);

G1ParScanHeapEvacClosure scan_evac_cl(_g1h, &pss, rp);

G1ParScanHeapEvacFailureClosure evac_failure_cl(_g1h, &pss, rp);

G1ParScanPartialArrayClosure partial_scan_cl(_g1h, &pss, rp);

//这里会把扫描闭包放入pss

pss.set_evac_closure(&scan_evac_cl);

pss.set_evac_failure_closure(&evac_failure_cl);

pss.set_partial_scan_closure(&partial_scan_cl);

//这些都是别名实际上是 G1ParCopyClosure迭代器

//只扫描根

//扫描根的迭代器纯youngGC中使用的时这个

G1ParScanExtRootClosure only_scan_root_cl(_g1h, &pss, rp);

//只扫描元数据

G1ParScanMetadataClosure only_scan_metadata_cl(_g1h, &pss, rp);

//扫描根的迭代器

OopClosure* scan_root_cl = &only_scan_root_cl;

G1KlassScanClosure* scan_klasses_cl = &only_scan_klasses_cl_s;

//用来将rset中的card推入queue中后面统一copy对象的闭包

//pss中有处理rset中引用的queue

G1ParPushHeapRSClosure push_heap_rs_cl(_g1h, &pss);

int so = SharedHeap::SO_AllClasses | SharedHeap::SO_Strings;

//扫描根节点,第三个参数是G1ParScanExtRootClosure是主要的迭代器

//第四个参数是G1ParScanThreadState

_g1h->g1_process_strong_roots(/* is scavenging */ true,

SharedHeap::ScanningOption(so),

scan_root_cl,

&push_heap_rs_cl,

scan_klasses_cl,

worker_id);

{

double start = os::elapsedTime();

//pss中有处理rset中引用的queue

G1ParEvacuateFollowersClosure evac(_g1h, &pss, _queues, &_terminator);

//更新rset,这个方法我们注意下后面会讲到

evac.do_void();

}

}

}

};

void

G1CollectedHeap::

g1_process_strong_roots(bool is_scavenging,

ScanningOption so,

OopClosure* scan_non_heap_roots,

OopsInHeapRegionClosure* scan_rs,

G1KlassScanClosure* scan_klasses,

int worker_i) {

//根迭代器

//缓冲迭代器,传入的即上面提到G1ParScanExtRootClosure

//缓冲迭代器即是将迭代任务缓冲起来分批执行即将扫描gcRoot的任务缓冲到里面执行

BufferingOopClosure buf_scan_non_heap_roots(scan_non_heap_roots);

CodeBlobToOopClosure eager_scan_code_roots(scan_non_heap_roots, true /* do_marking */);

//扫描根的方法,这个方法会将gcRoot分别进行扫描,我们就不展开看了

//我们看下刚刚提到的迭代器G1ParScanExtRootClosure,这个是主要处理gcRoot的方法

process_strong_roots(false, // no scoping; this is parallel code

is_scavenging, so,

&buf_scan_non_heap_roots,

&eager_scan_code_roots,

scan_klasses

);

// 等待缓冲迭代器执行完毕,此时GCRoot已经扫描完毕

buf_scan_non_heap_roots.done();

//省略一些记录时间的代码

最后

针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。

最新整理面试题
在这里插入图片描述

上述的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题

最新整理电子书

在这里插入图片描述

最新整理大厂面试文档

在这里插入图片描述

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

InHeapRegionClosure* scan_rs,

G1KlassScanClosure* scan_klasses,

int worker_i) {

//根迭代器

//缓冲迭代器,传入的即上面提到G1ParScanExtRootClosure

//缓冲迭代器即是将迭代任务缓冲起来分批执行即将扫描gcRoot的任务缓冲到里面执行

BufferingOopClosure buf_scan_non_heap_roots(scan_non_heap_roots);

CodeBlobToOopClosure eager_scan_code_roots(scan_non_heap_roots, true /* do_marking */);

//扫描根的方法,这个方法会将gcRoot分别进行扫描,我们就不展开看了

//我们看下刚刚提到的迭代器G1ParScanExtRootClosure,这个是主要处理gcRoot的方法

process_strong_roots(false, // no scoping; this is parallel code

is_scavenging, so,

&buf_scan_non_heap_roots,

&eager_scan_code_roots,

scan_klasses

);

// 等待缓冲迭代器执行完毕,此时GCRoot已经扫描完毕

buf_scan_non_heap_roots.done();

//省略一些记录时间的代码

最后

针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。

最新整理面试题
[外链图片转存中…(img-2KJ8icjW-1715267730744)]

上述的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题

最新整理电子书

[外链图片转存中…(img-txsklrta-1715267730745)]

最新整理大厂面试文档

[外链图片转存中…(img-nz9NSiJs-1715267730745)]

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值