推荐文章:
gcDrain函数扫描完根对象, 就会开始消费标记队列, 对从标记队列中取出的对象调用scanobject函数:
1// scanobject scans the object starting at b, adding pointers to gcw.
2// b must point to the beginning of a heap object or an oblet.
3// scanobject consults the GC bitmap for the pointer mask and the
4// spans for the size of the object.
5//
6//go:nowritebarrier
7func scanobject(b uintptr, gcw *gcWork) {
8 // Note that arena_used may change concurrently during
9 // scanobject and hence scanobject may encounter a pointer to
10 // a newly allocated heap object that is *not* in
11 // [start,used). It will not mark this object; however, we
12 // know that it was just installed by a mutator, which means
13 // that mutator will execute a write barrier and take care of
14 // marking it. This is even more pronounced on relaxed memory
15 // architectures since we access arena_used without barriers
16 // or synchronization, but the same logic applies.
17 arena_start := mheap_.arena_start
18 arena_used := mheap_.arena_used
19 // Find the bits for b and the size of the object at b.
20 //
21 // b is either the beginning of an object, in which case this
22 // is the size of the object to scan, or it points to an
23 // oblet, in which case we compute the size to scan below.
24 // 获取对象对应的bitmap
25 hbits := heapBitsForAddr(b)
26 // 获取对象所在的span
27 s := spanOfUnchecked(b)
28 // 获取对象的大小
29 n := s.elemsize
30 if n == 0 {
31 throw("scanobject n == 0")
32 }
33 // 对象大小过大时(maxObletBytes是128KB)需要分割扫描
34 // 每次最多只扫描128KB
35 if n > maxObletBytes {
36 // Large object. Break into oblets for better
37 // parallelism and lower latency.
38 if b == s.base() {
39 // It's possible this is a noscan object (not
40 // from greyobject, but from other code
41 // paths), in which case we must *not* enqueue
42 // oblets since their bitmaps will be
43 // uninitialized.
44 if s.spanclass.noscan() {
45 // Bypass the whole scan.
46 gcw.bytesMarked += uint64(n)
47 return
48 }
49 // Enqueue the other oblets to scan later.
50 // Some oblets may be in b's scalar tail, but
51 // these will be marked as "no more pointers",
52 // so we'll drop out immediately when we go to
53 // scan those.
54 for oblet := b + maxObletBytes; oblet < s.base()+s.elemsize; oblet += maxObletBytes {
55 if !gcw.putFast(oblet) {
56 gcw.put(oblet)
57 }
58 }
59 }
60 // Compute the size of the oblet. Since this object
61 // must be a large object, s.base() is the beginning
62 // of the object.
63 n = s.base() + s.elemsize - b
64 if n > maxObletBytes {
65 n = maxObletBytes
66 }
67 }
68 // 扫描对象中的指针
69 var i uintptr
70 for i = 0; i < n; i += sys.PtrSize {
71 // 获取对应的bit
72 // Find bits for this word.
73 if i != 0 {
74 // Avoid needless hbits.next() on last iteration.
75 hbits = hbits.next()
76 }
77 // Load bits once. See CL 22712 and issue 16973 for discussion.
78 bits := hbits.bits()
79 // 检查scan bit判断是否继续扫描, 注意第二个scan bit是checkmark
80 // During checkmarking, 1-word objects store the checkmark
81 // in the type bit for the one word. The only one-word objects
82 // are pointers, or else they'd be merged with other non-pointer
83 // data into larger allocations.
84 if i != 1*sys.PtrSize && bits&bitScan == 0 {
85 break // no more pointers in this object
86 }
87 // 检查pointer bit, 不是指针则继续
88 if bits&bitPointer == 0 {
89 continue // not a pointer
90 }
91 // 取出指针的值
92 // Work here is duplicated in scanblock and above.
93 // If you make changes here, make changes there too.
94 obj := *(*uintptr)(unsafe.Pointer(b + i))
95 // 如果指针在arena区域中, 则调用greyobject标记对象并把对象放到标记队列中
96 // At this point we have extracted the next potential pointer.
97 // Check if it points into heap and not back at the current object.
98 if obj != 0 && arena_start <= obj && obj < arena_used && obj-b >= n {
99 // Mark the object.
100 if obj, hbits, span, objIndex := heapBitsForObject(obj, b, i); obj != 0 {
101 greyobject(obj, b, i, hbits, span, gcw, objIndex)
102 }
103 }
104 }
105 // 统计扫描过的大小和对象数量
106 gcw.bytesMarked += uint64(n)
107 gcw.scanWork += int64(i)
108}
在所有后台标记任务都把标记队列消费完毕时, 会执行gcMarkDone函数准备进入完成标记阶段(mark termination):
在并行GC中gcMarkDone会被执行两次, 第一次会禁止本地标记队列然后重新开始后台标记任务, 第二次会进入完成标记阶段(mark termination)。
1// gcMarkDone transitions the GC from mark 1 to mark 2 and from mark 2
2// to mark termination.
3//
4// This should be called when all mark work has been drained. In mark
5// 1, this includes all root marking jobs, global work buffers, and
6// active work buffers in assists and background workers; however,
7// work may still be cached in per-P work buffers. In mark 2, per-P
8// caches are disabled.
9//
10// The calling context must be preemptible.
11//
12// Note that it is explicitly okay to have write barriers in this
13// function because completion of concurrent mark is best-effort
14// anyway. Any work created by write barriers here will be cleaned up
15// by mark termination.
16func gcMarkDone() {
17top:
18 semacquire(&work.markDoneSema)
19 // Re-check transition condition under transition lock.
20 if !(gcphase == _GCmark && work.nwait == work.nproc && !gcMarkWorkAvailable(nil)) {
21 semrelease(&work.markDoneSema)
22 return
23 }
24 // 暂时禁止启动新的后台标记任务
25 // Disallow starting new workers so that any remaining workers
26 // in the current mark phase will drain out.
27 //
28 // TODO(austin): Should dedicated workers keep an eye on this
29 // and exit gcDrain promptly?
30 atomic.Xaddint64(&gcController.dedicatedMarkWorkersNeeded, -0xffffffff)
31 atomic.Xaddint64(&gcController.fractionalMarkWorkersNeeded, -0xffffffff)
32 // 判断本地标记队列是否已禁用
33 if !gcBlackenPromptly {
34 // 本地标记队列是否未禁用, 禁用然后重新开始后台标记任务
35 // Transition from mark 1 to mark 2.
36 //
37 // The global work list is empty, but there can still be work
38 // sitting in the per-P work caches.
39 // Flush and disable work caches.
40 // 禁用本地标记队列
41 // Disallow caching workbufs and indicate that we're in mark 2.
42 gcBlackenPromptly = true
43 // Prevent completion of mark 2 until we've flushed
44 // cached workbufs.
45 atomic.Xadd(&work.nwait, -1)
46 // GC is set up for mark 2. Let Gs blocked on the
47 // transition lock go while we flush caches.
48 semrelease(&work.markDoneSema)
49 // 把所有本地标记队列中的对象都推到全局标记队列
50 systemstack(func() {
51 // Flush all currently cached workbufs and
52 // ensure all Ps see gcBlackenPromptly. This
53 // also blocks until any remaining mark 1
54 // workers have exited their loop so we can
55 // start new mark 2 workers.
56 forEachP(func(_p_ *p) {
57 _p_.gcw.dispose()
58 })
59 })
60 // 除错用
61 // Check that roots are marked. We should be able to
62 // do this before the forEachP, but based on issue
63 // #16083 there may be a (harmless) race where we can
64 // enter mark 2 while some workers are still scanning
65 // stacks. The forEachP ensures these scans are done.
66 //
67 // TODO(austin): Figure out the race and fix this
68 // properly.
69 gcMarkRootCheck()
70 // 允许启动新的后台标记任务
71 // Now we can start up mark 2 workers.
72 atomic.Xaddint64(&gcController.dedicatedMarkWorkersNeeded, 0xffffffff)
73 atomic.Xaddint64(&gcController.fractionalMarkWorkersNeeded, 0xffffffff)
74 // 如果确定没有更多的任务则可以直接跳到函数顶部
75 // 这样就当作是第二次调用了
76 incnwait := atomic.Xadd(&work.nwait, +1)
77 if incnwait == work.nproc && !gcMarkWorkAvailable(nil) {
78 // This loop will make progress because
79 // gcBlackenPromptly is now true, so it won't
80 // take this same "if" branch.
81 goto top
82 }
83 } else {
84 // 记录完成标记阶段开始的时间和STW开始的时间
85 // Transition to mark termination.
86 now := nanotime()
87 work.tMarkTerm = now
88 work.pauseStart = now
89 // 禁止G被抢占
90 getg().m.preemptoff = "gcing"
91 // 停止所有运行中的G, 并禁止它们运行
92 systemstack(stopTheWorldWithSema)
93 // !!!!!!!!!!!!!!!!
94 // 世界已停止(STW)...
95 // !!!!!!!!!!!!!!!!
96 // The gcphase is _GCmark, it will transition to _GCmarktermination
97 // below. The important thing is that the wb remains active until
98 // all marking is complete. This includes writes made by the GC.
99 // 标记对根对象的扫描已完成, 会影响gcMarkRootPrepare中的处理
100 // Record that one root marking pass has completed.
101 work.markrootDone = true
102 // 禁止辅助GC和后台标记任务的运行
103 // Disable assists and background workers. We must do
104 // this before waking blocked assists.
105 atomic.Store(&gcBlackenEnabled, 0)
106 // 唤醒所有因为辅助GC而休眠的G
107 // Wake all blocked assists. These will run when we
108 // start the world again.
109 gcWakeAllAssists()
110 // Likewise, release the transition lock. Blocked
111 // workers and assists will run when we start the
112 // world again.
113 semrelease(&work.markDoneSema)
114 // 计算下一次触发gc需要的heap大小
115 // endCycle depends on all gcWork cache stats being
116 // flushed. This is ensured by mark 2.
117 nextTriggerRatio := gcController.endCycle()
118 // 进入完成标记阶段, 会重新启动世界
119 // Perform mark termination. This will restart the world.
120 gcMarkTermination(nextTriggerRatio)
121 }
122}
gcMarkTermination函数会进入完成标记阶段:
1func gcMarkTermination(nextTriggerRatio float64) {
2 // World is stopped.
3 // Start marktermination which includes enabling the write barrier.
4 // 禁止辅助GC和后台标记任务的运行
5 atomic.Store(&gcBlackenEnabled, 0)
6 // 重新允许本地标记队列(下次GC使用)
7 gcBlackenPromptly = false
8 // 设置当前GC阶段到完成标记阶段, 并启用写屏障
9 setGCPhase(_GCmarktermination)
10 // 记录开始时间
11 work.heap1 = memstats.heap_live
12 startTime := nanotime()
13 // 禁止G被抢占
14 mp := acquirem()
15 mp.preemptoff = "gcing"
16 _g_ := getg()
17 _g_.m.traceback = 2
18 // 设置G的状态为等待中这样它的栈可以被扫描
19 gp := _g_.m.curg
20 casgstatus(gp, _Grunning, _Gwaiting)
21 gp.waitreason = "garbage collection"
22 // 切换到g0运行
23 // Run gc on the g0 stack. We do this so that the g stack
24 // we're currently running on will no longer change. Cuts
25 // the root set down a bit (g0 stacks are not scanned, and
26 // we don't need to scan gc's internal state). We also
27 // need to switch to g0 so we can shrink the stack.
28 systemstack(func() {
29 // 开始STW中的标记
30 gcMark(startTime)
31 // 必须立刻返回, 因为外面的G的栈有可能被移动, 不能在这之后访问外面的变量
32 // Must return immediately.
33 // The outer function's stack may have moved
34 // during gcMark (it shrinks stacks, including the
35 // outer function's stack), so we must not refer
36 // to any of its variables. Return back to the
37 // non-system stack to pick up the new addresses
38 // before continuing.
39 })
40 // 重新切换到g0运行
41 systemstack(func() {
42 work.heap2 = work.bytesMarked
43 // 如果启用了checkmark则执行检查, 检查是否所有可到达的对象都有标记
44 if debug.gccheckmark > 0 {
45 // Run a full stop-the-world mark using checkmark bits,
46 // to check that we didn't forget to mark anything during
47 // the concurrent mark process.
48 gcResetMarkState()
49 initCheckmarks()
50 gcMark(startTime)
51 clearCheckmarks()
52 }
53 // 设置当前GC阶段到关闭, 并禁用写屏障
54 // marking is complete so we can turn the write barrier off
55 setGCPhase(_GCoff)
56 // 唤醒后台清扫任务, 将在STW结束后开始运行
57 gcSweep(work.mode)
58 // 除错用
59 if debug.gctrace > 1 {
60 startTime = nanotime()
61 // The g stacks have been scanned so
62 // they have gcscanvalid==true and gcworkdone==true.
63 // Reset these so that all stacks will be rescanned.
64 gcResetMarkState()
65 finishsweep_m()
66 // Still in STW but gcphase is _GCoff, reset to _GCmarktermination
67 // At this point all objects will be found during the gcMark which
68 // does a complete STW mark and object scan.
69 setGCPhase(_GCmarktermination)
70 gcMark(startTime)
71 setGCPhase(_GCoff) // marking is done, turn off wb.
72 gcSweep(work.mode)
73 }
74 })
75 // 设置G的状态为运行中
76 _g_.m.traceback = 0
77 casgstatus(gp, _Gwaiting, _Grunning)
78 // 跟踪处理
79 if trace.enabled {
80 traceGCDone()
81 }
82 // all done
83 mp.preemptoff = ""
84 if gcphase != _GCoff {
85 throw("gc done but gcphase != _GCoff")
86 }
87 // 更新下一次触发gc需要的heap大小(gc_trigger)
88 // Update GC trigger and pacing for the next cycle.
89 gcSetTriggerRatio(nextTriggerRatio)
90 // 更新用时记录
91 // Update timing memstats
92 now := nanotime()
93 sec, nsec, _ := time_now()
94 unixNow := sec*1e9 + int64(nsec)
95 work.pauseNS += now - work.pauseStart
96 work.tEnd = now
97 atomic.Store64(&memstats.last_gc_unix, uint64(unixNow)) // must be Unix time to make sense to user
98 atomic.Store64(&memstats.last_gc_nanotime, uint64(now)) // monotonic time for us
99 memstats.pause_ns[memstats.numgc%uint32(len(memstats.pause_ns))] = uint64(work.pauseNS)
100 memstats.pause_end[memstats.numgc%uint32(len(memstats.pause_end))] = uint64(unixNow)
101 memstats.pause_total_ns += uint64(work.pauseNS)
102 // 更新所用cpu记录
103 // Update work.totaltime.
104 sweepTermCpu := int64(work.stwprocs) * (work.tMark - work.tSweepTerm)
105 // We report idle marking time below, but omit it from the
106 // overall utilization here since it's "free".
107 markCpu := gcController.assistTime + gcController.dedicatedMarkTime + gcController.fractionalMarkTime
108 markTermCpu := int64(work.stwprocs) * (work.tEnd - work.tMarkTerm)
109 cycleCpu := sweepTermCpu + markCpu + markTermCpu
110 work.totaltime += cycleCpu
111 // Compute overall GC CPU utilization.
112 totalCpu := sched.totaltime + (now-sched.procresizetime)*int64(gomaxprocs)
113 memstats.gc_cpu_fraction = float64(work.totaltime) / float64(totalCpu)
114 // 重置清扫状态
115 // Reset sweep state.
116 sweep.nbgsweep = 0
117 sweep.npausesweep = 0
118 // 统计强制开始GC的次数
119 if work.userForced {
120 memstats.numforcedgc++
121 }
122 // 统计执行GC的次数然后唤醒等待清扫的G
123 // Bump GC cycle count and wake goroutines waiting on sweep.
124 lock(&work.sweepWaiters.lock)
125 memstats.numgc++
126 injectglist(work.sweepWaiters.head.ptr())
127 work.sweepWaiters.head = 0
128 unlock(&work.sweepWaiters.lock)
129 // 性能统计用
130 // Finish the current heap profiling cycle and start a new
131 // heap profiling cycle. We do this before starting the world
132 // so events don't leak into the wrong cycle.
133 mProf_NextCycle()
134 // 重新启动世界
135 systemstack(startTheWorldWithSema)
136 // !!!!!!!!!!!!!!!
137 // 世界已重新启动...
138 // !!!!!!!!!!!!!!!
139 // 性能统计用
140 // Flush the heap profile so we can start a new cycle next GC.
141 // This is relatively expensive, so we don't do it with the
142 // world stopped.
143 mProf_Flush()
144 // 移动标记队列使用的缓冲区到自由列表, 使得它们可以被回收
145 // Prepare workbufs for freeing by the sweeper. We do this
146 // asynchronously because it can take non-trivial time.
147 prepareFreeWorkbufs()
148 // 释放未使用的栈
149 // Free stack spans. This must be done between GC cycles.
150 systemstack(freeStackSpans)
151 // 除错用
152 // Print gctrace before dropping worldsema. As soon as we drop
153 // worldsema another cycle could start and smash the stats
154 // we're trying to print.
155 if debug.gctrace > 0 {
156 util := int(memstats.gc_cpu_fraction * 100)
157 var sbuf [24]byte
158 printlock()
159 print("gc ", memstats.numgc,
160 " @", string(itoaDiv(sbuf[:], uint64(work.tSweepTerm-runtimeInitTime)/1e6, 3)), "s ",
161 util, "%: ")
162 prev := work.tSweepTerm
163 for i, ns := range []int64{work.tMark, work.tMarkTerm, work.tEnd} {
164 if i != 0 {
165 print("+")
166 }
167 print(string(fmtNSAsMS(sbuf[:], uint64(ns-prev))))
168 prev = ns
169 }
170 print(" ms clock, ")
171 for i, ns := range []int64{sweepTermCpu, gcController.assistTime, gcController.dedicatedMarkTime + gcController.fractionalMarkTime, gcController.idleMarkTime, markTermCpu} {
172 if i == 2 || i == 3 {
173 // Separate mark time components with /.
174 print("/")
175 } else if i != 0 {
176 print("+")
177 }
178 print(string(fmtNSAsMS(sbuf[:], uint64(ns))))
179 }
180 print(" ms cpu, ",
181 work.heap0>>20, "->", work.heap1>>20, "->", work.heap2>>20, " MB, ",
182 work.heapGoal>>20, " MB goal, ",
183 work.maxprocs, " P")
184 if work.userForced {
185 print(" (forced)")
186 }
187 print("\n")
188 printunlock()
189 }
190 semrelease(&worldsema)
191 // Careful: another GC cycle may start now.
192 // 重新允许当前的G被抢占
193 releasem(mp)
194 mp = nil
195 // 如果是并行GC, 让当前M继续运行(会回到gcBgMarkWorker然后休眠)
196 // 如果不是并行GC, 则让当前M开始调度
197 // now that gc is done, kick off finalizer thread if needed
198 if !concurrentSweep {
199 // give the queued finalizers, if any, a chance to run
200 Gosched()
201 }
202}
gcSweep函数会唤醒后台清扫任务:
后台清扫任务会在程序启动时调用的gcenable函数中启动.
1func gcSweep(mode gcMode) {
2 if gcphase != _GCoff {
3 throw("gcSweep being done but phase is not GCoff")
4 }
5 // 增加sweepgen, 这样sweepSpans中两个队列角色会交换, 所有span都会变为"待清扫"的span
6 lock(&mheap_.lock)
7 mheap_.sweepgen += 2
8 mheap_.sweepdone = 0
9 if mheap_.sweepSpans[mheap_.sweepgen/2%2].index != 0 {
10 // We should have drained this list during the last
11 // sweep phase. We certainly need to start this phase
12 // with an empty swept list.
13 throw("non-empty swept list")
14 }
15 mheap_.pagesSwept = 0
16 unlock(&mheap_.lock)
17 // 如果非并行GC则在这里完成所有工作(STW中)
18 if !_ConcurrentSweep || mode == gcForceBlockMode {
19 // Special case synchronous sweep.
20 // Record that no proportional sweeping has to happen.
21 lock(&mheap_.lock)
22 mheap_.sweepPagesPerByte = 0
23 unlock(&mheap_.lock)
24 // Sweep all spans eagerly.
25 for sweepone() != ^uintptr(0) {
26 sweep.npausesweep++
27 }
28 // Free workbufs eagerly.
29 prepareFreeWorkbufs()
30 for freeSomeWbufs(false) {
31 }
32 // All "free" events for this mark/sweep cycle have
33 // now happened, so we can make this profile cycle
34 // available immediately.
35 mProf_NextCycle()
36 mProf_Flush()
37 return
38 }
39 // 唤醒后台清扫任务
40 // Background sweep.
41 lock(&sweep.lock)
42 if sweep.parked {
43 sweep.parked = false
44 ready(sweep.g, 0, true)
45 }
46 unlock(&sweep.lock)
47}
48
后台清扫任务的函数是bgsweep:
1func bgsweep(c chan int) {
2 sweep.g = getg()
3 // 等待唤醒
4 lock(&sweep.lock)
5 sweep.parked = true
6 c <- 1
7 goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)
8 // 循环清扫
9 for {
10 // 清扫一个span, 然后进入调度(一次只做少量工作)
11 for gosweepone() != ^uintptr(0) {
12 sweep.nbgsweep++
13 Gosched()
14 }
15 // 释放一些未使用的标记队列缓冲区到heap
16 for freeSomeWbufs(true) {
17 Gosched()
18 }
19 // 如果清扫未完成则继续循环
20 lock(&sweep.lock)
21 if !gosweepdone() {
22 // This can happen if a GC runs between
23 // gosweepone returning ^0 above
24 // and the lock being acquired.
25 unlock(&sweep.lock)
26 continue
27 }
28 // 否则让后台清扫任务进入休眠, 当前M继续调度
29 sweep.parked = true
30 goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)
31 }
32}
33
gosweepone函数会从sweepSpans中取出单个span清扫:
1//go:nowritebarrier
2func gosweepone() uintptr {
3 var ret uintptr
4 // 切换到g0运行
5 systemstack(func() {
6 ret = sweepone()
7 })
8 return ret
9}
sweepone函数如下:
1// sweeps one span
2// returns number of pages returned to heap, or ^uintptr(0) if there is nothing to sweep
3//go:nowritebarrier
4func sweepone() uintptr {
5 _g_ := getg()
6 sweepRatio := mheap_.sweepPagesPerByte // For debugging
7 // 禁止G被抢占
8 // increment locks to ensure that the goroutine is not preempted
9 // in the middle of sweep thus leaving the span in an inconsistent state for next GC
10 _g_.m.locks++
11 // 检查是否已完成清扫
12 if atomic.Load(&mheap_.sweepdone) != 0 {
13 _g_.m.locks--
14 return ^uintptr(0)
15 }
16 // 更新同时执行sweep的任务数量
17 atomic.Xadd(&mheap_.sweepers, +1)
18 npages := ^uintptr(0)
19 sg := mheap_.sweepgen
20 for {
21 // 从sweepSpans中取出一个span
22 s := mheap_.sweepSpans[1-sg/2%2].pop()
23 // 全部清扫完毕时跳出循环
24 if s == nil {
25 atomic.Store(&mheap_.sweepdone, 1)
26 break
27 }
28 // 其他M已经在清扫这个span时跳过
29 if s.state != mSpanInUse {
30 // This can happen if direct sweeping already
31 // swept this span, but in that case the sweep
32 // generation should always be up-to-date.
33 if s.sweepgen != sg {
34 print("runtime: bad span s.state=", s.state, " s.sweepgen=", s.sweepgen, " sweepgen=", sg, "\n")
35 throw("non in-use span in unswept list")
36 }
37 continue
38 }
39 // 原子增加span的sweepgen, 失败表示其他M已经开始清扫这个span, 跳过
40 if s.sweepgen != sg-2 || !atomic.Cas(&s.sweepgen, sg-2, sg-1) {
41 continue
42 }
43 // 清扫这个span, 然后跳出循环
44 npages = s.npages
45 if !s.sweep(false) {
46 // Span is still in-use, so this returned no
47 // pages to the heap and the span needs to
48 // move to the swept in-use list.
49 npages = 0
50 }
51 break
52 }
53 // 更新同时执行sweep的任务数量
54 // Decrement the number of active sweepers and if this is the
55 // last one print trace information.
56 if atomic.Xadd(&mheap_.sweepers, -1) == 0 && atomic.Load(&mheap_.sweepdone) != 0 {
57 if debug.gcpacertrace > 0 {
58 print("pacer: sweep done at heap size ", memstats.heap_live>>20, "MB; allocated ", (memstats.heap_live-mheap_.sweepHeapLiveBasis)>>20, "MB during sweep; swept ", mheap_.pagesSwept, " pages at ", sweepRatio, " pages/byte\n")
59 }
60 }
61 // 允许G被抢占
62 _g_.m.locks--
63 // 返回清扫的页数
64 return npages
65}
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。
Golang语言社区
ID:GolangWeb
www.ByteEdu.Com
游戏服务器架构丨分布式技术丨大数据丨游戏算法学习