最后
针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。
上述的面试题答案都整理成文档笔记。 也还整理了一些面试资料&最新2021收集的一些大厂的面试真题(都整理成文档,小部分截图)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
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();
//省略一些记录时间的代码
…
// 更新rset这个方法我们之后会讲
if (scan_rs != NULL) {
g1_rem_set()->oops_into_collection_set_do(scan_rs, &eager_scan_code_roots, worker_i);
}
_process_strong_tasks->all_tasks_completed();
}
看下刚刚我们提到的G1ParScanExtRootClosure,其实是一个别名:
typedef G1ParCopyClosure<false, G1BarrierNone, false> G1ParScanExtRootClosure
G1ParCopyClosure的迭代方法才是真正的遍历gcRoot引用的方法:
//真正的迭代方法
//三个泛型 <是否用屏障,屏障类型,是否标记>
//这里传入的是 false, G1BarrierNone, false
template <bool do_gen_barrier, G1Barrier barrier, bool do_mark_object>
template
void G1ParCopyClosure<do_gen_barrier, barrier, do_mark_object>
::do_oop_work(T* p) {
//根据oop指针获取对象值(本质是解引用)
//这里获取的是gcRoot引用的对象
oop obj = oopDesc::load_decode_heap_oop§;
// 快速判断对象是否在cset回收集合中
if (_g1->in_cset_fast_test(obj)) {
oop forwardee;
// 判断对象是否被标记,根据对象头地址判断
// 这里会判断对象头中的标记信息
if (obj->is_forwarded()) {
//这里证明已经被扫描过且复制过,所以返回不带锁标记的对象地址
forwardee = obj->forwardee();
} else {
//没被标记则复制对象到survivor区,返回新对象的引用
//这个方法会判断对象的年龄,根据年龄去survivor区或者old区申请内存并复制对象
//具体我们就不进行展开了
forwardee = copy_to_survivor_space(obj);
}
//修改引用 p->fowardee
oopDesc::encode_store_heap_oop(p, forwardee);
…
} else {
…
}
…
}
我们可以看到对gcRoot扫描和复制对象的过程在youngGC中是一起进行的,并不是像一些其他文章一样是同时进行的。
这里我们简单解释下g1是如何如理标记对象的以便于我们和zgc比较学习,可以看到是先获取对象之后判断对象头的高位地址的最后两位来处理标记,而zgc则直接读取对象指针就可以判断:
四、处理dcq和Rset中的跨代引用card
======================
接下来是该处理Rset了,这个步骤本质其实是将dcq和rset中的存在跨代引用的card全部加入到pss(G1ParScanThreadState)中的queue中,网上许多其他文章都是简单说这里是更新Rset,其实有很多细节还需要学习源码才能学到。
我们先回到我们刚刚提到的那个g1_rem_set()->
oops_into_collection_set_do(scan_rs, &eager_scan_code_roots, worker_i)方法:
void G1RemSet::oops_into_collection_set_do(OopsInHeapRegionClosure* oc,
CodeBlobToOopClosure* code_root_cl,
int worker_i) {
//缓存闭包(更新rs迭代方法)
_cset_rs_update_cl[worker_i] = oc;
//这里的into_cset_dirty_card_queue_set()本质是属于JavaThread::dirty_card_queue_set()
//即dcqs,但其代表回收集合的dcqs再gc之后会进行处理,主要是保存引用了回收集合中对象的card
DirtyCardQueue into_cset_dcq(&_g1->into_cset_dirty_card_queue_set());
//更新rset
if (G1UseParallelRSetUpdating || (worker_i == 0)) {
updateRS(&into_cset_dcq, worker_i);
} else {
_g1p->phase_times()->record_update_rs_processed_buffers(worker_i, 0);
_g1p->phase_times()->record_update_rs_time(worker_i, 0.0);
}
//扫描并处理rset
if (G1UseParallelRSetScanning || (worker_i == 0)) {
scanRS(oc, code_root_cl, worker_i);
} else {
_g1p->phase_times()->record_scan_rs_time(worker_i, 0.0);
}
_cset_rs_update_cl[worker_i] = NULL;
}
//先看更新Rset
void G1RemSet::updateRS(DirtyCardQueue* into_cset_dcq, int worker_i) {
double start = os::elapsedTime();
// 处理card的闭包,这里我们留意下之后会碰到
RefineRecordRefsIntoCSCardTableEntryClosure into_cset_update_rs_cl(_g1, into_cset_dcq);
// 进行迭代
_g1->iterate_dirty_card_closure(&into_cset_update_rs_cl, into_cset_dcq, false, worker_i);
…
}
void G1CollectedHeap::iterate_dirty_card_closure(CardTableEntryClosure* cl,
DirtyCardQueue* into_cset_dcq,
bool concurrent,
int worker_i) {
G1HotCardCache* hot_card_cache = _cg1r->hot_card_cache();
// 这里会更新热卡,如果失败则将卡入列到刚刚创建的dcq中,此dcq属于dcqs集合
hot_card_cache->drain(worker_i, g1_rem_set(), into_cset_dcq);
//这里可能有点晕,我们简单理解之前创建的dcq用于处理回收集合,最后还是要归于dcqs来统一处理
DirtyCardQueueSet& dcqs = JavaThread::dirty_card_queue_set();
int n_completed_buffers = 0;
//处理dcqs中满的队列,在我们准备阶段已经把不满的队列标记为满的队列,所以这里相当于处理所有dcq
while (dcqs.apply_closure_to_completed_buffer(cl, worker_i, 0, true)) {
n_completed_buffers++;
}
dcqs.clear_n_completed_buffers();
}
bool DirtyCardQueueSet::apply_closure_to_completed_buffer(CardTableEntryClosure* cl,
总结:绘上一张Kakfa架构思维大纲脑图(xmind)
其实关于Kafka,能问的问题实在是太多了,扒了几天,最终筛选出44问:基础篇17问、进阶篇15问、高级篇12问,个个直戳痛点,不知道如果你不着急看答案,又能答出几个呢?
若是对Kafka的知识还回忆不起来,不妨先看我手绘的知识总结脑图(xmind不能上传,文章里用的是图片版)进行整体架构的梳理
梳理了知识,刷完了面试,如若你还想进一步的深入学习解读kafka以及源码,那么接下来的这份《手写“kafka”》将会是个不错的选择。
-
Kafka入门
-
为什么选择Kafka
-
Kafka的安装、管理和配置
-
Kafka的集群
-
第一个Kafka程序
-
Kafka的生产者
-
Kafka的消费者
-
深入理解Kafka
-
可靠的数据传递
-
Spring和Kafka的整合
-
SpringBoot和Kafka的整合
-
Kafka实战之削峰填谷
-
数据管道和流式处理(了解即可)
解之前创建的dcq用于处理回收集合,最后还是要归于dcqs来统一处理
DirtyCardQueueSet& dcqs = JavaThread::dirty_card_queue_set();
int n_completed_buffers = 0;
//处理dcqs中满的队列,在我们准备阶段已经把不满的队列标记为满的队列,所以这里相当于处理所有dcq
while (dcqs.apply_closure_to_completed_buffer(cl, worker_i, 0, true)) {
n_completed_buffers++;
}
dcqs.clear_n_completed_buffers();
}
bool DirtyCardQueueSet::apply_closure_to_completed_buffer(CardTableEntryClosure* cl,
总结:绘上一张Kakfa架构思维大纲脑图(xmind)
[外链图片转存中…(img-aP5jEtrC-1715254728726)]
其实关于Kafka,能问的问题实在是太多了,扒了几天,最终筛选出44问:基础篇17问、进阶篇15问、高级篇12问,个个直戳痛点,不知道如果你不着急看答案,又能答出几个呢?
若是对Kafka的知识还回忆不起来,不妨先看我手绘的知识总结脑图(xmind不能上传,文章里用的是图片版)进行整体架构的梳理
梳理了知识,刷完了面试,如若你还想进一步的深入学习解读kafka以及源码,那么接下来的这份《手写“kafka”》将会是个不错的选择。
-
Kafka入门
-
为什么选择Kafka
-
Kafka的安装、管理和配置
-
Kafka的集群
-
第一个Kafka程序
-
Kafka的生产者
-
Kafka的消费者
-
深入理解Kafka
-
可靠的数据传递
-
Spring和Kafka的整合
-
SpringBoot和Kafka的整合
-
Kafka实战之削峰填谷
-
数据管道和流式处理(了解即可)
[外链图片转存中…(img-qESz3Cbc-1715254728726)]
[外链图片转存中…(img-UhF0ctPB-1715254728727)]