内存屏障 WriteBarrier 垃圾回收 屏障技术
https://baike.baidu.com/item/内存屏障
- 中文名
- 内存屏障
- 别 称
- 内存栅栏,内存栅障
- 性 质
- 同步屏障指令
- 条 件
- 现代计算机为了提高性能
-
完全内存屏障(full memory barrier)保障了早于屏障的内存读写操作的结果提交到内存之后,再执行晚于屏障的读写操作。
-
内存读屏障(read memory barrier)仅确保了内存读操作;
-
内存写屏障(write memory barrier)仅保证了内存写操作。
1
|
lfence (asm), void _mm_lfence ( void ) 读操作屏障sfence (asm), void _mm_sfence ( void )[ 1 ] 写操作屏障mfence (asm), void _mm_mfence ( void )[ 2 ] 读写操作屏障 |
1
|
addl $ 0 , 0 (%esp) |
-
acquire semantics: 该操作结果可利用要早于代码中后续的所有操作的结果。
-
release semantics: 该操作结果可利用要晚于代码中之前的所有操作的结果。
-
fence semantics: acquire与release两种语义的共同有效。即该操作结果可利用要晚于代码中之前的所有操作的结果,且该操作结果可利用要早于代码中后续的所有操作的结果。
-
acq (acquire)
-
rel (release).
-
进出临界区(critical section)的函数
-
触发(signaled)同步对象的函数
-
等待函数(Wait function)
1
|
asm volatile ( "" ::: "memory" ); |
1
|
_ReadWriteBarrier() MemoryBarrier() |
1
|
__memory_barrier() |
- 参考资料
_ReadWriteBarrier | Microsoft Docs https://docs.microsoft.com/en-us/cpp/intrinsics/readwritebarrier?view=msvc-160
Limits the compiler optimizations that can reorder memory accesses across the point of the call.
Caution
The _ReadBarrier
, _WriteBarrier
, and _ReadWriteBarrier
compiler intrinsics and the MemoryBarrier
macro are all deprecated and should not be used. For inter-thread communication, use mechanisms such as atomic_thread_fence and std::atomic<T>, which are defined in the C++ Standard Library. For hardware access, use the /volatile:iso compiler option together with the volatile keyword.
链接:https://www.zhihu.com/question/62000722/answer/1424800807
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
首先要知道怎么标记垃圾(引用计数,根可达两种标记),再就是常用的垃圾回收算法(golang使用三色标记法,jvm使用分代回收法),然后关于写屏障有两种写屏障及过程(插入写屏障,删除写屏障),写屏障解决的问题(三色标记法错标或漏标问题)
1.垃圾定位算法
(1)引用计数法
通常C++通过指针引用计数来回收对象,但是这不能处理循环引用,原理是在每个对象内部维护一个引用计数,当对象被引用时引用计数加一,当对象不被引用时引用计数减一。当引用计数为 0 时,自动销毁对象。
(2)根可达算法
从GC Roots向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的,向JAVA、Go这种带有GC功能的高级语言使用的都是这种定位算法
简单来讲,从根对象往下查找引用,可以查找到的引用标记成可达,直到算法结束之后,没有被标记的对象就是不可达的,就会被GC回收。
2.垃圾回收算法
(1)标记-清除
(2)复制
(3)标记-压缩
以上三种算法是传统的垃圾回收算法,第一种容易产生内存碎片,第二种不会生成内存碎片,但是由于是整块复制,所以STW较长,效率太低,第三种是前两种的结合
(4)分代模型
JVM做垃圾回收时常用的GC算法,分为年轻代和老年代,年轻代使用复制算法,老年代使用标记压缩或者标记清除。
在分代模型中,年轻代的回收算法有ParNew、Serial、Parallel Scavenge,老年代的回收算法有CMS、Serial Old、Parallel Old,年轻代和老年代的回收算法一定是成对出现的,常见的回收对是ParNew-CMS、Serial-Serial Old、Parallel Scavenge-Parallel Old(jdk1.8默认)
另外jdk1.8可以用上面的分代模型,也可以使用不分代模型,即G1、ZGC等
(5)三色标记法
三色标记法是传统 Mark-Sweep 的一个改进,它是一个并发的 GC 算法。其实大部分的工作还是在标记垃圾,基本原理基于根可达
golang使用三色标记法来标记垃圾并回收
步骤:
a. 首先初始状态下所有对象都是白色的
b.从根对象开始遍历所有对象,将遍历到的对象从白色集合方放到灰色集合
c.遍历灰色集合中的对象将灰色对象引用的对象从白色集合放到灰色集合里面,此灰色对象放进黑色集合
d.重复c直到灰色集合为空
e.通过写屏障检测对象发生变化,重复上面操作
f.收集所有白色对象(垃圾)
3.三色标记算法标记垃圾会产生的问题
A对象已经标记并且引用的对象B也已经被标记,所以A放到黑色集合里,B对象被标记但是C对象还没标记,所以B是灰色
(1)浮动垃圾
如果B到C的引用断开,那么B找不到引用会被标黑,此时C就成了浮动垃圾,这种情况不碍事,大不了下次GC再收集
(2)漏标或者错标或者称作悬挂指针
但是如果此时用户goroutine新建对象A对对象C的引用,也就是从已经被标记成黑色的对象新建了引用指向了白色对象,因为A已经标黑,此时C将作为白色不可达对象被收集,这就出大问题了,程序里面A对象还正在引用C对象,但是GC把C对象看成垃圾给回收了,造成空指针异常。
4.写屏障
为了解决漏标的问题,需要使用写屏障,原理就是当A对象被标黑,此时A又引用C,就把C变灰入队
写屏障一定是在进行内存写操作之前执行的。
- 强三色不变性 — 黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象;
- 弱三色不变性 — 黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径
Go 语言中使用两种写屏障技术,分别是 Dijkstra 提出的插入写屏障和 Yuasa 提出的删除写屏障。
(1)插入写屏障
# 伪代码
writePointer(slot, ptr):
shade(ptr)
*slot = ptr
上述插入写屏障的伪代码非常好理解,每当我们执行类似 *slot = ptr 的表达式时,我们会执行上述写屏障通过 shade 函数尝试改变指针的颜色。如果 ptr 指针是白色的,那么该函数会将该对象设置成灰色,其他情况则保持不变.
- 垃圾收集器将根对象指向 A 对象标记成黑色并将 A 对象指向的对象 B 标记成灰色;
- 用户程序修改 A 对象的指针,将原本指向 B 对象的指针指向 C 对象,这时触发写屏障将 C 对象标记成灰色;
- 垃圾收集器依次遍历程序中的其他灰色对象,将它们分别标记成黑色;
说人话,就是如果两个对象之间新建立引用,那么引用指向的对象就会被标记为灰色以满足强三色不变性,这是一种相对保守的屏障技术。
插入写屏障的缺点:
因为栈上的对象在垃圾收集中也会被认为是根对象,所以为了保证内存的安全,Dijkstra 必须为栈上的对象增加写屏障或者在标记阶段完成重新对栈上的对象进行扫描,这两种方法各有各的缺点,前者会大幅度增加写入指针的额外开销,后者重新扫描栈对象时需要暂停程序。
(2)删除写屏障
# 伪代码
writePointer(slot, ptr)
shade(*slot)
*slot = ptr
上述代码会在老对象的引用被删除时,将白色的老对象涂成灰色,这样删除写屏障就可以保证弱三色不变性,老对象引用的下游对象一定可以被灰色对象引用。
- 垃圾收集器将根对象指向 A 对象标记成黑色并将 A 对象指向的对象 B 标记成灰色;
- 用户程序将 A 对象原本指向 B 的指针指向 C,触发删除写屏障,但是因为 B 对象已经是灰色的,所以不做改变;
- 用户程序将 B 对象原本指向 C 的指针删除,触发删除写屏障,白色的 C 对象被涂成灰色;
- 垃圾收集器依次遍历程序中的其他灰色对象,将它们分别标记成黑色;
说人话,如果一个灰色对象指向一个白色对象的引用被删除,那么在删除之前写屏障检测到内存变化,就会把这个白色对象标灰。
总结
Go的垃圾回收官方形容为 非分代 非紧缩 写屏障 并发标记清理。
非分代是golang GC区别于JVM GC分代模型的特点;非紧缩意味着在回收垃圾的过程中,不需要像复制算法那样移动内存中的对象,这样避免STW过长;标记清理算法的字面解释,就是将可达的内存块进行标记mark,最后没有标记的不可达内存块将进行清理sweep;Golang中实现标记功能的算法就是三色标记法,Golang里面三色标记法会造成错标问题,使用写屏障来解决这种问题,而JVM里面的CMS和G1解决错标或者漏标问题的算法分别是Increment Update和SATB
垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的对象,让出存储器资源,无需程序员手动执行。
Golang中的垃圾回收主要应用三色标记法,GC过程和其他用户goroutine可并发运行,但需要一定时间的STW(stop the world),STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,Golang进行了多次的迭代优化来解决这个问题。
- 1.3以前的版本使用标记-清扫的方式,整个过程都需要STW。
- 1.3版本分离了标记和清扫的操作,标记过程STW,清扫过程并发执行。
- 1.5版本在标记过程中使用三色标记法。回收过程主要有四个阶段,其中,标记和清扫都并发执行的,但标记阶段的前后需要STW一定时间来做GC的准备工作和栈的re-scan。
- Sweep Termination: 收集根对象,清扫上一轮未清扫完的span,启用写屏障和辅助GC,辅助GC是将一定量的标记和清扫工作交给用户goroutine来执行,写屏障在后面会详细说明。
- Mark: 扫描所有根对象和根对象可以到达的对象,并标记它们
- Mark Termination: 完成标记工作,重新扫描部分根对象(要求STW),关闭写屏障和辅助GC
- Sweep: 按标记结果清扫对象
- 1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。
标记-清扫算法
标记-清扫算法是一种追踪式的垃圾回收算法,并不会在对象死亡后立即将其清除掉,而是在一定条件下触发,统一校验系统中的存活对象,进行回收工作。在Golang中,有以下几种情况可触发:
- gcTriggerAlways: 强制触发GC
- gcTriggerHeap: 当前分配的内存达到一定阈值时触发,这个阈值在每次GC过后都会根据堆内存的增长情况和CPU占用率来调整
- gcTriggerTime: 当一定时间没有执行过GC就触发GC(2分钟)
- gcTriggerCycle: runtime.GC()调用
标记-清扫分为两个部分,标记和清扫,标记过程会遍历所有对象,查找出死亡对象。我们可以使用指针的可达性可确认对象的存活,也就是说,如果存在一条从根出发的指针链最终可指向某个对象,就认为这个对象是存活的。这样,未能证明存活的对象就可以标记为死亡了。
根:是一个有限的指针集合,程序可不经过其他对象直接访问这些指针,堆中的对象被加载时,需要先加载根中的指针。在Go中,一般为goroutine自己的栈空间和全局栈空间。
标记结束后,再次进行遍历,清除掉确认死亡的对象。
三色标记法
将标记-清扫法使用三色抽象的方式来描述,可以方便我们理解回收器的正确性。
回收器通过将对象图划分为三种状态来指示其扫描过程。黑色对象为该对象及其后代已处理且该对象确认存活的,灰色对象为已经扫描到但未处理完成或者还需要再次处理的,白色对象为尚未扫描到或已经死亡的。
如图所示,回收器从根出发,扫描到可达对象后标记其为灰色,放入灰色队列,在扫描灰色对象引用的对象,将他们标记为灰色,自身标记为黑色。扫描全部结束后,剩余未被扫描到的对象留在白色队列中,表示已死亡。
标记过程需的要STW,因为对象引用关系如果在标记阶段做了修改,会影响标记结果的正确性。例如下图,灰色对象B中包含指向白色对象C的指针e,对象C尚未被扫描,此时,如有其他程序,将e指针从B对象中删除,并将指向对象C的新指针f插入到黑色对象A中,由于对象A早已完成扫描,对象C就会一直保持白色状态直到被回收。
可以看出,一个白色对象被黑色对象引用,是注定无法通过这个黑色对象来保证自身存活的,与此同时,如果所有能到达它的灰色对象与它之间的可达关系全部遭到破坏,那么这个白色对象必然会被视为垃圾清除掉。 故当上述两个条件同时满足时,就会出现对象丢失的问题。
如果这个白色对象下游还引用了其他对象,并且这条路径是指向下游对象的唯一路径,那么他们也是必死无疑的。
为了防止这种现象的发生,最简单的方式就是STW,直接禁止掉其他用户程序对对象引用关系的干扰,但是STW的过程有明显的资源浪费,对所有的用户程序都有很大影响,如何能在保证对象不丢失的情况下合理的尽可能的提高GC效率,减少STW时间呢?
在Golang中使用并发的垃圾回收,也就是多个赋值器与回收器并发执行,与此同时,应用屏障技术来保证回收器的正确性。其原理主要就是破坏上述两个条件之一。
屏障技术
当回收器满足下面两种情况之一时,即可保证不会出现对象丢失问题。
弱三色不变式:所有被黑色对象引用的白色对象都处于灰色保护状态(直接或间接从灰色对象可达)。 强三色不变式:不存在黑色对象到白色对象的指针。
强三色不变式很好理解,强制性的不允许黑色对象引用白色对象即可。而弱三色不变式中,黑色对象可以引用白色对象,但是这个白色对象仍然存在其他灰色对象对它的引用,或者可达它的链路上游存在灰色对象。
三色抽象除了可以用于描述对象的状态的,还可用来描述赋值器的状态,如果一个赋值器已经被回收器扫描完成,则认为它是黑色的赋值器,如果尚未扫描过或者还需要重新扫描,则认为它是灰色的赋值器。
在强三色不变式中,黑色赋值器只存在到黑色对象或灰色对象的指针,因为此时所有黑色对象到白色对象的引用都是被禁止的。 在弱三色不变式中,黑色赋值器允许存在到白色对象的指针,但这个白色对象是被保护的。
上述这些可以通过屏障技术来保证。
插入屏障
插入屏障拦截将白色指针插入黑色对象的操作,标记其对应对象为灰色状态,这样就不存在黑色对象引用白色对象的情况了,满足强三色不变式,如上图例中,在插入指针f时将C对象标记为灰色。Go1.5版本使用的Dijkstra写屏障就是这个原理,伪代码如下:
writePointer(slot, ptr):
shade(ptr)
*slot = ptr
在Golang中,对栈上指针的写入添加写屏障的成本很高,所以Go选择仅对堆上的指针插入增加写屏障,这样就会出现在扫描结束后,栈上仍存在引用白色对象的情况,这时的栈是灰色的,不满足三色不变式,所以需要对栈进行重新扫描使其变黑,完成剩余对象的标记,这个过程需要STW。这期间会将所有goroutine挂起,当有大量应用程序时,时间可能会达到10~100ms。
删除屏障
删除屏障也是拦截写操作的,但是是通过保护灰色对象到白色对象的路径不会断来实现的。如上图例中,在删除指针e时将对象C标记为灰色,这样C下游的所有白色对象,即使会被黑色对象引用,最终也还是会被扫描标记的,满足了弱三色不变式。这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。Yuasa屏障伪代码如下:
writePointer(slot, ptr):
if (isGery(slot) || isWhite(slot))
shade(*slot)
*slot = ptr
在这种实现方式中,回收器悲观的认为所有被删除的对象都可能会被黑色对象引用。
混合写屏障
插入屏障和删除屏障各有优缺点,Dijkstra的插入写屏障在标记开始时无需STW,可直接开始,并发进行,但结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;Yuasa的删除写屏障则需要在GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象,但结束时无需STW。Go1.8版本引入的混合写屏障结合了Yuasa的删除写屏障和Dijkstra的插入写屏障,结合了两者的优点,伪代码如下:
writePointer(slot, ptr):
shade(*slot)
if current stack is grey:
shade(ptr)
*slot = ptr
这里使用了两个shade操作,shade(*slot)是删除写屏障的变形,例如,一个堆上的灰色对象B,引用白色对象C,在GC并发运行的过程中,如果栈已扫描置黑,而赋值器将指向C的唯一指针从B中删除,并让栈上其他对象引用它,这时,写屏障会在删除指向白色对象C的指针的时候就将C对象置灰,就可以保护下来了,且它下游的所有对象都处于被保护状态。 如果对象B在栈上,引用堆上的白色对象C,将其引用关系删除,且新增一个黑色对象到对象C的引用,那么就需要通过shade(ptr)来保护了,在指针插入黑色对象时会触发对对象C的置灰操作。如果栈已经被扫描过了,那么栈上引用的对象都是灰色或受灰色保护的白色对象了,所以就没有必要再进行这步操作。
Golang中的混合写屏障满足的是变形的弱三色不变式,同样允许黑色对象引用白色对象,白色对象处于灰色保护状态,但是只由堆上的灰色对象保护。由于结合了Yuasa的删除写屏障和Dijkstra的插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。
Write barrier
In operating systems, write barrier is a mechanism for enforcing a particular ordering in a sequence of writes to a storage system in a computer system. For example, a write barrier in a file system is a mechanism (program logic) that ensures that in-memory file system state is written out to persistent storage in the correct order.[1][2][3]
In Garbage collection[edit]
A write barrier in a garbage collector is a fragment of code emitted by the compiler immediately before every store operation to ensure that (e.g.) generational invariants are maintained. A write barrier in a memory system, also known as a memory barrier, is a hardware-specific compiler intrinsic that ensures that all preceding memory operations "happen before" all subsequent ones.[4]
In Computer storage[edit]
This section needs expansion. You can help by adding to it.(July 2016)
|
See also[edit]
References[edit]
- ^ "Chapter 16. Write Barriers". docs.fedoraproject.org. Retrieved 2014-01-24.
- ^ Tejun Heo (2005-07-22). "I/O Barriers". kernel/git/torvalds/linux.git - Linux kernel source tree. git.kernel.org. Retrieved 2014-01-24.
- ^ Jonathan Corbet (2010-08-18). "The end of block barriers". LWN.net. Retrieved 2014-01-24.
- ^ "GC FAQ -- algorithms". www.iecc.com. Retrieved 2020-06-30.
// Garbage collector (GC).
6 //
7 // The GC runs concurrently with mutator threads, is type accurate (aka precise), allows multiple
8 // GC thread to run in parallel. It is a concurrent mark and sweep that uses a write barrier. It is
9 // non-generational and non-compacting. Allocation is done using size segregated per P allocation
10 // areas to minimize fragmentation while eliminating locks in the common case.
11 //
12 // The algorithm decomposes into several steps.
13 // This is a high level description of the algorithm being used. For an overview of GC a good
14 // place to start is Richard Jones' gchandbook.org.
15 //
16 // The algorithm's intellectual heritage includes Dijkstra's on-the-fly algorithm, see
17 // Edsger W. Dijkstra, Leslie Lamport, A. J. Martin, C. S. Scholten, and E. F. M. Steffens. 1978.
18 // On-the-fly garbage collection: an exercise in cooperation. Commun. ACM 21, 11 (November 1978),
19 // 966-975.
20 // For journal quality proofs that these steps are complete, correct, and terminate see
21 // Hudson, R., and Moss, J.E.B. Copying Garbage Collection without stopping the world.
22 // Concurrency and Computation: Practice and Experience 15(3-5), 2003.
23 //
24 // 1. GC performs sweep termination.
25 //
26 // a. Stop the world. This causes all Ps to reach a GC safe-point.
27 //
28 // b. Sweep any unswept spans. There will only be unswept spans if
29 // this GC cycle was forced before the expected time.
30 //
31 // 2. GC performs the mark phase.
32 //
33 // a. Prepare for the mark phase by setting gcphase to _GCmark
34 // (from _GCoff), enabling the write barrier, enabling mutator
35 // assists, and enqueueing root mark jobs. No objects may be
36 // scanned until all Ps have enabled the write barrier, which is
37 // accomplished using STW.
38 //
39 // b. Start the world. From this point, GC work is done by mark
40 // workers started by the scheduler and by assists performed as
41 // part of allocation. The write barrier shades both the
42 // overwritten pointer and the new pointer value for any pointer
43 // writes (see mbarrier.go for details). Newly allocated objects
44 // are immediately marked black.
45 //
46 // c. GC performs root marking jobs. This includes scanning all
47 // stacks, shading all globals, and shading any heap pointers in
48 // off-heap runtime data structures. Scanning a stack stops a
49 // goroutine, shades any pointers found on its stack, and then
50 // resumes the goroutine.
51 //
52 // d. GC drains the work queue of grey objects, scanning each grey
53 // object to black and shading all pointers found in the object
54 // (which in turn may add those pointers to the work queue).
55 //
56 // e. Because GC work is spread across local caches, GC uses a
57 // distributed termination algorithm to detect when there are no
58 // more root marking jobs or grey objects (see gcMarkDone). At this
59 // point, GC transitions to mark termination.
60 //
61 // 3. GC performs mark termination.
62 //
63 // a. Stop the world.
64 //
65 // b. Set gcphase to _GCmarktermination, and disable workers and
66 // assists.
67 //
68 // c. Perform housekeeping like flushing mcaches.
69 //
70 // 4. GC performs the sweep phase.
71 //
72 // a. Prepare for the sweep phase by setting gcphase to _GCoff,
73 // setting up sweep state and disabling the write barrier.
74 //
75 // b. Start the world. From this point on, newly allocated objects
76 // are white, and allocating sweeps spans before use if necessary.
77 //
78 // c. GC does concurrent sweeping in the background and in response
79 // to allocation. See description below.
80 //
81 // 5. When sufficient allocation has taken place, replay the sequence
82 // starting with 1 above. See discussion of GC rate below.