日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Go GC 20 问

發布時間:2024/4/11 编程问答 50 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Go GC 20 问 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文作者歐長坤,德國慕尼黑大學在讀博士,Go/etcd/Tensorflow contributor,開源書籍《Go 語言原本》作者,《Go 夜讀》SIG 成員/講師,對 Go 有很深的研究。Github:@changkun,https://changkun.de。

本文首發于 Github 開源項目 《Go-Questions》,點擊閱讀原文直達。全文不計代碼,共 1.7w+ 字,建議收藏后精讀。另外,本文結尾有彩蛋。

按慣例,貼上本文的目錄:

本文寫于 Go 1.14 beta1,當文中提及目前、目前版本等字眼時均指 Go 1.14,此外,文中所有 go 命令版本均為 Go 1.14。

GC 的認識

1. 什么是 GC,有什么作用?

GC,全稱 GarbageCollection,即垃圾回收,是一種自動內存管理的機制。

當程序向操作系統申請的內存不再需要時,垃圾回收主動將其回收并供其他代碼進行內存申請時候復用,或者將其歸還給操作系統,這種針對內存級別資源的自動回收過程,即為垃圾回收。而負責垃圾回收的程序組件,即為垃圾回收器。

垃圾回收其實一個完美的 “Simplicity is Complicated” 的例子。一方面,程序員受益于 GC,無需操心、也不再需要對內存進行手動的申請和釋放操作,GC 在程序運行時自動釋放殘留的內存。另一方面,GC 對程序員幾乎不可見,僅在程序需要進行特殊優化時,通過提供可調控的 API,對 GC 的運行時機、運行開銷進行把控的時候才得以現身。

通常,垃圾回收器的執行過程被劃分為兩個半獨立的組件:

  • 賦值器(Mutator):這一名稱本質上是在指代用戶態的代碼。因為對垃圾回收器而言,用戶態的代碼僅僅只是在修改對象之間的引用關系,也就是在對象圖(對象之間引用關系的一個有向圖)上進行操作。

  • 回收器(Collector):負責執行垃圾回收的代碼。

2. 根對象到底是什么?

