各位老铁们好,相信很多人对go电商网站源码分享都不是特别的了解,因此呢,今天就来为大家分享下关于go电商网站源码分享以及go实战电商网站的问题知识,还望可以帮助大家,解决大家的一些困惑,下面一起来看看吧!
通常C++通过指针引用计数来回收对象,但是这不能处理循环引用。为了避免引用计数的缺陷,后来出现了标记清除,分代等垃圾回收算法。Go的垃圾回收官方形容为非分代非紧缩写屏障并发标记清理。标记清理算法的字面解释,就是将可达的内存块进行标记mark,最后没有标记的不可达内存块将进行清理sweep。
三色标记法
判断一个对象是不是垃圾需不需要标记,就看是否能从当前栈或全局数据区直接或间接的引用到这个对象。这个初始的当前goroutine的栈和全局数据区称为GC的root区。扫描从这里开始,通过markroot将所有root区域的指针标记为可达,然后沿着这些指针扫描,递归地标记遇到的所有可达对象。因此引出几个问题:
标记清理能不能与用户代码并发
如何获得对象的类型而找到所有可达区域标记位记录在哪里
何时触发标记清理
如何并发标记
标记清扫算法在标记和清理时需要停止所有的goroutine,来保证已经被标记的区域不会被用户修改引用关系,造成清理错误。但是每次GC都要StopTheWorld显然是不能接受的。Go的各个版本为减少STW做了各种努力。从Go1.5开始采用三色标记法实现标记阶段的并发。
最开始所有对象都是白色
扫描所有可达对象,标记为灰色,放入待处理队列
从队列提取灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色
写屏障监控对象内存修改,重新标色或是放入队列
完成标记后对象不是白色就是黑色,清理操作只需要把白色对象回收内存回收就好。
大概理解所谓并发标记,首先是指能够跟用户代码并发的进行,其次是指标记工作不是递归地进行,而是多个goroutine并发的进行。前者通过write-barrier解决并发问题,后者通过gc-work队列实现非递归地mark可达对象。
write-barrier
用下面这个例子解释并发带来的问题,原文引用自CMS垃圾回收器原理。当从A这个GCroot找到引用对象B时,B变灰A变黑。这时用户goroutine执行把A到B的引用改成了A到C的引用,同时B不再引用C。然后GCgoroutine又执行,发现B没有引用对象,B变黑。而这时由于A已经变黑完成了扫描,C将当做白色不可达对象被清除。
解决办法:引入写屏障。当发现A已经标记为黑色了,若A又引用C,那么把C变灰入队。这个write_barrier是编译器在每一处内存写操作前生成一小段代码来做的。
//写屏障伪代码\nwrite_barrier(obj,field,newobj){\nif(newobj.mark==FALSE){\nnewobj.mark=TRUE\npush(newobj,$mark_stack)\n}\n*field=newobj\n}
gc-work
如何非递归的实现遍历mark可达节点,显然需要一个队列。
这个队列也帮助区分黑色对象和灰色对象,因为标记位只有一个。标记并且在队列中的是灰色对象,标记了但是不在队列中的黑色对象,末标记的是白色对象。
rootnodequeuewhile(queueisnotnil){\ndequeue//节点出队\nprocess//处理当前节点\nchildnodequeue//子节点入队\n}
总结一下并发标记的过程:
gcstart启动阶段准备了N个goMarkWorkers。每个worker都处理以下相同流程。
如果是第一次mark则首先markroot将所有root区的指针入队。
从gcw中取节点出对开始扫描处理scanobject,节点出队列就是黑色了。
扫描时获取该节点所有子节点的类型信息判断是不是指针,若是指针且并没有被标记则greyobject入队。
每个worker都去gcw中拿任务直到为空break。
//每个markWorker都执行gcDrain这个标记过程\nfuncgcDrain(gcw*gcWork,flagsgcDrainFlags){\n//如果还没有root区域入队则markroot\nmarkroot(gcw,job)\nifidle&&pollWork(){\ngotodone\n}\n//节点出队\nb=gcw.get()\nscanobject(b,gcw)\ndone:\n}\n\nfuncscanobject(buintptr,gcw*gcWork){\nhbits:=heapBitsForAddr(b)\ns:=spanOfUnchecked(b)\nn:=s.elemsize\nfori=0;i<n;i+=sys.PtrSize{\n//Findbitsforthisword.\nifbits&bitPointer==0{\ncontinue//notapointer\n}….\n//Marktheobject.\nifobj,hbits,span,objIndex:=heapBitsForObject(obj,b,i);obj!=0{\ngreyobject(obj,b,i,hbits,span,gcw,objIndex)\n}\n}\ngcw.bytesMarked+=uint64(n)\ngcw.scanWork+=int64(i)}funcgreyobject(obj,base,offuintptr,hbitsheapBits,\nspan*mspan,gcw*gcWork,objIndexuintptr){\nmbits:=span.markBitsForIndex(objIndex)\n//Ifmarkedwehavenothingtodo.\nifmbits.isMarked(){\nreturn\n}\nif!hbits.hasPointers(span.elemsize){\nreturn\n}\ngcw.put(obj)\n}
标记位
实现精确地垃圾回收的前提,就是能获得对象区域的类型信息,从而判断是否是指针。如何判断,最后又把可达标记记在哪里:通过堆区arena前面对应的bitmap。
结构体中不包含指针,其实不需要递归地标记结构体成员。如果没有类型信息只能对所有的结构体成员递归地标记下去。还有如果非指针成员刚好存储的内容对应着合法地址,那这个地址的对象就会碰巧被标记,导致无法回收。
这个bitmap位图区域每个字(32位或64位)会对应4位的标记位。
heapBitsForAddr
可以获取对应堆地址的bitmap位hbits,根据它可以判断是否是指针,如果是指针且之前没有被标记过,则mark当前对象为可达,并且greayObject入队,供给其他的markWorker来处理。
//获取b对应的bitmap位图\nobj,hbits,span,objIndex:=heapBitsForObject(obj,b,i)\nmbits:=span.markBitsForIndex(objIndex)\n//判断是否被标记过已标记或不是指针都不入队\nmbits.isMarked()hbits.hasPointers(span.elemsize)
gc_trigger最开始是4MB,next_gc初始为4MB,之后每次标记完成时将重新计算动态调整值大小。但gc_trigger至少要大于初始的4MB,同时至少要比当前使用的heap大1MB。
gcmark在每次标记结束后重置阈值大小。当前使用了4MB内存,这时设置gc_trigger为2*4MB,也就是当内存分配到8MB时会再次触发GC。回收之后内存为5MB,那下一次要达到10MB才会触发GC。这个比例triggerRatio是由gcpercent/100决定的。
funcgcinit(){\n_=setGCPercent(readgogc())\nmemstats.gc_trigger=heapminimum\nmemstats.next_gc=uint64(float64(memstats.gc_trigger)/(1+\ngcController.triggerRatio)*(1+float64(gcpercent)/100))\nwork.startSema=1\nwork.markDoneSema=1\n}\n\nfuncgcMark(){\nmemstats.gc_trigger=uint64(float64(memstats.heap_marked)*\n(1+gcController.triggerRatio))\n}
强制垃圾回收
如果系统启动或短时间内大量分配对象,会将垃圾回收的gc_trigger推高。当服务正常后,活跃对象远小于这个阈值,造成垃圾回收无法触发。这个问题交给sysmon解决。它每隔2分钟force触发GC一次。这个forcegc的goroutine一直park在后台,直到sysmon将它唤醒开始执行gc而不用检查阈值。
//proc.govarforcegcperiodint64=2*60*1e9\nfuncinit(){goforcegchelper()}funcsysmon(){\nlastgc:=int64(atomic.Load64(&memstats.last_gc))\nifgcphase==_GCoff&&lastgc!=0&&\nunixnow-lastgc>forcegcperiod&&\natomic.Load(&forcegc.idle)!=0{\ninjectglist(forcegc.g)\n}\n}\n\nfuncforcegchelper(){\nfor{\ngoparkunlock(&forcegc.lock,”forcegc(idle)”,traceEvGoBlock,1)\ngcStart(gcBackgroundMode,true)\n}\n}
标记与清理过程
这里结合gc-work那一节从头梳理一下gc的启动和流程。下面这个图总结了mark-sweep所有的状态变化。在代码里只有三个GC状态,分别对应这几个阶段。总结两个问题:
为什么markTermination需要rescan全局指针和栈。因为mark阶段是跟用户代码并发的,所以有可能栈上都分了新的对象,这些对象通过writebarrier记录下来,在rescan的时候再检查一遍。
为什么还需要两个stopTheWorld在GCtermination时需要STW不然永远都可能栈上出现新对象。在GC开始之前做准备工作(比如enablewritebarrier)的时候也要STW。
Off:_GCoff
Stackscan+Mark:_GCmark
Marktermination:_GCmarktermination
GofftoGmark
gcstart由每次mallocgc触发,当然要满足gc_trriger等阈值条件才触发。整个启动过程都是STW的,它启动了所有将并发执行标记工作的goroutine,然后进入GCMark状态使能写屏障,启动gcController。
funcgcStart(modegcMode,forceTriggerbool){\n//启动MarkStartWorkers的goroutine\nifmode==gcBackgroundMode{\ngcBgMarkStartWorkers()\n}\ngcResetMarkState()\nsystemstack(stopTheWorldWithSema)\n//完成之前的清理工作\nsystemstack(func(){\nfinishsweep_m()\n})\n//进入Mark状态使能写屏障\nifmode==gcBackgroundMode{\ngcController.startCycle()\nsetGCPhase(_GCmark)\ngcBgMarkPrepare()\ngcMarkRootPrepare()\natomic.Store(&gcBlackenEnabled,1)\nsystemstack(startTheWorldWithSema)\n}\n}
Gmark
解释一下gcMarkWorker跟gcController的关系。gcstart中只是为所有的P都准备好对应的goroutine来做标记。但是他们一开始就gopark住当前G,直到被gccontroller的findRunnableGCWorker唤醒。goroutine源码记录讲了goroutine的过程,m启动后会一直通过schedule查找可执行的G,其中gcworker也是G的来源,但是首先要检查当前状态是不是Gmark。如果是就唤醒worker开始标记工作。
funcgcBgMarkStartWorkers(){\nfor_,p:=range&allp{\ngogcBgMarkWorker(p)\nnotetsleepg(&work.bgMarkReady,-1)\nnoteclear(&work.bgMarkReady)\n}\n}\n\nfuncschedule(){\n…//schedule优先唤醒markworkerG但首先gcBlackenEnabled!=0\nifgp==nil&&gcBlackenEnabled!=0{\ngp=gcController.findRunnableGCWorker(_g_.m.p.ptr())\n}\n}
唤醒后开始进入mark标记工作gcDrain。gc-work那一节讲了并发标记的过程,这里不重复。总结来说就是每个worker都去队列中拿节点(黑化节点),然后处理当前节点看有没有指针和没标记的对象,继续入队子节点(灰化节点),直到队列为空再也找不到可达对象。
funcgcBgMarkWorker(_p_*p){\nnotewakeup(&work.bgMarkReady)\nfor{\ngopark(func(g*g,parkpunsafe.Pointer)bool{\n},unsafe.Pointer(park),”GCworker(idle)”,traceEvGoBlock,0)\nsystemstack(func(){\ncasgstatus(gp,_Grunning,_Gwaiting)\ngcDrain(&_p_.gcw,…)\ncasgstatus(gp,_Gwaiting,_Grunning)\n})\n//标记完成gcMarkDone()\nifincnwait==work.nproc&&!gcMarkWorkAvailable(nil){\ngcMarkDone()\n}\n}\n}
Gmarktermination
mark结束后调用gcMarkDone,它主要是StopTheWorld然后进入gcMarkTermination中的gcMark。大概是做了rescanroot区域的工作,但是看到有博客说Go1.8已经没有再rescan了,细节没看懂,代码里看起来却是又重新扫描了一次啊。
funcgcMarkTermination(){\natomic.Store(&gcBlackenEnabled,0)\nsetGCPhase(_GCmarktermination)\ncasgstatus(gp,_Grunning,_Gwaiting)\ngp.waitreason=”garbagecollection”\nsystemstack(func(){\ngcMark(startTime)\nsetGCPhase(_GCoff)\ngcSweep(work.mode)\n})\ncasgstatus(gp,_Gwaiting,_Grunning)\nsystemstack(startTheWorldWithSema)}funcgcMark(start_timeint64){\ngcMarkRootPrepare()\ngchelperstart()\ngcDrain(gcw,gcDrainBlock)\ngcw.dispose()\n//gc结束后重置gc_trigger等阈值\n…\n}
Gsweep
有多个地方可以触发sweep,比如GC标记结束会触发gcsweep。如果是并发清除,需要回收从gc_trigger到当前活跃内存的那么多heap区域,唤醒后台的sweepgoroutine。
funcgcSweep(modegcMode){\nlock(&mheap_.lock)\nmheap_.sweepgen+=2\nmheap_.sweepdone=0\nunlock(&mheap_.lock)\n//Backgroundsweep.\nready(sweep.g,0,true)}//在runtime初始化时进行gcenablefuncgcenable(){\ngobgsweep(c)}funcbgsweep(cchanint){\ngoparkunlock(&sweep.lock,”GCsweepwait”,traceEvGoBlock,1)\nfor{\nforgosweepone()!=^uintptr(0){\nsweep.nbgsweep++\nGosched()\n}\ngoparkunlock(&sweep.lock,”GCsweepwait”,traceEvGoBlock,1)\n}\n}
也就是系统初始化的时候开启了后台的bgsweepgoroutine。这个G也是一进去就park了,唤醒后执行gosweepone。seepone的过程大概是:遍历所有的spans看它的sweepgen是否需要检查,如果要就检查这个mspan里所有的object的bit位看是否需要回收。这个过程可能触发mspan到mcentral的回收,最终可能回收到mheap的freelist当中。在freelist当中的内存再超过一定阈值时间后会被sysmon建议交还给内核。
参考文章
Proposal:EliminateSTWstackre-scanning
go笔记-GC
go1.5的垃圾回收
go垃圾回收剖析
作者:nino
OK,本文到此结束,希望对大家有所帮助。
