@@ -451,11 +451,124 @@ func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
451451 // 当前是否在 GC mark 阶段且 write barrier 启用
452452 // 如果需要,mutator(分配者)需要帮忙标记一些对象
453453 if gcBlackenEnabled != 0 {
454- deductAssistCredit (size) // 扣除助理信用 ,并可能触发 gcDrain
454+ deductAssistCredit (size) // 借款 ,并可能触发 gcDrain
455455 }
456456
457457 ...
458458}
459459```
460460
461- 通过上面的分析,我们可以发现,我们的 GC 是通过广度优先搜索的方式去从堆上扫描对象来进行回收,也就是说,如果堆上的内存小,但是对象多,就会给 GC 带来很大的压力,所以这就是我们需要进行逃逸分析,在一些情况下尽量避免内存逃逸到堆上,看完这部分源码我觉得《Go 语言设计与实现》讲的是真的不错,但是真的得自己再去看看源码才能把整个链路搞明白,光看书还是很糊里糊涂的。
461+ 在这里借款之后,如果发现分配的 size 过大,哪怕是从全局借款中也没办法抵消债务,那么就会触发 gcDrainN,强制执行一部分标记工作,当然,如果还是不够,那么就会直接让这个 goroutine “坐牢”,即不能让他继续被 P 调度,同时放入 AssistQueue,这里的意思就是说,当前 goroutine 借款过多,无法继续调度,需要等待其他的 gcWorker 去执行标记工作,以此来生产借款到全局债务中,此时,我们又可以唤醒这些 AssistQueue 中的 goroutine,也就是让他们能够继续运行。总的来说,其实就是一个生产者消费者的协作模型,当一部分 goroutine 需要申请大量内存,而标记的 worker 速度跟不上的时候,此时就会阻塞这些 goroutine 进行执行,直到 gcworker 的速度跟上申请的速度,此时就会让他们继续执行,借款的链路为 gcAssistAlloc->gcParkAssist:
462+ ``` go
463+ // gcParkAssist 将当前的 goroutine 放入 assist 队列并将其挂起,
464+ // 直到满足 GC 协助条件。协助标记的任务由多个 goroutine 共同完成。
465+ //
466+ // 当返回值为 true 时,表示该协助任务已经完成,goroutine 可继续执行。
467+ // 如果返回 false,说明协助任务还未完成,调用者应当重试协助。
468+ //
469+ // 该函数通过加锁、检查 GC 状态、挂起当前 goroutine 来保证协助任务的顺利进行。
470+ // 它帮助实现协作式垃圾回收,避免 GC 阻塞或资源浪费。
471+ func gcParkAssist () bool {
472+ // 加锁以确保对 assistQueue 的操作是线程安全的
473+ lock (&work.assistQueue .lock )
474+
475+ // 如果 GC 循环已经完成,则直接退出协助,返回 true
476+ // 因为在持有锁时,GC 周期无法结束
477+ if atomic.Load (&gcBlackenEnabled) == 0 {
478+ unlock (&work.assistQueue .lock )
479+ return true
480+ }
481+
482+ // 获取当前的 goroutine(gp),并将其加入 assist 队列
483+ gp := getg ()
484+ oldList := work.assistQueue .q
485+ work.assistQueue .q .pushBack (gp)
486+
487+ // 重新检查背景扫描的 credit,以确保当前的挂起 goroutine 不会被漏掉
488+ // 如果背景标记已生成足够的 credit,则可以让当前 goroutine 继续执行
489+ if gcController.bgScanCredit .Load () > 0 {
490+ // 恢复队列状态,取消挂起的 goroutine
491+ work.assistQueue .q = oldList
492+ if oldList.tail != 0 {
493+ oldList.tail .ptr ().schedlink .set (nil )
494+ }
495+ unlock (&work.assistQueue .lock )
496+ return false
497+ }
498+
499+ // 如果 credit 不够,挂起当前 goroutine
500+ goparkunlock (&work.assistQueue .lock , waitReasonGCAssistWait, traceBlockGCMarkAssist, 2 )
501+ return true
502+ }
503+ ```
504+ 而我们的每次 gcWorker 执行了标记工作之后,都会去调用 ` gcFlushBgCredit ` 尝试去唤醒这些消费者:
505+ ``` go
506+ // gcFlushBgCredit 将指定数量的后台扫描工作单位(scanWork)信用刷新到后台扫描信用池。
507+ // 它首先会满足阻塞在工作队列中的 goroutine 的协助债务,然后将剩余的信用刷新到
508+ // gcController.bgScanCredit,供其他需要的协助任务使用。
509+ //
510+ // 由于这是由 gcDrain 使用,在执行时确保所有的工作都已完成,所以在该函数中不允许
511+ // 写入屏障。
512+ //
513+ // 该函数的核心逻辑是分配信用并协助完成挂起的协助任务,保证 GC 协作过程的平衡。
514+ // go:nowritebarrierrec
515+ func gcFlushBgCredit (scanWork int64 ) {
516+ // 如果 assist 队列为空,则表示没有待协助的 goroutine,直接将扫描工作信用加到后台信用池
517+ if work.assistQueue .q .empty () {
518+ // 快速路径;没有阻塞的协助任务。这里有一个小的窗口,如果有协助任务被加入并挂起,
519+ // 它会在下一次调用时处理。
520+ gcController.bgScanCredit .Add (scanWork)
521+ return
522+ }
523+
524+ // 计算每单位扫描工作需要多少字节的协助信用
525+ assistBytesPerWork := gcController.assistBytesPerWork .Load ()
526+ scanBytes := int64 (float64 (scanWork) * assistBytesPerWork)
527+
528+ // 加锁,确保对 assistQueue 操作的线程安全
529+ lock (&work.assistQueue .lock )
530+
531+ // 遍历队列中的所有阻塞 goroutine,尝试用当前扫描信用偿还它们的协助债务
532+ for !work.assistQueue .q .empty () && scanBytes > 0 {
533+ gp := work.assistQueue .q .pop ()
534+
535+ // 注意,gp.gcAssistBytes 是负数,因为 goroutine 之前积累了协助债务
536+ // 判断当前扫描信用是否能够满足 goroutine 的债务
537+ if scanBytes+gp.gcAssistBytes >= 0 {
538+ // 如果当前的信用足够偿还整个债务,更新扫描字节并清空债务
539+ scanBytes += gp.gcAssistBytes
540+ gp.gcAssistBytes = 0
541+
542+ // 注意:不要将这个 goroutine 放到 runnext 队列中,以避免它的高优先级
543+ // 被滥用,阻塞其他 goroutine 执行。
544+ ready (gp, 0 , false )
545+ } else {
546+ // 如果信用不足以偿还整个债务,只偿还部分债务
547+ gp.gcAssistBytes += scanBytes
548+ scanBytes = 0
549+
550+ // 为了避免大的协助任务堵塞队列,我们将该任务移到队列的末尾,
551+ // 确保小的协助任务能及时得到处理。
552+ work.assistQueue .q .pushBack (gp)
553+ break
554+ }
555+ }
556+
557+ // 如果仍然有剩余的扫描字节(信用不足以偿还所有的协助债务),
558+ // 我们将它们转回到后台扫描信用池中
559+ if scanBytes > 0 {
560+ // 将剩余的扫描字节转换为相应的工作量
561+ assistWorkPerByte := gcController.assistWorkPerByte .Load ()
562+ scanWork = int64 (float64 (scanBytes) * assistWorkPerByte)
563+ gcController.bgScanCredit .Add (scanWork)
564+ }
565+
566+ // 解锁,完成当前的工作信用分配
567+ unlock (&work.assistQueue .lock )
568+ }
569+ ```
570+
571+
572+ 通过上面的分析,我们可以发现,我们的 GC 是通过广度优先搜索的方式去从堆上扫描对象来进行回收,也就是说,如果堆上的内存小,但是对象多,就会给 GC 带来很大的压力,所以这就是我们需要进行逃逸分析,在一些情况下尽量避免内存逃逸到堆上;除了这些 GC 的步骤之外,还引入了租约的制度来平衡申请内存和标记的速度。
573+
574+ 看完这部分源码我觉得《Go 语言设计与实现》讲的是真的不错,但是真的得自己再去看看源码才能把整个链路搞明白,光看书还是很糊里糊涂的。
0 commit comments