根對象在垃圾回收的術語中又叫做根集合,它是垃圾回收器在標記過程時最先檢查的對象,包括:

  • 全局變量:程序在編譯期就能確定的那些存在于程序整個生命周期的變量。

  • 執行棧:每個 goroutine 都包含自己的執行棧,這些執行棧上包含棧上的變量及指向分配的堆內存區塊的指針。

  • 寄存器:寄存器的值可能表示一個指針,參與計算的這些指針可能指向某些賦值器分配的堆內存區塊。

  • 3. 常見的 GC 實現方式有哪些?Go 語言的 GC 使用的是什么?

    所有的 GC 算法其存在形式可以歸結為追蹤(Tracing)和引用計數(Reference Counting)這兩種形式的混合運用。

    • 追蹤式 GC

    從根對象出發,根據對象之間的引用信息,一步步推進直到掃描完畢整個堆并確定需要保留的對象,從而回收所有可回收的對象。Go、 Java、V8 對 JavaScript 的實現等均為追蹤式 GC。

    • 引用計數式 GC

    每個對象自身包含一個被引用的計數器,當計數器歸零時自動得到回收。因為此方法缺陷較多,在追求高性能時通常不被應用。Python、Objective-C 等均為引用計數式 GC。

    目前比較常見的 GC 實現方式包括:

    • 追蹤式,分為多種不同類型,例如:

    • 標記清掃:從根對象出發,將確定存活的對象進行標記,并清掃可以回收的對象。

    • 標記整理:為了解決內存碎片問題而提出,在標記過程中,將對象盡可能整理到一塊連續的內存上。

    • 增量式:將標記與清掃的過程分批執行,每次執行很小的部分,從而增量的推進垃圾回收,達到近似實時、幾乎無停頓的目的。

    • 增量整理:在增量式的基礎上,增加對對象的整理過程。

    • 分代式:將對象根據存活時間的長短進行分類,存活時間小于某個值的為年輕代,存活時間大于某個值的為老年代,永遠不會參與回收的對象為永久代。并根據分代假設(如果一個對象存活時間不長則傾向于被回收,如果一個對象已經存活很長時間則傾向于存活更長時間)對對象進行回收。

    • 引用計數:根據對象自身的引用計數來回收,當引用計數歸零時立即回收。

    關于各類方法的詳細介紹及其實現不在本文中詳細討論。對于 Go 而言,Go 的 GC 目前使用的是無分代(對象沒有代際之分)、不整理(回收過程中不對對象進行移動與整理)、并發(與用戶代碼并發執行)的三色標記清掃算法。原因在于:

  • 對象整理的優勢是解決內存碎片問題以及“允許”使用順序內存分配器。但 Go 運行時的分配算法基于 tcmalloc,基本上沒有碎片問題。并且順序內存分配器在多線程的場景下并不適用。Go 使用的是基于 tcmalloc 的現代內存分配算法,對對象進行整理不會帶來實質性的性能提升。

  • 分代 GC 依賴分代假設,即 GC 將主要的回收目標放在新創建的對象上(存活時間短,更傾向于被回收),而非頻繁檢查所有對象。但 Go 的編譯器會通過逃逸分析將大部分新生對象存儲在棧上(棧直接被回收),只有那些需要長期存在的對象才會被分配到需要進行垃圾回收的堆中。也就是說,分代 GC 回收的那些存活時間短的對象在 Go 中是直接被分配到棧上,當 goroutine 死亡后棧也會被直接回收,不需要 GC 的參與,進而分代假設并沒有帶來直接優勢。并且 Go 的垃圾回收器與用戶代碼并發執行,使得 STW 的時間與對象的代際、對象的 size 沒有關系。Go 團隊更關注于如何更好地讓 GC 與用戶代碼并發執行(使用適當的 CPU 來執行垃圾回收),而非減少停頓時間這一單一目標上。

  • 4. 三色標記法是什么?

    理解三色標記法的關鍵是理解對象的三色抽象以及波面(wavefront)推進這兩個概念。三色抽象只是一種描述追蹤式回收器的方法,在實踐中并沒有實際含義,它的重要作用在于從邏輯上嚴密推導標記清理這種垃圾回收方法的正確性。也就是說,當我們談及三色標記法時,通常指標記清掃的垃圾回收。

    從垃圾回收器的視角來看,三色抽象規定了三種不同類型的對象,并用不同的顏色相稱:

    • 白色對象(可能死亡):未被回收器訪問到的對象。在回收開始階段,所有對象均為白色,當回收結束后,白色對象均不可達。

    • 灰色對象(波面):已被回收器訪問到的對象,但回收器需要對其中的一個或多個指針進行掃描,因為他們可能還指向白色對象。

    • 黑色對象(確定存活):已被回收器訪問到的對象,其中所有字段都已被掃描,黑色對象中任何一個指針都不可能直接指向白色對象。

    這樣三種不變性所定義的回收過程其實是一個波面不斷前進的過程,這個波面同時也是黑色對象和白色對象的邊界,灰色對象就是這個波面。

    當垃圾回收開始時,只有白色對象。隨著標記過程開始進行時,灰色對象開始出現(著色),這時候波面便開始擴大。當一個對象的所有子節點均完成掃描時,會被著色為黑色。當整個堆遍歷完成時,只剩下黑色和白色對象,這時的黑色對象為可達對象,即存活;而白色對象為不可達對象,即死亡。這個過程可以視為以灰色對象為波面,將黑色對象和白色對象分離,使波面不斷向前推進,直到所有可達的灰色對象都變為黑色對象為止的過程。如下圖所示:

    圖中展示了根對象、可達對象、不可達對象,黑、灰、白對象以及波面之間的關系。

    5. STW 是什么意思?

    STW 是 StoptheWorld 的縮寫,即萬物靜止,是指在垃圾回收過程中為了保證實現的正確性、防止無止境的內存增長等問題而不可避免的需要停止賦值器進一步操作對象圖的一段過程。

    在這個過程中整個用戶代碼被停止或者放緩執行, STW 越長,對用戶代碼造成的影響(例如延遲)就越大,早期 Go 對垃圾回收器的實現中 STW 長達幾百毫秒,對時間敏感的實時通信等應用程序會造成巨大的影響。我們來看一個例子:

    package mainimport ("runtime""time" )func main() {go func() {for {}}()time.Sleep(time.Millisecond)runtime.GC()println("OK") }

    上面的這個程序在 Go 1.14 以前永遠都不會輸出 OK,其罪魁禍首是 STW 無限制的被延長。

    盡管 STW 如今已經優化到了半毫秒級別以下,但這個程序被卡死原因在于仍然是 STW 導致的。原因在于,GC 在進入 STW 時,需要等待讓所有的用戶態代碼停止,但是 for{} 所在的 goroutine 永遠都不會被中斷,從而停留在 STW 階段。實際實踐中也是如此,當程序的某個 goroutine 長時間得不到停止,強行拖慢 STW,這種情況下造成的影響(卡死)是非常可怕的。好在自 Go 1.14 之后,這類 goroutine 能夠被異步地搶占,從而使得 STW 的時間如同普通程序那樣,不會超過半個毫秒,程序也不會因為僅僅等待一個 goroutine 的停止而停頓在 STW 階段。

    6. 如何觀察 Go GC?

    我們以下面的程序為例,先使用四種不同的方式來介紹如何觀察 GC,并在后面的問題中通過幾個詳細的例子再來討論如何優化 GC。

    package mainfunc allocate() {_ = make([]byte, 1<<20) }func main() {for n := 1; n < 100000; n++ {allocate()} }

    方式1:?GODEBUG=gctrace=1

    我們首先可以通過:

    $ go build -o main $ GODEBUG=gctrace=1 ./maingc 1 @0.000s 2%: 0.009+0.23+0.004 ms clock, 0.11+0.083/0.019/0.14+0.049 ms cpu, 4->6->2 MB, 5 MB goal, 12 P scvg: 8 KB released scvg: inuse: 3, idle: 60, sys: 63, released: 57, consumed: 6 (MB) gc 2 @0.001s 2%: 0.018+1.1+0.029 ms clock, 0.22+0.047/0.074/0.048+0.34 ms cpu, 4->7->3 MB, 5 MB goal, 12 P scvg: inuse: 3, idle: 60, sys: 63, released: 56, consumed: 7 (MB) gc 3 @0.003s 2%: 0.018+0.59+0.011 ms clock, 0.22+0.073/0.008/0.042+0.13 ms cpu, 5->6->1 MB, 6 MB goal, 12 P scvg: 8 KB released scvg: inuse: 2, idle: 61, sys: 63, released: 56, consumed: 7 (MB) gc 4 @0.003s 4%: 0.019+0.70+0.054 ms clock, 0.23+0.051/0.047/0.085+0.65 ms cpu, 4->6->2 MB, 5 MB goal, 12 P scvg: 8 KB released scvg: inuse: 3, idle: 60, sys: 63, released: 56, consumed: 7 (MB) scvg: 8 KB released scvg: inuse: 4, idle: 59, sys: 63, released: 56, consumed: 7 (MB) gc 5 @0.004s 12%: 0.021+0.26+0.49 ms clock, 0.26+0.046/0.037/0.11+5.8 ms cpu, 4->7->3 MB, 5 MB goal, 12 P scvg: inuse: 5, idle: 58, sys: 63, released: 56, consumed: 7 (MB) gc 6 @0.005s 12%: 0.020+0.17+0.004 ms clock, 0.25+0.080/0.070/0.053+0.051 ms cpu, 5->6->1 MB, 6 MB goal, 12 P scvg: 8 KB released scvg: inuse: 1, idle: 62, sys: 63, released: 56, consumed: 7 (MB)

    在這個日志中可以觀察到兩類不同的信息:

    gc 1 @0.000s 2%: 0.009+0.23+0.004 ms clock, 0.11+0.083/0.019/0.14+0.049 ms cpu, 4->6->2 MB, 5 MB goal, 12 P gc 2 @0.001s 2%: 0.018+1.1+0.029 ms clock, 0.22+0.047/0.074/0.048+0.34 ms cpu, 4->7->3 MB, 5 MB goal, 12 P ...

    以及:

    scvg: 8 KB released scvg: inuse: 3, idle: 60, sys: 63, released: 57, consumed: 6 (MB) scvg: inuse: 3, idle: 60, sys: 63, released: 56, consumed: 7 (MB) ...

    對于用戶代碼向運行時申請內存產生的垃圾回收:

    gc 2 @0.001s 2%: 0.018+1.1+0.029 ms clock, 0.22+0.047/0.074/0.048+0.34 ms cpu, 4->7->3 MB, 5 MB goal, 12 P

    含義由下表所示:

    字段含義
    gc 2第二個 GC 周期
    0.001程序開始后的 0.001 秒
    2%該 GC 周期中 CPU 的使用率
    0.018標記開始時, STW 所花費的時間(wall clock)
    1.1標記過程中,并發標記所花費的時間(wall clock)
    0.029標記終止時, STW 所花費的時間(wall clock)
    0.22標記開始時, STW 所花費的時間(cpu time)
    0.047標記過程中,標記輔助所花費的時間(cpu time)
    0.074標記過程中,并發標記所花費的時間(cpu time)
    0.048標記過程中,GC 空閑的時間(cpu time)
    0.34標記終止時, STW 所花費的時間(cpu time)
    4標記開始時,堆的大小的實際值
    7標記結束時,堆的大小的實際值
    3標記結束時,標記為存活的對象大小
    5標記結束時,堆的大小的預測值
    12P 的數量

    wall clock 是指開始執行到完成所經歷的實際時間,包括其他程序和本程序所消耗的時間;cpu time 是指特定程序使用 CPU 的時間;他們存在以下關系:

    • wall clock < cpu time: 充分利用多核

    • wall clock ≈ cpu time: 未并行執行

    • wall clock > cpu time: 多核優勢不明顯

    對于運行時向操作系統申請內存產生的垃圾回收(向操作系統歸還多余的內存):

    scvg: 8 KB released scvg: inuse: 3, idle: 60, sys: 63, released: 57, consumed: 6 (MB)

    含義由下表所示:

    字段含義
    8 KB released向操作系統歸還了 8 KB 內存
    3已經分配給用戶代碼、正在使用的總內存大小 (MB)。MB used or partially used spans
    60空閑以及等待歸還給操作系統的總內存大小(MB)。MB spans pending scavenging
    63通知操作系統中保留的內存大小(MB)MB mapped from the system
    57已經歸還給操作系統的(或者說還未正式申請)的內存大小(MB)。MB released to the system
    6已經從操作系統中申請的內存大小(MB)。MB allocated from the system

    方式2:?go tool trace

    go tool trace 的主要功能是將統計而來的信息以一種可視化的方式展示給用戶。要使用此工具,可以通過調用 trace API:

    package mainfunc main() {f, _ := os.Create("trace.out")defer f.Close()trace.Start(f)defer trace.Stop()(...) }

    并通過:

    $ go tool trace trace.out 2019/12/30 15:50:33 Parsing trace... 2019/12/30 15:50:38 Splitting trace... 2019/12/30 15:50:45 Opening browser. Trace viewer is listening on http://127.0.0.1:51839

    命令來啟動可視化界面:

    選擇第一個鏈接可以獲得如下圖示:

    右上角的問號可以打開幫助菜單,主要使用方式包括:

    • w/s 鍵可以用于放大或者縮小視圖

    • a/d 鍵可以用于左右移動

    方式3:?debug.ReadGCStats

    此方式可以通過代碼的方式來直接實現對感興趣指標的監控,例如我們希望每隔一秒鐘監控一次 GC 的狀態:

    func printGCStats() {t := time.NewTicker(time.Second)s := debug.GCStats{}for {select {case <-t.C:debug.ReadGCStats(&s)fmt.Printf("gc %d last@%v, PauseTotal %v\n", s.NumGC, s.LastGC, s.PauseTotal)}} } func main() {go printGCStats()(...) }

    我們能夠看到如下輸出:

    $ go run main.gogc 4954 last@2019-12-30 15:19:37.505575 +0100 CET, PauseTotal 29.901171ms gc 9195 last@2019-12-30 15:19:38.50565 +0100 CET, PauseTotal 77.579622ms gc 13502 last@2019-12-30 15:19:39.505714 +0100 CET, PauseTotal 128.022307ms gc 17555 last@2019-12-30 15:19:40.505579 +0100 CET, PauseTotal 182.816528ms gc 21838 last@2019-12-30 15:19:41.505595 +0100 CET, PauseTotal 246.618502ms

    方式4:?runtime.ReadMemStats

    除了使用 debug 包提供的方法外,還可以直接通過運行時的內存相關的 API 進行監控:

    func printMemStats() {t := time.NewTicker(time.Second)s := runtime.MemStats{}for {select {case <-t.C:runtime.ReadMemStats(&s)fmt.Printf("gc %d last@%v, next_heap_size@%vMB\n", s.NumGC, time.Unix(int64(time.Duration(s.LastGC).Seconds()), 0), s.NextGC/(1<<20))}} } func main() {go printMemStats()(...) }

    運行:

    $ go run main.gogc 4887 last@2019-12-30 15:44:56 +0100 CET, next_heap_size@4MB gc 10049 last@2019-12-30 15:44:57 +0100 CET, next_heap_size@4MB gc 15231 last@2019-12-30 15:44:58 +0100 CET, next_heap_size@4MB gc 20378 last@2019-12-30 15:44:59 +0100 CET, next_heap_size@6MB

    當然,后兩種方式能夠監控的指標很多,讀者可以自行查看 debug.GCStats 和runtime.MemStats 的字段,這里不再贅述。

    7. 有了 GC,為什么還會發生內存泄露?

    在一個具有 GC 的語言中,我們常說的內存泄漏,用嚴謹的話來說應該是:預期的能很快被釋放的內存由于附著在了長期存活的內存上、或生命期意外地被延長,導致預計能夠立即回收的內存而長時間得不到回收。

    在 Go 中,由于 goroutine 的存在,所謂的內存泄漏除了附著在長期對象上之外,還存在多種不同的形式。

    形式1:預期能被快速釋放的內存因被根對象引用而沒有得到迅速釋放

    當有一個全局對象時,可能不經意間將某個變量附著在其上,且忽略的將其進行釋放,則該內存永遠不會得到釋放。例如:

    var cache = map[interface{}]interface{}{}func keepalloc() {for i := 0; i < 10000; i++ {m := make([]byte, 1<<10)cache[i] = m} }

    形式2:goroutine 泄漏

    Goroutine 作為一種邏輯上理解的輕量級線程,需要維護執行用戶代碼的上下文信息。在運行過程中也需要消耗一定的內存來保存這類信息,而這些內存在目前版本的 Go 中是不會被釋放的。因此,如果一個程序持續不斷地產生新的 goroutine、且不結束已經創建的 goroutine 并復用這部分內存,就會造成內存泄漏的現象,例如:

    func keepalloc2() {for i := 0; i < 100000; i++ {go func() {select {}}()} }

    驗證

    我們可以通過如下形式來調用上述兩個函數:

    package mainimport ("os""runtime/trace" )func main() {f, _ := os.Create("trace.out")defer f.Close()trace.Start(f)defer trace.Stop()keepalloc()keepalloc2() }

    運行程序:

    go run main.go

    會看到程序中生成了 trace.out 文件,我們可以使用 go tool trace trace.out 命令得到下圖:

    可以看到,途中的 Heap 在持續增長,沒有內存被回收,產生了內存泄漏的現象。

    值得一提的是,這種形式的 goroutine 泄漏還可能由 channel 泄漏導致。而 channel 的泄漏本質上與 goroutine 泄漏存在直接聯系。Channel 作為一種同步原語,會連接兩個不同的 goroutine,如果一個 goroutine 嘗試向一個沒有接收方的無緩沖 channel 發送消息,則該 goroutine 會被永久的休眠,整個 goroutine 及其執行棧都得不到釋放,例如:

    var ch = make(chan struct{})func keepalloc3() {for i := 0; i < 100000; i++ {// 沒有接收方,goroutine 會一直阻塞go func() { ch <- struct{}{} }()} }

    8. 并發標記清除法的難點是什么?

    在沒有用戶態代碼并發修改 三色抽象的情況下,回收可以正常結束。但是并發回收的根本問題在于,用戶態代碼在回收過程中會并發地更新對象圖,從而造成賦值器和回收器可能對對象圖的結構產生不同的認知。這時以一個固定的三色波面作為回收過程前進的邊界則不再合理。

    我們不妨考慮賦值器寫操作的例子:

    回收器賦值器說明
    1shade(A, gray)
    回收器:根對象的子節點著色為灰色對象
    2shade(C, black)
    回收器:當所有子節點著色為灰色后,將節點著為黑色
    3
    C.ref3 = C.ref2.ref1賦值器:并發的修改了 C 的子節點
    4
    A.ref1 = nil賦值器:并發的修改了 A 的子節點
    5shade(A.ref1, gray)
    回收器:進一步灰色對象的子節點并著色為灰色對象,這時由于?A.ref1?為?nil,什么事情也沒有發生
    6shade(A, black)
    回收器:由于所有子節點均已標記,回收器也不會重新掃描已經被標記為黑色的對象,此時 A 被著色為黑色,?scan(A)?什么也不會發生,進而 B 在此次回收過程中永遠不會被標記為黑色,進而錯誤地被回收
    • 初始狀態:假設某個黑色對象 C 指向某個灰色對象 A ,而 A 指向白色對象 B;

    • C.ref3 = C.ref2.ref1:賦值器并發地將黑色對象 C 指向(ref3)了白色對象 B;

    • A.ref1 = nil:移除灰色對象 A 對白色對象 B 的引用(ref2);

    • 最終狀態:在繼續掃描的過程中,白色對象 B 永遠不會被標記為黑色對象了(回收器不會重新掃描黑色對象),進而對象 B 被錯誤地回收。

    總而言之,并發標記清除中面臨的一個根本問題就是如何保證標記與清除過程的正確性。

    9. 什么是寫屏障、混合寫屏障,如何實現?

    要講清楚寫屏障,就需要理解三色標記清除算法中的強弱不變性以及賦值器的顏色,理解他們需要一定的抽象思維。寫屏障是一個在并發垃圾回收器中才會出現的概念,垃圾回收器的正確性體現在:不應出現對象的丟失,也不應錯誤的回收還不需要回收的對象。

    可以證明,當以下兩個條件同時滿足時會破壞垃圾回收器的正確性:

    • 條件 1: 賦值器修改對象圖,導致某一黑色對象引用白色對象;

    • 條件 2: 從灰色對象出發,到達白色對象的、未經訪問過的路徑被賦值器破壞。

    只要能夠避免其中任何一個條件,則不會出現對象丟失的情況,因為:

    • 如果條件 1 被避免,則所有白色對象均被灰色對象引用,沒有白色對象會被遺漏;

    • 如果條件 2 被避免,即便白色對象的指針被寫入到黑色對象中,但從灰色對象出發,總存在一條沒有訪問過的路徑,從而找到到達白色對象的路徑,白色對象最終不會被遺漏。

    我們不妨將三色不變性所定義的波面根據這兩個條件進行削弱:

    • 當滿足原有的三色不變性定義(或上面的兩個條件都不滿足時)的情況稱為強三色不變性(strong tricolor invariant)

    • 當賦值器令黑色對象引用白色對象時(滿足條件 1 時)的情況稱為弱三色不變性(weak tricolor invariant)

    當賦值器進一步破壞灰色對象到達白色對象的路徑時(進一步滿足條件 2 時),即打破弱三色不變性,也就破壞了回收器的正確性;或者說,在破壞強弱三色不變性時必須引入額外的輔助操作。弱三色不變形的好處在于:只要存在未訪問的能夠到達白色對象的路徑,就可以將黑色對象指向白色對象。

    如果我們考慮并發的用戶態代碼,回收器不允許同時停止所有賦值器,就是涉及了存在的多個不同狀態的賦值器。為了對概念加以明確,還需要換一個角度,把回收器視為對象,把賦值器視為影響回收器這一對象的實際行為(即影響 GC 周期的長短),從而引入賦值器的顏色:

    • 黑色賦值器:已經由回收器掃描過,不會再次對其進行掃描。

    • 灰色賦值器:尚未被回收器掃描過,或盡管已經掃描過但仍需要重新掃描。

    賦值器的顏色對回收周期的結束產生影響:

    • 如果某種并發回收器允許灰色賦值器的存在,則必須在回收結束之前重新掃描對象圖。

    • 如果重新掃描過程中發現了新的灰色或白色對象,回收器還需要對新發現的對象進行追蹤,但是在新追蹤的過程中,賦值器仍然可能在其根中插入新的非黑色的引用,如此往復,直到重新掃描過程中沒有發現新的白色或灰色對象。

    于是,在允許灰色賦值器存在的算法,最壞的情況下,回收器只能將所有賦值器線程停止才能完成其跟對象的完整掃描,也就是我們所說的 STW。

    為了確保強弱三色不變性的并發指針更新操作,需要通過賦值器屏障技術來保證指針的讀寫操作一致。因此我們所說的 Go 中的寫屏障、混合寫屏障,其實是指賦值器的寫屏障,賦值器的寫屏障用來保證賦值器在進行指針寫操作時,不會破壞弱三色不變性。

    有兩種非常經典的寫屏障:Dijkstra 插入屏障和 Yuasa 刪除屏障。

    灰色賦值器的 Dijkstra 插入屏障的基本思想是避免滿足條件 1:

    // 灰色賦值器 Dijkstra 插入屏障 func DijkstraWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {shade(ptr)*slot = ptr }

    ?為了防止黑色對象指向白色對象,應該假設 *slot 可能會變為黑色,為了確保 ptr 不會在被賦值到 *slot 前變為白色, shade(ptr) 會先將指針 ptr 標記為灰色,進而避免了條件 1。但是,由于并不清楚賦值器以后會不會將這個引用刪除,因此還需要重新掃描來重新確定關系圖,這時需要 STW,如圖所示:

    Dijkstra 插入屏障的好處在于可以立刻開始并發標記,但由于產生了灰色賦值器,缺陷是需要標記終止階段 STW 時進行重新掃描。

    黑色賦值器的 Yuasa 刪除屏障的基本思想是避免滿足條件 2:

    // 黑色賦值器 Yuasa 屏障 func YuasaWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {shade(*slot)*slot = ptr }

    為了防止丟失從灰色對象到白色對象的路徑,應該假設 *slot 可能會變為黑色,為了確保 ptr 不會在被賦值到 *slot 前變為白色, shade(*slot) 會先將 *slot 標記為灰色,進而該寫操作總是創造了一條灰色到灰色或者灰色到白色對象的路徑,進而避免了條件 2。

    Yuasa 刪除屏障的優勢則在于不需要標記結束階段的重新掃描,缺陷是依然會產生丟失的對象,需要在標記開始前對整個對象圖進行快照。

    Go 在 1.8 的時候為了簡化 GC 的流程,同時減少標記終止階段的重掃成本,將 Dijkstra 插入屏障和 Yuasa 刪除屏障進行混合,形成混合寫屏障。該屏障提出時的基本思想是:對正在被覆蓋的對象進行著色,且如果當前棧未掃描完成,則同樣對指針進行著色。

    但在最終實現時原提案中對 ptr 的著色還額外包含對執行棧的著色檢查,但由于時間有限,并未完整實現過,所以混合寫屏障在目前的實現偽代碼是:

    // 混合寫屏障 func HybridWritePointerSimple(slot *unsafe.Pointer, ptr unsafe.Pointer) {shade(*slot)shade(ptr)*slot = ptr }

    在這個實現中,如果無條件對引用雙方進行著色,自然結合了 Dijkstra 和 Yuasa 寫屏障的優勢,但缺點也非常明顯,因為著色成本是雙倍的,而且編譯器需要插入的代碼也成倍增加,隨之帶來的結果就是編譯后的二進制文件大小也進一步增加。為了針對寫屏障的性能進行優化,Go 1.10 前后,Go 團隊隨后實現了批量寫屏障機制。其基本想法是將需要著色的指針同一寫入一個緩存,每當緩存滿時統一對緩存中的所有 ptr 指針進行著色。


    GC 的實現細節

    10. Go 語言中 GC 的流程是什么?

    當前版本的 Go 以 STW 為界限,可以將 GC 劃分為五個階段:

    階段說明賦值器狀態
    GCMark標記準備階段,為并發標記做準備工作,啟動寫屏障STW
    GCMark掃描標記階段,與賦值器并發執行,寫屏障開啟并發
    GCMarkTermination標記終止階段,保證一個周期內標記任務完成,停止寫屏障STW
    GCoff內存清掃階段,將需要回收的內存歸還到堆中,寫屏障關閉并發
    GCoff內存歸還階段,將過多的內存歸還給操作系統,寫屏障關閉并發

    具體而言,各個階段的觸發函數分別為:

    11. 觸發 GC 的時機是什么?

    Go 語言中對 GC 的觸發時機存在兩種形式:

  • 主動觸發,通過調用 runtime.GC 來觸發 GC,此調用阻塞式地等待當前 GC 運行完畢。

  • 被動觸發,分為兩種方式:

    • 使用系統監控,當超過兩分鐘沒有產生任何 GC 時,強制觸發 GC。

    • 使用步調(Pacing)算法,其核心思想是控制內存增長的比例。

    由于本問題剩余內容公式太多,無法完美在公眾號文章展示,建議點擊閱讀原文,直達原文,享受更好的閱讀體驗。

    12. 如果內存分配速度超過了標記清除的速度怎么辦?

    目前的 Go 實現中,當 GC 觸發后,會首先進入并發標記的階段。并發標記會設置一個標志,并在 mallocgc 調用時進行檢查。當存在新的內存分配時,會暫停分配內存過快的那些 goroutine,并將其轉去執行一些輔助標記(Mark Assist)的工作,從而達到放緩繼續分配、輔助 GC 的標記工作的目的。

    編譯器會分析用戶代碼,并在需要分配內存的位置,將申請內存的操作翻譯為 mallocgc 調用,而 mallocgc 的實現決定了標記輔助的實現,其偽代碼思路如下:

    func mallocgc(t typ.Type, size uint64) {if enableMarkAssist {// 進行標記輔助,此時用戶代碼沒有得到執行(...)}// 執行內存分配(...) }

    GC 的優化問題

    13. GC 關注的指標有哪些?

    Go 的 GC 被設計為成比例觸發、大部分工作與賦值器并發、不分代、無內存移動且會主動向操作系統歸還申請的內存。因此最主要關注的、能夠影響賦值器的性能指標有:

    • CPU 利用率:回收算法會在多大程度上拖慢程序?有時候,這個是通過回收占用的 CPU 時間與其它 CPU 時間的百分比來描述的。

    • GC 停頓時間:回收器會造成多長時間的停頓?目前的 GC 中需要考慮 STW 和 Mark Assist 兩個部分可能造成的停頓。

    • GC 停頓頻率:回收器造成的停頓頻率是怎樣的?目前的 GC 中需要考慮 STW 和 Mark Assist 兩個部分可能造成的停頓。

    • GC 可擴展性:當堆內存變大時,垃圾回收器的性能如何?但大部分的程序可能并不一定關心這個問題。

    14. Go 的 GC 如何調優?

    Go 的 GC 被設計為極致簡潔,與較為成熟的 Java GC 的數十個可控參數相比,嚴格意義上來講,Go 可供用戶調整的參數只有 GOGC 環境變量。當我們談論 GC 調優時,通常是指減少用戶代碼對 GC 產生的壓力,這一方面包含了減少用戶代碼分配內存的數量(即對程序的代碼行為進行調優),另一方面包含了最小化 Go 的 GC 對 CPU 的使用率(即調整 GOGC)。

    GC 的調優是在特定場景下產生的,并非所有程序都需要針對 GC 進行調優。只有那些對執行延遲非常敏感、當 GC 的開銷成為程序性能瓶頸的程序,才需要針對 GC 進行性能調優,幾乎不存在于實際開發中 99% 的情況。除此之外,Go 的 GC 也仍然有一定的可改進的空間,也有部分 GC 造成的問題,目前仍屬于 Open Problem。

    總的來說,我們可以在現在的開發中處理的有以下幾種情況:

  • 對停頓敏感:GC 過程中產生的長時間停頓、或由于需要執行 GC 而沒有執行用戶代碼,導致需要立即執行的用戶代碼執行滯后。

  • 對資源消耗敏感:對于頻繁分配內存的應用而言,頻繁分配內存增加 GC 的工作量,原本可以充分利用 CPU 的應用不得不頻繁地執行垃圾回收,影響用戶代碼對 CPU 的利用率,進而影響用戶代碼的執行效率。

  • 從這兩點來看,所謂 GC 調優的核心思想也就是充分的圍繞上面的兩點來展開:優化內存的申請速度,盡可能的少申請內存,復用已申請的內存。或者簡單來說,不外乎這三個關鍵字:控制、減少、復用

    我們將通過三個實際例子介紹如何定位 GC 的存在的問題,并一步一步進行性能調優。當然,在實際情況中問題遠比這些例子要復雜,這里也只是討論調優的核心思想,更多的時候也只能具體問題具體分析。

    例1:合理化內存分配的速度、提高賦值器的 CPU 利用率

    我們來看這樣一個例子。在這個例子中, concat 函數負責拼接一些長度不確定的字符串。并且為了快速完成任務,出于某種原因,在兩個嵌套的 for 循環中一口氣創建了 800 個 goroutine。在 main 函數中,啟動了一個 goroutine 并在程序結束前不斷的觸發 GC,并嘗試輸出 GC 的平均執行時間:

    package mainimport ("fmt""os""runtime""runtime/trace""sync/atomic""time" )var (stop int32count int64sum time.Duration )func concat() {for n := 0; n < 100; n++ {for i := 0; i < 8; i++ {go func() {s := "Go GC"s += " " + "Hello"s += " " + "World"_ = s}()}} }func main() {f, _ := os.Create("trace.out")defer f.Close()trace.Start(f)defer trace.Stop()go func() {var t time.Timefor atomic.LoadInt32(&stop) == 0 {t = time.Now()runtime.GC()sum += time.Since(t)count++}fmt.Printf("GC spend avg: %v\n", time.Duration(int64(sum)/count))}()concat()atomic.StoreInt32(&stop, 1) }

    這個程序的執行結果是:

    $ go build -o main $ ./main GC spend avg: 2.583421ms

    GC 平均執行一次需要長達 2ms 的時間,我們再進一步觀察 trace 的結果:

    程序的整個執行過程中僅執行了一次 GC,而且僅 Sweep STW 就耗費了超過 1 ms,非常反常。甚至查看賦值器 mutator 的 CPU 利用率,在整個 trace 尺度下連 40% 都不到:

    主要原因是什么呢?我們不妨查看 goroutine 的分析:

    在這個榜單中我們不難發現,goroutine 的執行時間占其生命周期總時間非常短的一部分,但大部分時間都花費在調度器的等待上了(藍色的部分),說明同時創建大量 goroutine 對調度器產生的壓力確實不小,我們不妨將這一產生速率減慢,一批一批地創建 goroutine:

    func concat() {wg := sync.WaitGroup{}for n := 0; n < 100; n++ {wg.Add(8)for i := 0; i < 8; i++ {go func() {s := "Go GC"s += " " + "Hello"s += " " + "World"_ = swg.Done()}()}wg.Wait()} }

    這時候我們再來看:

    $ go build -o main $ ./main GC spend avg: 328.54μs

    GC 的平均時間就降到 300 微秒了。這時的賦值器 CPU 使用率也提高到了 60%,相對來說就很可觀了:

    當然,這個程序仍然有優化空間,例如我們其實沒有必要等待很多 goroutine 同時執行完畢才去執行下一組 goroutine。而可以當一個 goroutine 執行完畢時,直接啟動一個新的 goroutine,也就是 goroutine 池的使用。有興趣的讀者可以沿著這個思路進一步優化這個程序中賦值器對 CPU 的使用率。

    例2:降低并復用已經申請的內存

    我們通過一個非常簡單的 Web 程序來說明復用內存的重要性。在這個程序中,每當產生一個 /example2的請求時,都會創建一段內存,并用于進行一些后續的工作。

    package mainimport ("fmt""net/http"_ "net/http/pprof" )func newBuf() []byte {return make([]byte, 10<<20) }func main() {go func() {http.ListenAndServe("localhost:6060", nil)}()http.HandleFunc("/example2", func(w http.ResponseWriter, r *http.Request) {b := newBuf()// 模擬執行一些工作for idx := range b {b[idx] = 1}fmt.Fprintf(w, "done, %v", r.URL.Path[1:])})http.ListenAndServe(":8080", nil) }

    為了進行性能分析,我們還額外創建了一個監聽 6060 端口的 goroutine,用于使用 pprof 進行分析。我們先讓服務器跑起來:

    $ go build -o main $ ./main

    我們這次使用 pprof 的 trace 來查看 GC 在此服務器中面對大量請求時候的狀態,要使用 trace 可以通過訪問 /debug/pprof/trace 路由來進行,其中 seconds 參數設置為 20s,并將 trace 的結果保存為 trace.out:

    $ wget http://127.0.0.1:6060/debug/pprof/trace\?seconds\=20 -O trace.out --2020-01-01 22:13:34-- http://127.0.0.1:6060/debug/pprof/trace?seconds=20 Connecting to 127.0.0.1:6060... connected. HTTP request sent, awaiting response...

    這時候我們使用一個壓測工具 ab,來同時產生 500 個請求( -n 一共 500 個請求, -c 一個時刻執行請求的數量,每次 100 個并發請求):

    $ ab -n 500 -c 100 http://127.0.0.1:8080/example2 This is ApacheBench, Version 2.3 <$Revision: 1843412 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/Benchmarking 127.0.0.1 (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Finished 500 requestsServer Software: Server Hostname: 127.0.0.1 Server Port: 8080Document Path: /example2 Document Length: 14 bytesConcurrency Level: 100 Time taken for tests: 0.987 seconds Complete requests: 500 Failed requests: 0 Total transferred: 65500 bytes HTML transferred: 7000 bytes Requests per second: 506.63 [#/sec] (mean) Time per request: 197.382 [ms] (mean) Time per request: 1.974 [ms] (mean, across all concurrent requests) Transfer rate: 64.81 [Kbytes/sec] receivedConnection Times (ms)min mean[+/-sd] median max Connect: 0 1 1.1 0 7 Processing: 13 179 77.5 170 456 Waiting: 10 168 78.8 162 455 Total: 14 180 77.3 171 458Percentage of the requests served within a certain time (ms)50% 17166% 20375% 22280% 23990% 28195% 33598% 36599% 400100% 458 (longest request)

    GC 反復被觸發,一個顯而易見的原因就是內存分配過多。我們可以通過 go tool pprof 來查看究竟是誰分配了大量內存(使用 web 指令來使用瀏覽器打開統計信息的可視化圖形):

    $ go tool pprof http://127.0.0.1:6060/debug/pprof/heap Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap Saved profile in /Users/changkun/pprof/pprof.alloc_objects.alloc_space.inuse_o bjects.inuse_space.003.pb.gz Type: inuse_space Time: Jan 1, 2020 at 11:15pm (CET) Entering interactive mode (type "help" for commands, "o" for options) (pprof) web (pprof)

    可見 newBuf 產生的申請的內存過多,現在我們使用 sync.Pool 來復用 newBuf 所產生的對象:

    package mainimport ("fmt""net/http"_ "net/http/pprof""sync" )// 使用 sync.Pool 復用需要的 buf var bufPool = sync.Pool{New: func() interface{} {return make([]byte, 10<<20)}, }func main() {go func() {http.ListenAndServe("localhost:6060", nil)}()http.HandleFunc("/example2", func(w http.ResponseWriter, r *http.Request) {b := bufPool.Get().([]byte)for idx := range b {b[idx] = 0}fmt.Fprintf(w, "done, %v", r.URL.Path[1:])bufPool.Put(b)})http.ListenAndServe(":8080", nil) }

    其中 ab 輸出的統計結果為:

    $ ab -n 500 -c 100 http://127.0.0.1:8080/example2 This is ApacheBench, Version 2.3 <$Revision: 1843412 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/Benchmarking 127.0.0.1 (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Finished 500 requestsServer Software: Server Hostname: 127.0.0.1 Server Port: 8080Document Path: /example2 Document Length: 14 bytesConcurrency Level: 100 Time taken for tests: 0.427 seconds Complete requests: 500 Failed requests: 0 Total transferred: 65500 bytes HTML transferred: 7000 bytes Requests per second: 1171.32 [#/sec] (mean) Time per request: 85.374 [ms] (mean) Time per request: 0.854 [ms] (mean, across all concurrent requests) Transfer rate: 149.85 [Kbytes/sec] receivedConnection Times (ms)min mean[+/-sd] median max Connect: 0 1 1.4 1 9 Processing: 5 75 48.2 66 211 Waiting: 5 72 46.8 63 207 Total: 5 77 48.2 67 211Percentage of the requests served within a certain time (ms)50% 6766% 8975% 10780% 12290% 14895% 16798% 19699% 204100% 211 (longest request)

    但從 Requestsper second 每秒請求數來看,從原來的 506.63 變為 1171.32 得到了近乎一倍的提升。從 trace 的結果來看,GC 也沒有頻繁的被觸發從而長期消耗 CPU 使用率:

    sync.Pool 是內存復用的一個最為顯著的例子,從語言層面上還有很多類似的例子,例如在例 1 中, concat 函數可以預先分配一定長度的緩存,而后再通過 append 的方式將字符串存儲到緩存中:

    func concat() {wg := sync.WaitGroup{}for n := 0; n < 100; n++ {wg.Add(8)for i := 0; i < 8; i++ {go func() {s := make([]byte, 0, 20)s = append(s, "Go GC"...)s = append(s, ' ')s = append(s, "Hello"...)s = append(s, ' ')s = append(s, "World"...)_ = string(s)wg.Done()}()}wg.Wait()} }

    原因在于 + 運算符會隨著字符串長度的增加而申請更多的內存,并將內容從原來的內存位置拷貝到新的內存位置,造成大量不必要的內存分配,先提前分配好足夠的內存,再慢慢地填充,也是一種減少內存分配、復用內存形式的一種表現。

    例3:調整 GOGC

    我們已經知道了 GC 的觸發原則是由步調算法來控制的,其關鍵在于估計下一次需要觸發 GC 時,堆的大小。可想而知,如果我們在遇到海量請求的時,為了避免 GC 頻繁觸發,是否可以通過將 GOGC 的值設置得更大,讓 GC 觸發的時間變得更晚,從而減少其觸發頻率,進而增加用戶代碼對機器的使用率呢?答案是肯定的。

    我們可以非常簡單粗暴的將 GOGC 調整為 1000,來執行上一個例子中未復用對象之前的程序:

    $ GOGC=1000 ./main

    這時我們再重新執行壓測:

    $ ab -n 500 -c 100 http://127.0.0.1:8080/example2 This is ApacheBench, Version 2.3 <$Revision: 1843412 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/Benchmarking 127.0.0.1 (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Finished 500 requestsServer Software: Server Hostname: 127.0.0.1 Server Port: 8080Document Path: /example2 Document Length: 14 bytesConcurrency Level: 100 Time taken for tests: 0.923 seconds Complete requests: 500 Failed requests: 0 Total transferred: 65500 bytes HTML transferred: 7000 bytes Requests per second: 541.61 [#/sec] (mean) Time per request: 184.636 [ms] (mean) Time per request: 1.846 [ms] (mean, across all concurrent requests) Transfer rate: 69.29 [Kbytes/sec] receivedConnection Times (ms)min mean[+/-sd] median max Connect: 0 1 1.8 0 20 Processing: 9 171 210.4 66 859 Waiting: 5 158 199.6 62 813 Total: 9 173 210.6 68 860Percentage of the requests served within a certain time (ms)50% 6866% 13375% 19880% 29290% 56695% 69698% 72399% 743100% 860 (longest request)

    可以看到,壓測的結果得到了一定幅度的改善( Requestsper second 從原來的 506.63 提高為了 541.61),

    并且 GC 的執行頻率明顯降低:

    在實際實踐中可表現為需要緊急處理一些由 GC 帶來的瓶頸時,人為將 GOGC 調大,加錢加內存,扛過這一段峰值流量時期。

    當然,這種做法其實是治標不治本,并沒有從根本上解決內存分配過于頻繁的問題,極端情況下,反而會由于 GOGC 太大而導致回收不及時而耗費更多的時間來清理產生的垃圾,這對時間不算敏感的應用還好,但對實時性要求較高的程序來說就是致命的打擊了。

    因此這時更妥當的做法仍然是,定位問題的所在,并從代碼層面上進行優化。

    小結

    通過上面的三個例子我們可以看到在 GC 調優過程中 go tool pprof 和 go tool trace 的強大作用是幫助我們快速定位 GC 導致瓶頸的具體位置,但這些例子中僅僅覆蓋了其功能的很小一部分,我們也沒有必要完整覆蓋所有的功能,因為總是可以通過http pprof 官方文檔、runtime pprof官方文檔以及trace 官方文檔來舉一反三。

    現在我們來總結一下前面三個例子中的優化情況:

  • 控制內存分配的速度,限制 goroutine 的數量,從而提高賦值器對 CPU 的利用率。

  • 減少并復用內存,例如使用 sync.Pool 來復用需要頻繁創建臨時對象,例如提前分配足夠的內存來降低多余的拷貝。

  • 需要時,增大 GOGC 的值,降低 GC 的運行頻率。

  • 這三種情況幾乎涵蓋了 GC 調優中的核心思路,雖然從語言上還有很多小技巧可說,但我們并不會在這里事無巨細的進行總結。實際情況也是千變萬化,我們更應該著重于培養具體問題具體分析的能力。

    當然,我們還應該謹記 過早優化是萬惡之源這一警語,在沒有遇到應用的真正瓶頸時,將寶貴的時間分配在開發中其他優先級更高的任務上。

    15. Go 的垃圾回收器有哪些相關的 API?其作用分別是什么?

    在 Go 中存在數量極少的與 GC 相關的 API,它們是

    • runtime.GC:手動觸發 GC

    • runtime.ReadMemStats:讀取內存相關的統計信息,其中包含部分 GC 相關的統計信息

    • debug.FreeOSMemory:手動將內存歸還給操作系統

    • debug.ReadGCStats:讀取關于 GC 的相關統計信息

    • debug.SetGCPercent:設置 GOGC 調步變量

    • debug.SetMaxHeap(尚未發布):設置 Go 程序堆的上限值


    GC 的歷史及演進

    16. Go 歷史各個版本在 GC 方面的改進?

    Go 1:串行三色標記清掃

    Go 1.3:并行清掃,標記過程需要 STW,停頓時間在約幾百毫秒

    Go 1.5:并發標記清掃,停頓時間在一百毫秒以內

    Go 1.6:使用 bitmap 來記錄回收內存的位置,大幅優化垃圾回收器自身消耗的內存,停頓時間在十毫秒以內

    Go 1.7:停頓時間控制在兩毫秒以內

    Go 1.8:混合寫屏障,停頓時間在半個毫秒左右

    Go 1.9:徹底移除了棧的重掃描過程

    Go 1.12:整合了兩個階段的 Mark Termination,但引入了一個嚴重的 GC Bug 至今未修(見問題 20),尚無該 Bug 對 GC 性能影響的報告

    Go 1.13:著手解決向操作系統歸還內存的,提出了新的 Scavenger

    Go 1.14:替代了僅存活了一個版本的 scavenger,全新的頁分配器,優化分配內存過程的速率與現有的擴展性問題,并引入了異步搶占,解決了由于密集循環導致的 STW 時間過長的問題

    可以用下圖直觀地說明 GC 的演進歷史:

    在 Go 1 剛發布時的版本中,甚至沒有將 Mark-Sweep 的過程并行化,當需要進行垃圾回收時,所有的代碼都必須進入 STW 的狀態。而到了 Go 1.1 時,官方迅速地將清掃過程進行了并行化的處理,即僅在標記階段進入 STW。

    這一想法很自然,因為并行化導致算法結果不一致的情況僅僅發生在標記階段,而當時的垃圾回收器沒有針對并行結果的一致性進行任何優化,因此才需要在標記階段進入 STW。對于 Scavenger 而言,早期的版本中會有一個單獨的線程來定期將多余的內存歸還給操作系統。

    而到了 Go 1.5 后,Go 團隊花費了相當大的力氣,通過引入寫屏障的機制來保證算法的一致性,才得以將整個 GC 控制在很小的 STW 內,而到了 1.8 時,由于新的混合屏障的出現,消除了對棧本身的重新掃描,STW 的時間進一步縮減。

    從這個時候開始,Scavenger 已經從獨立線程中移除,并合并至系統監控這個獨立的線程中,并周期性地向操作系統歸還內存,但仍然會有內存溢出這種比較極端的情況出現,因為程序可能在短時間內應對突發性的內存申請需求時,內存還沒來得及歸還操作系統,導致堆不斷向操作系統申請內存,從而出現內存溢出。

    到了 Go 1.13,定期歸還操作系統的問題得以解決,Go 團隊開始將周期性的 Scavenger 轉化為可被調度的 goroutine,并將其與用戶代碼并發執行。而到了 Go 1.14,這一向操作系統歸還內存的操作時間進一步得到縮減。

    17. Go GC 在演化過程中還存在哪些其他設計?為什么沒有被采用?

    并發棧重掃

    正如我們前面所說,允許灰色賦值器存在的垃圾回收器需要引入重掃過程來保證算法的正確性,除了引入混合屏障來消除重掃這一過程外,有另一種做法可以提高重掃過程的性能,那就是將重掃的過程并發執行。然而這一方案并沒有得以實現,原因很簡單:實現過程相比引入混合屏障而言十分復雜,而且引入混合屏障能夠消除重掃這一過程,將簡化垃圾回收的步驟。

    ROC

    ROC 的全稱是面向請求的回收器(Request Oriented Collector),它其實也是分代 GC 的一種重新敘述。它提出了一個請求假設(Request Hypothesis):與一個完整請求、休眠 goroutine 所關聯的對象比其他對象更容易死亡。這個假設聽起來非常符合直覺,但在實現上,由于垃圾回收器必須確保是否有 goroutine 私有指針被寫入公共對象,因此寫屏障必須一直打開,這也就產生了該方法的致命缺點:昂貴的寫屏障及其帶來的緩存未命中,這也是這一設計最終沒有被采用的主要原因。

    傳統分代 GC

    在發現 ROC 性能不行之后,作為備選方案,Go 團隊還嘗試了實現傳統的分代式 GC。但最終同樣發現分代假設并不適用于 Go 的運行棧機制,年輕代對象在棧上就已經死亡,掃描本就該回收的執行棧并沒有為由于分代假設帶來明顯的性能提升。這也是這一設計最終沒有被采用的主要原因。

    18. 目前提供 GC 的語言以及不提供 GC 的語言有哪些?GC 和 No GC 各自的優缺點是什么?

    從原理上而言,所有的語言都能夠自行實現 GC。從語言誕生之初就提供 GC 的語言,例如:

    • Python

    • JavaScript

    • Java

    • Objective-C

    • Swift

    而不以 GC 為目標,被直接設計為手動管理內存、但可以自行實現 GC 的語言有:

    • C

    • C++

    也有一些語言可以在編譯期,依靠編譯器插入清理代碼的方式,實現精準的清理,例如:

    • Rust

    垃圾回收使程序員無需手動處理內存釋放,從而能夠消除一些需要手動管理內存才會出現的運行時錯誤:

  • 在仍然有指向內存區塊的指針的情況下釋放這塊內存時,會產生懸掛指針,從而后續可能錯誤的訪問已經用于他用的內存區域。

  • 多重釋放同一塊申請的內存區域可能導致不可知的內存損壞。

  • 當然,垃圾回收也會伴隨一些缺陷,這也就造就了沒有 GC 的一些優勢:

  • 沒有額外的性能開銷

  • 精準的手動內存管理,極致的利用機器的性能

  • 19. Go 對比 Java、V8 中 JavaScript 的 GC 性能如何?

    無論是 Java 還是 JavaScript 中的 GC 均為分代式 GC。分代式 GC 的一個核心假設就是分代假說:將對象依據存活時間分配到不同的區域,每次回收只回收其中的一個區域。

    V8 的 GC

    在 V8 中主要將內存分為新生代和老生代。新生代中的對象為存活時間較短的對象,老生代中的對象為存活時間較長、常駐內存、占用內存較大的對象:

  • 新生代中的對象主要通過副垃圾回收器進行回收。該回收過程是一種采用復制的方式實現的垃圾回收算法,它將堆內存一分為二,這兩個空間中只有一個處于使用中,另一個則處于閑置狀態。處于使用狀態的空間稱為 From 空間,處于閑置的空間稱為 To 空間。分配對象時,先是在 From 空間中進行分配,當開始垃圾回收時,會檢查 From 空間中的存活對象,并將這些存活對象復制到 To 空間中,而非存活對象占用的空間被釋放。完成復制后,From 空間和 To 空間的角色互換。也就是通過將存活對象在兩個空間中進行復制。

  • 老生代則由主垃圾回收器負責。它實現的是標記清掃過程,但略有不同之處在于它還會在清掃完成后對內存碎片進行整理,進而是一種標記整理的回收器。

  • Java 的 GC

    Java 的 GC 稱之為 G1,并將整個堆分為年輕代、老年代和永久代。包括四種不同的收集操作,從上往下的這幾個階段會選擇性地執行,觸發條件是用戶的配置和實際代碼行為的預測。

  • 年輕代收集周期:只對年輕代對象進行收集與清理

  • 老年代收集周期:只對老年代對象進行收集與清理

  • 混合式收集周期:同時對年輕代和老年代進行收集與清理

  • 完整 GC 周期:完整的對整個堆進行收集與清理

  • 在回收過程中,G1 會對停頓時間進行預測,竭盡所能地調整 GC 的策略從而達到用戶代碼通過系統參數( -XX:MaxGCPauseMillis)所配置的對停頓時間的要求。

    這四個周期的執行成本逐漸上升,優化得當的程序可以完全避免完整 GC 周期。

    性能比較

    在 Go、Java 和 V8 JavaScript 之間比較 GC 的性能本質上是一個不切實際的問題。如前面所說,垃圾回收器的設計權衡了很多方面的因素,同時還受語言自身設計的影響,因為語言的設計也直接影響了程序員編寫代碼的形式,也就自然影響了產生垃圾的方式。

    但總的來說,他們三者對垃圾回收的實現都需要 STW,并均已達到了用戶代碼幾乎無法感知到的狀態(據 Go GC 作者 Austin 宣稱 STW 小于 100 微秒)。當然,隨著 STW 的減少,垃圾回收器會增加 CPU 的使用率,這也是程序員在編寫代碼時需要手動進行優化的部分,即充分考慮內存分配的必要性,減少過多申請內存帶給垃圾回收器的壓力。

    20. 目前 Go 語言的 GC 還存在哪些問題?

    盡管 Go 團隊宣稱 STW 停頓時間得以優化到 100 微秒級別,但這本質上是一種取舍。原本的 STW 某種意義上來說其實轉移到了可能導致用戶代碼停頓的幾個位置;除此之外,由于運行時調度器的實現方式,同樣對 GC 存在一定程度的影響。

    目前 Go 中的 GC 仍然存在以下問題:

    1. Mark Assist 停頓時間過長

    package mainimport ("fmt""os""runtime""runtime/trace""time" )const (windowSize = 200000msgCount = 1000000 )var (best time.Duration = time.SecondbestAt time.Timeworst time.DurationworstAt time.Timestart = time.Now() )func main() {f, _ := os.Create("trace.out")defer f.Close()trace.Start(f)defer trace.Stop()for i := 0; i < 5; i++ {measure()worst = 0best = time.Secondruntime.GC()} }func measure() {var c channelfor i := 0; i < msgCount; i++ {c.sendMsg(i)}fmt.Printf("Best send delay %v at %v, worst send delay: %v at %v. Wall clock: %v \n", best, bestAt.Sub(start), worst, worstAt.Sub(start), time.Since(start)) }type channel [windowSize][]bytefunc (c *channel) sendMsg(id int) {start := time.Now()// 模擬發送(*c)[id%windowSize] = newMsg(id)end := time.Now()elapsed := end.Sub(start)if elapsed > worst {worst = elapsedworstAt = end}if elapsed < best {best = elapsedbestAt = end} }func newMsg(n int) []byte {m := make([]byte, 1024)for i := range m {m[i] = byte(n)}return m }

    運行此程序我們可以得到類似下面的結果:

    $ go run main.goBest send delay 330ns at 773.037956ms, worst send delay: 7.127915ms at 579.835487ms. Wall clock: 831.066632ms Best send delay 331ns at 873.672966ms, worst send delay: 6.731947ms at 1.023969626s. Wall clock: 1.515295559s Best send delay 330ns at 1.812141567s, worst send delay: 5.34028ms at 2.193858359s. Wall clock: 2.199921749s Best send delay 338ns at 2.722161771s, worst send delay: 7.479482ms at 2.665355216s. Wall clock: 2.920174197s Best send delay 337ns at 3.173649445s, worst send delay: 6.989577ms at 3.361716121s. Wall clock: 3.615079348s

    在這個結果中,第一次的最壞延遲時間高達 7.12 毫秒,發生在程序運行 578 毫秒左右。通過 go tool trace 可以發現,這個時間段中,Mark Assist 執行了 7112312ns,約為 7.127915ms;可見,此時最壞情況下,標記輔助拖慢了用戶代碼的執行,是造成 7 毫秒延遲的原因。

    2. Sweep 停頓時間過長

    同樣還是剛才的例子,如果我們仔細觀察 Mark Assist 后發生的 Sweep 階段,竟然對用戶代碼的影響長達約 30ms,根據調用棧信息可以看到,該 Sweep 過程發生在內存分配階段:

    3. 由于 GC 算法的不正確性導致 GC 周期被迫重新執行

    此問題很難復現,但是一個已知的問題,根據 Go 團隊的描述,能夠在 1334 次構建中發生一次,我們可以計算出其觸發概率約為 0.0007496251874。雖然發生概率很低,但一旦發生,GC 需要被重新執行,非常不幸。

    4. 創建大量 Goroutine 后導致 GC 消耗更多的 CPU

    這個問題可以通過以下程序進行驗證:

    func BenchmarkGCLargeGs(b *testing.B) {wg := sync.WaitGroup{}for ng := 100; ng <= 1000000; ng *= 10 {b.Run(fmt.Sprintf("#g-%d", ng), func(b *testing.B) {// 創建大量 goroutine,由于每次創建的 goroutine 會休眠// 從而運行時不會復用正在休眠的 goroutine,進而不斷創建新的 gwg.Add(ng)for i := 0; i < ng; i++ {go func() {time.Sleep(100 * time.Millisecond)wg.Done()}()}wg.Wait()// 現運行一次 GC 來提供一致的內存環境runtime.GC()// 記錄運行 b.N 次 GC 需要的時間b.ResetTimer()for i := 0; i < b.N; i++ {runtime.GC()}})} }

    其結果可以通過如下指令來獲得:

    $ go test -bench=BenchmarkGCLargeGs -run=^$ -count=5 -v . | tee 4.txt $ benchstat 4.txt name time/op GCLargeGs/#g-100-12 192μs ± 5% GCLargeGs/#g-1000-12 331μs ± 1% GCLargeGs/#g-10000-12 1.22ms ± 1% GCLargeGs/#g-100000-12 10.9ms ± 3% GCLargeGs/#g-1000000-12 32.5ms ± 4%

    這種情況通常發生于峰值流量后,大量 goroutine 由于任務等待被休眠,從而運行時不斷創建新的 goroutine,舊的 goroutine 由于休眠未被銷毀且得不到復用,導致 GC 需要掃描的執行棧越來越多,進而完成 GC 所需的時間越來越長。一個解決辦法是使用 goroutine 池來限制創建的 goroutine 數量。

    總結

    GC 是一個復雜的系統工程,本文討論的二十個問題盡管已經展現了一個相對全面的 Go GC。但它們仍然只是 GC 這一宏觀問題的一些較為重要的部分,還有非常多的細枝末節、研究進展無法在有限的篇幅內完整討論。

    從 Go 誕生之初,Go 團隊就一直在對 GC 的表現進行實驗與優化,但仍然有諸多未解決的問題,我們不妨對 GC 未來的改進拭目以待。

    推薦閱讀

    【Why golang garbage-collector not implement Generational and Compact gc?】https://groups.google.com/forum/#!msg/golang-nuts/KJiyv2mV2pU/wdBUH1mHCAAJ

    【寫一個內存分配器】http://dmitrysoshnikov.com/compilers/writing-a-memory-allocator/#more-3590

    【觀察 GC】https://www.ardanlabs.com/blog/2019/05/garbage-collection-in-go-part2-gctraces.html

    【煎魚 Go debug】https://segmentfault.com/a/1190000020255157

    【煎魚 go tool trace】https://eddycjy.gitbook.io/golang/di-9-ke-gong-ju/go-tool-trace

    【trace 講解】https://www.itcodemonkey.com/article/5419.html

    【An Introduction to go tool trace】https://about.sourcegraph.com/go/an-introduction-to-go-tool-trace-rhys-hiltner

    【http pprof 官方文檔】https://golang.org/pkg/net/http/pprof/

    【runtime pprof 官方文檔】https://golang.org/pkg/runtime/pprof/

    【trace 官方文檔】https://golang.org/pkg/runtime/trace/


    下面是彩蛋時間:

    新建立了一個免費的知識星球,大家可以在這里分享? Go 相關的面試、筆試經驗,提出Go 相關的問題,分享 Go 相關的文章等等。

    我也會邀請一些業界大佬來分享經驗,解答問題。

    當然,星球不會僅限于 Go,也不僅限于技術,任何能幫助大家在職場中成長的內容都歡迎分享。

    最重要的是希望大家都能得到成長!成為更好的自己!

    歐神和多位大佬已經在星球等你了,你不來嗎?



    總結

    以上是生活随笔為你收集整理的Go GC 20 问的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    天天翘av | 国产精品入口传媒 | 日韩激情小视频 | 亚洲 综合 精品 | 欧美精品一区二区在线观看 | 国产三级在线播放 | 在线影视 一区 二区 三区 | av性网站 | 精品视频专区 | 日韩a在线播放 | 视频国产在线观看18 | 欧美日韩免费视频 | 最近在线中文字幕 | 91精品国自产在线观看欧美 | 波多野结衣一区二区 | 精品亚洲欧美一区 | 青青久草在线 | 国产一级免费在线 | 久久久久久久久久久综合 | 久久精品人 | 国产精品久久久久久五月尺 | 狠狠干狠狠操 | 激情五月亚洲 | 99精品免费久久久久久久久日本 | 国产精品永久免费在线 | 久久激情五月激情 | av久久在线 | 亚洲 欧美变态 另类 综合 | 久久se视频| 亚洲激情视频在线 | 波多野结衣在线播放视频 | 成年人在线视频观看 | 亚洲欧洲一区二区在线观看 | 九色精品在线 | 国产成人久久精品亚洲 | 人人爱人人爽 | 成人国产精品一区二区 | 中文字幕一区二区三区在线观看 | 久久天天躁夜夜躁狠狠躁2022 | 亚洲精品乱码久久久久v最新版 | 女人18片毛片90分钟 | 超碰在线个人 | 国产精品99久久久久久宅男 | 国产精品 中文字幕 亚洲 欧美 | 韩国精品在线观看 | 亚洲 欧洲av | 国产视频一区二区在线播放 | 91网站观看 | 91精品人成在线观看 | 在线视频观看成人 | av官网在线| 91麻豆免费看 | 午夜在线观看一区 | 亚洲毛片久久 | 色插综合 | 国产免费观看av | 久久久久久久久久久久久影院 | 夜色资源站国产www在线视频 | 亚洲人成网站精品片在线观看 | 久二影院 | 中文字幕 婷婷 | se婷婷| 欧美日韩国产一二三区 | 96国产在线 | 久久久久久久久久免费视频 | 黄毛片在线观看 | 国产在线观看二区 | 91麻豆操 | 中文字幕中文字幕在线中文字幕三区 | 国产不卡免费av | 免费高清国产 | 韩国av在线播放 | 欧美亚洲一区二区在线 | 欧美日韩在线免费视频 | 黄色在线免费观看网站 | 国产一区二区在线影院 | 久久精品一区二区三区中文字幕 | 国产精品高清免费在线观看 | 99精品在线免费观看 | 高清国产午夜精品久久久久久 | av性网站 | 97av色| 久久免费精品视频 | 久草新在线 | 人人澡超碰碰 | 精品在线视频观看 | 日韩高清片 | 爱爱av在线 | 成 人 黄 色 视频 免费观看 | 婷婷综合成人 | 日日夜夜免费精品 | av不卡在线看 | av经典在线 | 久久久久久国产精品 | www.com在线观看 | 久久精品精品电影网 | www.婷婷色 | 国产青春久久久国产毛片 | 久久久麻豆视频 | 天天干天天射天天插 | 热久久这里只有精品 | 亚洲精品中文字幕视频 | 日韩精品一区不卡 | 国产美腿白丝袜足在线av | 日韩精品中文字幕在线不卡尤物 | 视频在线国产 | 国产女人免费看a级丨片 | 在线视频欧美精品 | 欧美日韩不卡一区二区 | 天天干天天干天天干天天干天天干天天干 | 97日日碰人人模人人澡分享吧 | 亚洲最快最全在线视频 | 欧洲av在线 | 日韩精品欧美一区 | 日韩美在线 | 麻豆视频91 | 中文字幕观看视频 | 深夜国产在线 | 中文字幕 影院 | 国产视频2| 久草视频在线资源站 | 四虎成人av| 中文字幕在线观看一区二区三区 | 91九色视频在线观看 | 高清久久久久久 | 婷婷久久综合九色综合 | 国产精品18毛片一区二区 | 99这里都是精品 | 国产成人精品一区二区三区福利 | 国产精品美 | 精品久久久久久久久久久久久 | 狠狠干狠狠色 | 91免费高清| 中文字幕视频网站 | 成人影音av| 五月天婷婷视频 | 国产成人精品一区二三区 | 黄色精品一区二区 | 最新在线你懂的 | 国产精品99在线播放 | 国产毛片久久 | 久久精品综合一区 | 男女精品久久 | 久久久亚洲电影 | 日韩欧美一区二区三区黑寡妇 | 中文字幕一区二区三 | 色综合久久久久 | 欧美色图狠狠干 | 五月天网站在线 | 四虎国产 | 少妇自拍av | 四虎在线免费观看 | 久久久综合九色合综国产精品 | 九草视频在线观看 | 五月婷久久 | 久久免费视频这里只有精品 | 久久dvd| 久久久三级视频 | 亚洲精品高清一区二区三区四区 | 欧美 日韩 成人 | 欧美在线一二区 | 波多野结衣理论片 | 91在线国内视频 | 国产在线中文 | 免费人成在线观看网站 | 国产一区二区在线播放视频 | www色 | 精品久久久久久久久久久久 | 欧美大片aaa | 日韩网站一区二区 | 免费观看91视频大全 | 激情五月婷婷综合网 | 日日夜夜噜| 97在线观看免费观看高清 | av一级片网站 | 日韩精品在线视频免费观看 | 久久精品3 | 五月天中文字幕mv在线 | 在线播放国产一区二区三区 | 午夜av免费 | 日韩精品一区二区三区丰满 | av中文字幕av | 免费视频一区 | 国产精品入口传媒 | 欧美在线一二区 | 中文字幕在线日亚洲9 | 国内精品国产三级国产aⅴ久 | 精品福利视频在线观看 | 午夜私人影院 | 在线看成人 | 国产一区免费视频 | 欧美一进一出抽搐大尺度视频 | 国产另类av | 国产黄色精品视频 | 国产亚洲精品久 | 国产网站在线免费观看 | 一区 在线 影院 | 欧洲成人av| 久久免费高清视频 | 欧美污污网站 | 天天插伊人| 日韩mv欧美mv国产精品 | 很黄很污的视频网站 | 久久美女精品 | 在线观看免费一级片 | 91精品国产高清自在线观看 | 久久无码av一区二区三区电影网 | 日韩在线视频精品 | 亚洲香蕉视频 | 国产精品国内免费一区二区三区 | 91av视频观看| 色天天综合网 | 午夜精品福利在线 | 精品国产一区二区三区噜噜噜 | 在线观看网站av | 97视频网址 | a电影在线观看 | 精品在线免费观看 | 欧美大片第1页 | av不卡中文 | 六月丁香六月婷婷 | 久久免费毛片视频 | 日韩理论视频 | 亚洲天堂香蕉 | 国产理论免费 | 欧美精品免费在线观看 | 日韩三级免费观看 | 亚洲理论片| 我爱av激情网 | 欧美午夜一区二区福利视频 | 国产清纯在线 | 四虎免费在线观看视频 | 国产99久久精品一区二区永久免费 | 久久国语 | 99精品福利视频 | 久久久久欧美精品 | 免费看wwwwwwwwwww的视频 久久久久久99精品 91中文字幕视频 | 视频在线观看一区 | 亚洲狠狠丁香婷婷综合久久久 | 国产精品99久久久久久久久久久久 | 中文字幕一区在线 | 一区二区视频在线播放 | 9色在线视频| 午夜美女福利直播 | 久久成人国产精品 | 国内精品久久久久久久久久 | 久精品视频在线 | 在线观看视频99 | 三级黄色a| 亚洲欧美国产日韩在线观看 | 国产精品一区二区三区在线播放 | 国产视频中文字幕 | 久久亚洲影视 | 深夜免费小视频 | 五月婷婷综合在线观看 | 99在线热播精品免费 | 最新日韩在线 | 婷婷色 亚洲 | 欧美日韩久久不卡 | 国产综合香蕉五月婷在线 | 人人澡人人模 | 91亚洲国产成人 | 久久理论电影网 | 在线视频99 | 色www精品视频在线观看 | 欧美成人精品欧美一级乱 | 国内免费久久久久久久久久久 | 韩日av一区二区 | 色婷婷啪啪免费在线电影观看 | 欧美午夜a | 欧美伦理一区二区三区 | 国产丝袜美腿在线 | 狠狠操狠狠干2017 | 日韩国产高清在线 | 国产午夜精品福利视频 | 国产成人av一区二区三区在线观看 | 日韩中文久久 | 成年人免费电影在线观看 | 午夜久久久影院 | 婷婷四房综合激情五月 | 麻豆影视在线免费观看 | 中文字幕亚洲欧美日韩2019 | 天天干 天天摸 天天操 | 天天干人人干 | 亚洲精品播放 | 久久久久久久99精品免费观看 | jizz欧美性9 国产一区高清在线观看 | 人人要人人澡人人爽人人dvd | www日韩欧美 | 色综合天天综合在线视频 | 免费精品 | 久久狠狠亚洲综合 | 少妇高潮冒白浆 | 久久免费视频4 | 天天曰天天干 | 91成人在线看 | av怡红院 | 国产精品激情偷乱一区二区∴ | 日韩理论片中文字幕 | 97超碰在线人人 | 81国产精品久久久久久久久久 | 免费久久99精品国产婷婷六月 | 深爱激情五月综合 | 精品一区二三区 | 日韩理论片中文字幕 | 毛片网在线 | 国产护士在线 | 成年人黄色免费看 | 久久观看 | 99精品国产福利在线观看免费 | 天天天射 | 日韩av影视在线 | 精壮的侍卫呻吟h | 国产免码va在线观看免费 | 西西www4444大胆在线 | 91探花在线视频 | 精品在线视频一区二区三区 | 国产精品久久久久av免费 | 人人舔人人爽 | 黄色成人在线观看 | 91精品国产自产在线观看 | 国产福利91精品一区 | 欧美一区二区视频97 | 91九色视频导航 | 精品久久久网 | 久久久久在线视频 | 天天爽夜夜爽精品视频婷婷 | 天天干天天操天天干 | 国产精品丝袜久久久久久久不卡 | 中文字幕免费观看 | 国产一区二区在线视频观看 | 精品一区免费 | 日韩精品视频免费专区在线播放 | 在线观看你懂的网站 | 日韩超碰在线 | 国产成人av免费在线观看 | 2024av| 51久久成人国产精品麻豆 | 亚洲精品久久在线 | 国产精品久一 | 韩国av一区二区三区 | 91九色在线观看视频 | 日韩av午夜在线观看 | 天天射射天天 | 国产精品成人免费一区久久羞羞 | 欧美a级成人淫片免费看 | 欧美日韩一区三区 | 日韩国产欧美在线视频 | 国产精品一区二区免费视频 | 午夜12点 | 99操视频 | 美女视频黄免费网站 | 久久久久免费精品 | 玖玖色在线观看 | 国产精品久久久久久久久久ktv | 国产视频网站在线观看 | 激情伊人五月天久久综合 | 国产三级久久久 | 色午夜| 国内精品久久久久影院优 | 成人av教育 | 国产小视频免费在线观看 | 成年人免费观看在线视频 | 黄色在线网站噜噜噜 | 日韩中文字幕免费电影 | 美女黄濒| 国产视频综合在线 | 88av色 | 黄色一级免费电影 | 亚洲 欧洲av | 天天曰| 免费成人av网站 | 日韩在线大片 | 国产美女精品视频免费观看 | 美女黄频在线观看 | 四虎成人av | 国产精品久久久久久久久久久久 | 国产伦理精品一区二区 | 91大神电影| 99热在线国产 | 国产伦理久久精品久久久久_ | 亚洲va在线va天堂va偷拍 | 麻豆视频观看 | 久久黄色免费观看 | 天堂av在线免费 | 亚洲精品www. | 亚洲aⅴ乱码精品成人区 | 国产伦精品一区二区三区在线 | 亚洲欧美日本一区二区三区 | 午夜资源站 | 又黄又刺激视频 | 日韩欧美xxxx | 欧美日韩99| 日韩在线在线 | 色综合久久88色综合天天人守婷 | 最新超碰 | 日韩在线二区 | 免费国产一区二区视频 | 91亚洲精品国偷拍 | 91成人在线看 | 欧美激情精品久久久久 | 在线观看精品视频 | 91黄视频在线 | 中文字幕永久免费 | 人人看人人 | 美女视频免费精品 | 国产美女被啪进深处喷白浆视频 | 国产精品第52页 | 精品一区精品二区高清 | 狠狠色丁香婷婷综合欧美 | 婷婷社区五月天 | 亚洲成a人片在线观看中文 中文字幕在线视频第一页 狠狠色丁香婷婷综合 | 樱空桃av | 97精品视频在线播放 | av一区二区三区在线观看 | 中文字幕观看在线 | 久久经典视频 | 天天射天天爱天天干 | 亚洲国产一区二区精品专区 | 九九视频免费在线观看 | 四虎影视精品成人 | 一色屋精品视频在线观看 | 99热这里只有精品国产首页 | 欧美日韩精品二区第二页 | 草草草影院 | 色综合天天狠天天透天天伊人 | 福利久久 | 2017狠狠干 | 日韩三级免费观看 | 在线日本看片免费人成视久网 | 国产精品v欧美精品 | 久久精品视频在线看 | 天天操网址 | 日韩欧美视频一区二区三区 | 午夜久久久久久久 | 手机在线观看国产精品 | 日韩成人精品在线观看 | 日韩一级黄色av | 天天天干天天射天天天操 | 黄色三级久久 | 久久草精品 | 久av电影 | 色91av| 国产精品久久久久999 | 在线国产高清 | 成人av在线影视 | 中文字幕在线观看亚洲 | 丁香导航 | 久久久亚洲网站 | 国产免费黄色 | 玖草在线观看 | 久久久99精品免费观看 | 人人爽人人爱 | 久久亚洲福利视频 | 国产成人精品av久久 | 国产精品毛片久久久久久久 | 精品国产一区二区三区免费 | 亚洲涩涩涩涩涩涩 | 久精品视频在线观看 | 色爽网站 | 欧美福利在线播放 | 国产精品久久久久久久久久久久午夜片 | 韩日精品在线 | 超碰免费av | 在线精品国产 | 99久久99久久精品国产片果冰 | 日日干 天天干 | 日韩欧美一区二区三区在线观看 | 国产精品视频专区 | 婷婷视频在线播放 | 国产一级免费视频 | 日韩精品一区二区三区免费观看视频 | 婷婷激情综合五月天 | 久久黄色网页 | 久久免费在线观看 | 中文资源在线观看 | 美女久久久久久久久久 | 中文字幕资源网在线观看 | 天天草天天爽 | 91视频在线看 | 奇米影视四色8888 | 亚洲色图27p | 天天操天天色天天射 | 国产一级久久 | 成人资源站| 亚洲视频久久久久 | 久久精品2 | 亚洲一区久久 | 波多野结衣在线视频免费观看 | 亚洲精品自拍 | 国产精品高潮久久av | 亚洲电影第一页av | 中文字幕在线观看视频免费 | 中文区中文字幕免费看 | 久久久资源网 | 91av视频免费观看 | 亚洲一级在线观看 | 婷婷久草 | 国产精久久久久久妇女av | www日韩精品 | 欧美做受高潮电影o | 99在线精品视频观看 | 中文在线字幕免费观看 | 国产精品成人久久久久 | 亚洲成年人免费网站 | 日韩中文三级 | 手机av观看| 高清国产午夜精品久久久久久 | 国产精品原创 | 超碰在线98| 美女网站视频免费都是黄 | 日韩91av| 亚洲成人免费观看 | 亚洲精品麻豆 | 国产一区二区在线播放视频 | 一区二区三区中文字幕在线观看 | 一区二区三区动漫 | 国产理论片在线观看 | 激情综合色综合久久 | 婷婷久久久久 | 国产成人精品综合 | 国产一级淫片免费看 | 精品亚洲男同gayvideo网站 | 欧美性粗大hdvideo | 日本女人在线观看 | 97精品国产97久久久久久 | 特级毛片网站 | 少妇性xxx| av电影在线免费 | 91精品国产欧美一区二区成人 | www.狠狠色.com | 久久这里只有精品1 | 久久精品屋 | 永久免费看av | 91精彩在线视频 | 免费观看91视频大全 | 久久电影国产免费久久电影 | 婷婷丁香六月天 | 久综合网 | 99视频在线免费观看 | 日韩在线电影观看 | 国产精品九色 | 亚洲欧美婷婷六月色综合 | 午夜久久久久久久久久影院 | 在线免费观看麻豆 | 亚洲黄色在线观看 | 精品视频免费久久久看 | 亚洲v欧美v国产v在线观看 | av免费在线播放 | 久久久久国产精品一区二区 | 国产精品一区二区无线 | 人人干网站 | 久久人人爽人人片av | 一区二区三区免费在线观看 | 97在线观看视频 | 在线观看黄色免费视频 | 91九色蝌蚪视频网站 | 福利一区二区 | 91精品毛片 | 看片在线亚洲 | 国产在线观看你懂的 | 99理论片 | 91精彩在线视频 | 在线中文字幕视频 | 亚洲国产97在线精品一区 | 激情久久久久 | 黄色在线观看污 | 在线观看成人福利 | 久久y| 久章草在线 | 婷婷亚洲综合 | 亚洲精品在线视频观看 | 久久小视频 | 婷婷色网视频在线播放 | 黄色一级大片在线观看 | 人人爽人人片 | 中文字幕第一页在线视频 | 黄色一级大片在线免费看国产一 | 精品在线你懂的 | 91av在线电影 | 亚州精品在线视频 | 欧洲精品二区 | 午夜成人影视 | 国产亚洲成人精品 | 日韩一区二区三区免费视频 | 中文亚洲欧美日韩 | 亚洲精品午夜久久久久久久 | 久久网页| 国产精品久久久久一区二区三区 | 婷婷伊人五月天 | www.亚洲在线 | 成年人在线免费看视频 | 国产精品2018 | a黄色片在线观看 | 中文字幕日韩精品有码视频 | 久久99久国产精品黄毛片入口 | 日日干天天插 | 91伊人久久大香线蕉蜜芽人口 | 99视频久| 久久亚洲成人网 | av黄色一级片| 丁香久久婷婷 | 激情婷婷综合网 | 国产精品 日韩 欧美 | 免费久久99精品国产婷婷六月 | 久草www| www.夜色321.com | 国产精品九九九 | 午夜视频欧美 | 久久久国产99久久国产一 | 日韩av一区二区在线影视 | 天天爽夜夜爽精品视频婷婷 | www.av免费观看 | 欧美精品久久久久久 | 久久综合毛片 | 伊人色综合网 | 天天艹天天干天天 | 国产xxxx性hd极品 | 伊人色综合久久天天 | 国产精品大片在线观看 | 婷婷国产v亚洲v欧美久久 | 97成人精品 | 国产在线观看91 | 国产成人精品一区二区三区免费 | 亚洲精品乱码久久久久久久久久 | 国产最新精品视频 | 超碰伊人网 | 久久久久久久久福利 | 久久看视频 | 精品国产中文字幕 | 中文字幕中文字幕在线中文字幕三区 | 国产尤物在线 | 成年人免费在线观看网站 | 中国老女人日b | 狠狠干成人 | 日本中文字幕在线一区 | 日韩三级视频 | 日本99热| 黄色一级大片在线免费看国产一 | 久久一级片 | 国产精品久久久久久一区二区 | 日韩中文字幕在线观看 | 亚洲五月激情 | 狠狠狠色丁香综合久久天下网 | 91亚洲精品久久久久图片蜜桃 | 欧美a级成人淫片免费看 | 亚洲视频网站在线观看 | 久久这里只有精品1 | 韩国一区视频 | 国产精品18久久久久白浆 | 免费在线国产精品 | 色婷婷激情网 | 亚洲精品资源在线观看 | 国产精品白浆 | 三级av免费 | 欧美日韩国产一区二区在线观看 | 日韩av一区在线观看 | 日产中文字幕 | 国产福利一区二区在线 | 在线免费视频一区 | 丝袜制服天堂 | 欧美日韩在线播放 | 91女子私密保健养生少妇 | 久久99热精品 | 欧美一区二区在线免费看 | 在线观看中文av | 久久99精品久久久久久清纯直播 | 二区中文字幕 | 伊人激情综合 | 91成年人网站 | 丰满少妇麻豆av | 久99久在线 | 91夜夜夜 | 免费又黄又爽的视频 | 在线观看成人小视频 | 久久99久久99 | 午夜久久| 久久小视频 | 欧美在线观看禁18 | 国产一级二级三级在线观看 | 天天干天天操av | 六月丁香婷婷久久 | 四虎影视www| 国产精品原创av片国产免费 | 中文久草 | 久久久久婷 | 久久久.com | 99电影456麻豆| 久久久这里有精品 | 九九精品视频在线看 | 日韩电影中文字幕在线 | 在线观看免费av网站 | 成人av电影网址 | 久久色视频 | av在线永久免费观看 | 免费精品国产 | 成人免费视频网 | 五月婷婷av在线 | 亚洲精品乱码白浆高清久久久久久 | 亚洲精品1区2区3区 超碰成人网 | 日韩在线观看小视频 | 久久夜靖品 | 九九久久免费 | 97国产精品久久 | 欧美精品久久久久久 | 国产精品视频免费看 | 亚洲成人精品av | 精品久久网 | 男女日麻批 | 在线免费视频一区 | 亚洲乱码精品久久久久 | 成年人免费观看在线视频 | 亚洲一区二区三区毛片 | 久久精品国产v日韩v亚洲 | 日韩中文字幕在线不卡 | 久久精品系列 | 激情欧美xxxx | 亚洲精品乱码久久久久 | 三上悠亚一区二区在线观看 | 亚洲成人资源在线观看 | 91久久偷偷做嫩草影院 | 久久成人久久 | 国产精品久久9 | 久久久久婷 | 日本久久91 | 一区二区三区动漫 | 国产精品无 | 欧美一级免费高清 | 免费看国产一级片 | 成人国产精品入口 | 色在线网站 | 亚洲最新合集 | 玖玖国产精品视频 | 国产精品一区二区久久精品爱涩 | 黄色a在线 | av成人在线播放 | 欧美成人播放 | 99精品在线视频播放 | 粉嫩av一区二区三区四区 | 碰超在线97人人 | 操操操人人 | 日韩二区三区 | 亚洲不卡av一区二区三区 | 国产美女免费 | 亚洲天天看 | 欧美久久久久久久久久久久久 | 亚洲高清av | 永久免费精品视频 | 久爱综合 | 中文字幕色播 | 天天操天天干天天摸 | 国产亚洲精品女人久久久久久 | 亚洲在线黄色 | 二区精品视频 | 久久久精品视频网站 | 中文字幕免费看 | 国产精品久久一区二区无卡 | 日精品在线观看 | a级片在线播放 | 久久乱码卡一卡2卡三卡四 五月婷婷久 | 日韩免费看视频 | av黄色一级片 | 午夜美女av | 香蕉影视| 国产精品久久久久久久久蜜臀 | 三级av在线免费观看 | 精品国产诱惑 | 中文av在线播放 | 国产一级免费播放 | 国产欧美精品一区二区三区四区 | 亚洲激情精品 | 91激情视频在线观看 | 免费在线观看成人av | 亚州欧美精品 | 91天堂在线观看 | 狠狠操精品 | 天天干天天操天天爱 | 中文字幕中文字幕在线中文字幕三区 | 欧美日韩性 | a午夜在线| 天天躁天天操 | 免费福利视频网站 | 国产区精品在线 | 国产成人久久av977小说 | 精品亚洲免a | 久久99国产精品免费 | 亚洲国产人午在线一二区 | 天天干 天天摸 天天操 | 91视视频在线直接观看在线看网页在线看 | 欧美另类z0zx | 亚洲国产一区二区精品专区 | 五月婷婷六月综合 | 亚洲成av片人久久久 | 天天综合天天做天天综合 | 国产午夜精品一区二区三区欧美 | 2022中文字幕在线观看 | 国产视频亚洲视频 | 色播五月激情五月 | av综合 日韩| 97超碰免费在线观看 | 91av手机在线观看 | 日韩视频www | 久久久在线视频 | 久久久久久久久久久网站 | 色资源在线 | 夜色在线资源 | 成年人免费av | 人人澡人摸人人添学生av | 一区av在线播放 | 国产视频精品久久 | 日韩精品一区二区三区水蜜桃 | 国产视频综合在线 | 激情网第四色 | 一二区av| 免费在线观看国产黄 | 欧美无极色 | 日韩久久久久久久久久久久 | 亚洲乱亚洲乱亚洲 | 久草精品在线 | 91私密保健 | 成人精品久久久 | 中文在线字幕观看电影 | 狠狠干在线播放 | 日日草天天干 | 中文字幕资源站 | 免费亚洲婷婷 | 99久久久久国产精品免费 | 国产精品美女久久久久久久久久久 | 成人午夜精品久久久久久久3d | 国产精品久久久久久麻豆一区 | 国产不卡在线 | 久久综合久久久久88 | 97在线观视频免费观看 | 国产精品美女久久久久久久久久久 | 久久深夜福利免费观看 | 毛片在线播放网址 | 欧美午夜精品久久久久 | 日韩中文免费视频 | 亚洲精品动漫久久久久 | 国产成人一区二区三区免费看 | 在线观看一 | 免费黄色a网站 | 成人在线观看资源 | 91资源在线 | 在线免费观看av网站 | 日本在线视频一区二区三区 | 亚洲日本在线一区 | 久久久久久99精品 | 播五月婷婷 | 久久免费99 | 在线观看视频一区二区三区 | 婷婷国产v亚洲v欧美久久 | 精品天堂av | 午夜av免费在线观看 | 午夜精品一二区 | 国产亚洲精品久久久久久久久久 | 婷婷六月中文字幕 | 国产特级毛片aaaaaaa高清 | 99精品欧美一区二区三区黑人哦 | 欧美性精品| 777xxx欧美 | 国产资源在线观看 | 欧美国产亚洲精品久久久8v | 在线午夜| 国产成人精品免高潮在线观看 | 国产精品普通话 | 欧美久久久久久久 | 91九色自拍 | 手机av在线网站 | 久久视频在线观看免费 | 免费黄色小网站 | 成人黄色小说视频 | 中文字幕在线观看视频网站 | 亚洲成aⅴ人在线观看 | 久久99精品久久只有精品 | 一区二区三区免费在线观看视频 | 狠狠色丁香久久婷婷综合五月 | 日韩中文字幕一区 | 手机在线视频福利 | 久草在线观 | 丁香六月婷婷 | 麻豆免费看片 | 国产一区电影在线观看 | 国产大尺度视频 | 高清免费在线视频 | 国产黄色片在线免费观看 | 丁香六月在线观看 | 在线视频 影院 | 欧美超碰在线 | 久久久久久久久毛片 | 久久欧美在线电影 | 国产区久久 | 日本丶国产丶欧美色综合 | 黄色成人影视 | 一级一级一片免费 | 中文字幕在线观看你懂的 | 五月天综合 | 很污的网站| 欧美一级电影在线观看 | 超碰在线人人艹 | 亚洲综合小说电影qvod | a级免费观看 | 国产蜜臀av | 久久久久久久久久久久久久av | 中国一级片免费看 | av不卡在线看 | 97成人在线观看 | 成年人视频在线免费 | 日韩免费视频播放 | 亚洲精品免费在线播放 | 日韩av在线影视 | 国产精品av电影 | 色婷婷色| 国产免费资源 | 午夜精品一区二区国产 | 亚洲精品一区二区久 | 麻豆91小视频 | 久久在视频| 久久综合狠狠综合久久综合88 | 日本三级香港三级人妇99 | 天天做天天爱天天综合网 | 国内精品视频在线播放 | 中文一区二区三区在线观看 | 69精品久久久 | 视频在线国产 | 91视频这里只有精品 | 97色资源| www夜夜操 | 81精品国产乱码久久久久久 | 九九热精品国产 | 日韩精品视频在线观看免费 | 日日干日日色 | 天天综合网天天综合色 | 色婷婷五 | 久草在线99 | 香蕉影院在线观看 | 亚洲欧美综合精品久久成人 | 国产精品久久99综合免费观看尤物 | 天天插天天射 | 免费成人在线观看 | 九九有精品 | 午夜久久久久久久久久影院 | 高潮久久久 | 91精品国产乱码在线观看 | 97视频人人澡人人爽 | 午夜精品久久久久久久99 | 一级欧美黄 | 久久不射电影院 | 97精品在线视频 | 天堂av高清 | 美女精品国产 | 天天操天天色天天射 | 在线视频18在线视频4k | www日| 国产蜜臀av | 亚洲第一久久久 | 亚洲视频分类 | 99精品在线观看视频 | 热re99久久精品国产66热 | 亚洲一区久久久 | 国产精品久久久一区二区 | 91私密视频 | 9草在线 | www免费网站在线观看 | 久久电影日韩 | 一区二区三区四区免费视频 | 久久精品91视频 | 91精品欧美一区二区三区 | 日韩免费小视频 | 夜色资源站国产www在线视频 | 九九精品在线观看 | 日本久久成人 | 在线国产精品视频 | 免费日韩一区二区 | 久久天天躁狠狠躁亚洲综合公司 | 黄色国产在线观看 | 国产精品麻 | 久久精品99国产精品亚洲最刺激 | 91精品久久久久久久久久久久久 | 日韩精品免费在线观看 | 性色av一区二区三区在线观看 | 在线观看成人网 | 久久国产综合视频 | 久久精品精品 | 五月婷婷开心中文字幕 | av免费网页| 成人av在线网 | 日韩午夜视频在线观看 | 亚洲国产人午在线一二区 | 激情婷婷在线 | 98超碰在线观看 | 成人午夜电影网站 | 人人爱天天操 | 国产日产欧美在线观看 | 色干综合 | 岛国av在线不卡 | 成人av.com | 六月丁香综合网 |