精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

我所理解的 Go 的 GC (Garbage Collection) 垃圾回收機制

開發 前端
Green Tea 的原型主要針對 小對象 span 。這是因為小對象的掃描本身耗時很短,傳統 GC 為每個小對象進行獨立調度和元數據訪問的開銷占比更高,因此從按塊掃描中獲益最大。較大的對象則可能繼續使用原有的掃描算法。

Go 語言(Golang)作為一款內置運行時的現代編程語言,其垃圾回收(Garbage Collection, GC)機制是開發者理解其性能和行為的關鍵一環。要深入理解 Go 的 GC,我們首先需要明確垃圾回收的核心任務是什么,以及它在設計上需要面對哪些權衡與博弈。

在主流的編程語言內存模型中,程序運行時使用到的內存通常可以劃分為幾個區域,其中最主要的是靜態數據區、棧(stack)和堆(heap)。 棧內存 的管理相對簡單:當一個函數被調用時,系統會為其分配一個棧幀(stack frame),用于存儲局部變量、參數以及函數返回地址等信息;當函數執行完畢并返回時,其對應的棧幀會被自動銷毀,所占用的內存也隨之釋放。這種后進先出(LIFO)的管理方式使得棧上數據的生命周期與函數調用周期緊密綁定,無需開發者手動干預。

然而,并非所有數據都適合存放在棧上。對于那些需要在函數調用結束后依然存在,或者大小在編譯期難以確定的數據,通常會在 堆內存 中分配。對于像 C/C++ 這類沒有內置垃圾回收機制的語言,開發者需要顯式地使用如 malloc 這樣的函數向操作系統申請堆內存,并且在不再需要時通過 free 函數手動釋放。如果忘記釋放,就會導致 內存泄漏 (memory leak),即程序持續占用不再使用的內存,最終可能耗盡系統資源。反之,如果在內存釋放后繼續嘗試訪問它(懸垂指針),則可能導致程序崩潰或未定義行為,即 釋放后使用 (use-after-free)錯誤。

在 Go 語言中,開發者通常不需要像 C/C++ 那樣顯式地“申請”和“釋放”內存來管理對象的生命周期,盡管 Go 提供了如 new 關鍵字(用于分配零值的內存并返回指針)和 make 函數(用于初始化切片、映射和通道等內建類型)來進行內存分配。當這些分配發生在堆上,或者當變量因為 逃逸分析 (escape analysis)——一種編譯器優化技術,用于決定變量是分配在棧上還是堆上——而被分配到堆上時,這些對象的生命周期管理便不再由棧的自動機制控制。

這些脫離了棧作用域控制的堆上對象,其內存何時以及如何被回收,就成了 GC 的核心職責。一個優秀的 GC 機制需要在多個維度上進行優化:

  • 高效檢測 :如何快速準確地識別哪些內存是不再被使用的“垃圾”?
  • 回收時機與頻率 :過于頻繁的垃圾回收會中斷業務邏輯的執行,影響程序性能(吞吐量和延遲);而不頻繁的回收則可能導致不再使用的內存長時間累積,造成內存膨脹,甚至引發內存溢出(Out Of Memory, OOM)。
  • 內存碎片 (memory fragmentation):反復分配和釋放不同大小的內存塊可能導致堆中出現許多不連續的小塊空閑內存,這些碎片雖然總量可能不少,但難以滿足較大的內存分配請求。如何減少內存碎片,提高內存利用率,也是 GC 需要考量的。

本文將圍繞 Go 語言的 GC 設計展開討論,力求在過程中逐步厘清上述問題,并理解 Go 如何在這些挑戰中取得平衡。

Go GC 總體設計

通用的垃圾回收原理主要可以歸納為兩大類:

第一類是基于 引用計數 (reference counting)的機制。這種方法為每個對象維護一個計數器,記錄有多少個引用指向該對象。當一個新的引用指向對象時,計數器加一;當一個引用被移除時,計數器減一。一旦對象的引用計數變為零,就表明該對象不再被任何部分使用,可以立即被回收。Python 語言的 GC 機制中就包含了引用計數,C++ 的智能指針 std::shared_ptr 也是基于這一原理。引用計數的主要優點是對象可以在不再被引用的那一刻立即被回收,內存管理及時。然而,它也存在一些顯著的缺點:一是頻繁更新引用計數會帶來額外的運行時開銷;二是它難以處理 循環引用 (circular references)的問題,即一組對象互相引用,即使它們整體已經與程序的其他部分隔離(即成為垃圾),但它們的引用計數都大于零,導致無法被回收。為了解決循環引用,通常需要引入額外的檢測機制,如 C++ 中的 std::weak_ptr(弱指針)。

第二類是基于 可達性分析 (reachability analysis)的追蹤式垃圾回收(tracing garbage collection)。這類 GC 的核心思想是“可達性即存活性”:程序從一組固定的 根對象 (roots)——通常包括全局變量、當前活躍的函數調用棧上的局部變量和參數,以及 CPU 寄存器中的指針等——開始,沿著指針引用關系遍歷內存中的對象圖。所有從根對象出發能夠訪問到的對象都被認為是“活”對象,其余未被訪問到的對象則被視為“垃圾”,可以被回收。Go 語言的 GC 正是采用了基于可達性分析的并發標記清掃(concurrent mark-and-sweep)算法。

那么,Go 語言是如何基于可達性分析的假設,高效且正確地標記并清掃不再使用的內存呢?這需要我們先了解一些 Go 內存管理和 GC 相關的基礎概念。

在進一步探討 Go GC 的具體流程和技術細節之前(假設讀者對 Go 的 GPM 調度模型已有基本了解),我們先介紹一些核心的術語和組件:

  • 頁(page) :在 Go 的內存管理中,操作系統級別的內存頁(通常為 4KB 或 8KB)是內存分配的最小單位之一。Go 的運行時會向操作系統申請大塊內存,然后將這些大塊內存劃分為更小的、固定大小的頁進行內部管理。Go 自身的內存管理系統通常使用 8KB 大小的頁。
  • span (mspan) :mspan 是 Go 內存管理的核心數據結構,代表了一段連續的頁。一個 mspan 可以用來存儲特定 大小類 (size class)的多個小對象,或者一個單獨的大對象。mspan 內部維護了關于其所含對象的重要元數據,例如對象的分配狀態、存活狀態(用于 GC 標記)等。
  • 大小類(size class) :為了高效管理不同大小的對象分配并減少內部碎片,Go 將小對象(通常指小于 32KB 的對象)歸入不同的固定大小等級,即大小類。每個 mspan 通常服務于一種特定的大小類,這意味著它內部所有可分配的槽位(slot)都是同樣大小的。
  • mcache :mcache 是一個與每個 P(Processor,對應于 GPM 模型中的 P,代表一個邏輯處理器,用于執行 goroutine)相關聯的本地內存緩存。它為當前 P 上運行的 goroutine 提供快速的小對象分配服務。mcache 中包含各種大小類的 mspan 列表,由于是 P 本地緩存,從 mcache 分配內存通常不需要加鎖,極大地提高了并發分配的效率。
  • mcentral :mcentral 是一個全局的、按大小類組織的數據結構。每個大小類都有一個對應的 mcentral 實例。當某個 P 的 mcache 中缺少特定大小類的 mspan 時,它會向相應的 mcentral 請求。反之,如果 mcache 中有多余的空閑 mspan,也會歸還給 mcentralmcentral 起到了在不同 P 之間平衡 mspan 資源的作用,它也需要處理鎖來保證并發安全。
  • mheap :mheap 是 Go 程序的全局堆內存管理器。它持有所有從操作系統申請到的內存(這些大塊內存被稱為 arenas ,例如在 64 位系統上一個 arena 可能是 64MB),并將這些內存切分成 mspan 分配給各個 mcentralmheap 負責管理堆的整體大小,決定何時向操作系統申請更多內存或何時將未使用的內存歸還給操作系統。所有的大對象(大于 32KB)直接由 mheap 分配。
  • 根對象(root object) :如前所述,根對象是 GC 開始追蹤對象可達性的起點。在 Go 中,這主要包括全局變量區域中的指針、每個 goroutine 棧(goroutine stack)上指向堆對象的指針,以及一些運行時內部結構的指針。
  • 存活堆(live heap) :指在一輪 GC 標記階段結束后,被確認為仍然存活(即從根對象可達)的所有堆對象的總大小。
  • 堆目標(heapGoal) :heapGoal 是一個動態計算的目標堆大小。當實際的堆內存占用達到或超過 heapGoal 時,就會觸發新一輪的 GC。它的計算通常基于上一輪 GC 結束后的存活堆大小以及一個由環境變量 GOGC(默認為 100)控制的百分比,公式為 heapGoal = liveHeap * (1 + GOGC/100)

基于上述概念,Go 的一輪并發標記清掃 GC 大致可以分為以下幾個主要階段:

首先是 標記準備(Mark Setup) 階段。這是一個短暫的 STW (Stop The World)過程,意味著所有用戶 goroutine 都會被暫停。在此期間,GC 會啟用 寫屏障 (write barrier)——一種在并發標記期間確保數據一致性的關鍵機制,我們稍后會詳細討論。同時,還會進行一些必要的初始化工作,為接下來的并發標記做準備。

接下來是 并發標記(Concurrent Marking) 階段。在這個階段,用戶 goroutine 恢復運行,與 GC 的標記工作并行執行。GC 會從根對象開始,遞歸地遍歷所有可達的對象圖。如果一個對象是可達的,它會被標記。為了提高效率,Go 語言的 goroutine 在進行內存分配時,也可能會被要求輔助 GC 執行一部分標記工作(這被稱為 標記輔助 ,mark assist)。寫屏障在這一階段持續工作,以正確處理用戶 goroutine 在標記過程中對指針的修改。在三色標記法中,對象會從初始的“白色”(未訪問)變為“灰色”(已發現但其引用的對象尚未完全掃描),最終變為“黑色”(已掃描且其所有引用的對象也已掃描或加入掃描隊列)。

標記工作基本完成后,進入 標記終止(Mark Termination) 階段。這同樣是一個 STW 階段,所有用戶 goroutine 再次暫停。GC 會完成所有剩余的標記工作,例如處理一些在并發標記期間被寫屏障記錄下來的指針修改,確保所有存活對象都被正確標記。在此之后,所有未被標記(即仍為“白色”)的對象都被認為是垃圾。

最后是 并發清掃(Concurrent Sweeping) 階段。用戶 goroutine 恢復運行。GC 會在后臺或者在用戶 goroutine 嘗試分配新內存時,逐步回收那些在標記階段被識別為垃圾(白色)的對象所占用的內存空間,將其管理的 mspan 中的對應槽位重新標記為空閑,以便后續分配使用。如果一個 mspan 中的所有對象都被回收,那么整個 mspan 就可以被歸還給 mheap,用于其他目的,甚至可能被歸還給操作系統。

值得注意的是,Go 的 GC 設計目標之一是盡可能縮短 STW 的時間,將大部分工作并發化,以減少對應用程序延遲的影響。關于 STW 的具體細節和其必要性,我們將在后續章節中進一步討論。

三色標記法

為了在并發環境中準確地識別存活對象,Go 的 GC 采用了 三色標記法(Tri-color Marking Algorithm)。這個算法將堆中的對象邏輯上分為三種顏色:

  • 白色(White) :對象初始狀態,表示尚未被 GC 訪問到。在一輪 GC 結束時,所有仍然是白色的對象都被認為是垃圾,將被回收。
  • 灰色(Gray) :對象已被 GC 發現(即從根可達),但其內部的指針(即它所引用的其他對象)尚未被完全掃描。灰色對象被視為一個臨界狀態,它們存在于一個待處理的工作隊列中。
  • 黑色(Black) :對象已被 GC 發現,并且其內部所有指針都已經被掃描完畢(即它引用的對象要么也變成了灰色或黑色,要么是 nil)。黑色對象表示 GC 已經處理完畢,并且在當前 GC 周期內是存活的。

垃圾回收進行“染色”(標記)時,其標記信息(例如三色標記法中的顏色)是針對 mspan 中具體的對象或對象槽位(slot)的。 對于包含小對象的 mspan,它內部有一個位圖(bitmap)或者類似的數據結構,用于記錄每個小對象的標記狀態。因此,雖然 mspan 是頁的集合,但 GC 標記的精度是對象級別的,這些標記信息存儲在 mspan 的元數據中。

GC 的標記過程可以想象成一個從根對象開始向外“染色”的過程:

  1. 初始時,所有對象(除了少量特殊對象)都被認為是白色的。
  2. GC 從所有根對象開始,將它們標記為灰色,并放入一個待掃描的灰色對象集合(工作隊列)中。
  3. GC 從灰色對象集合中取出一個灰色對象,掃描它引用的所有其他對象:

對于每一個它引用的白色對象,將其標記為灰色,并放入灰色對象集合中。

當這個灰色對象的所有引用都被掃描完畢后,該對象自身被標記為黑色。

  1. 重復步驟 3,直到灰色對象集合為空。
  2. 此時,所有仍然是白色的對象就是不可達的垃圾,可以被回收。

那么,為什么需要三種顏色而不是簡單的兩種(例如,白色和黑色)呢?在非并發的 GC 中,兩種顏色確實足夠。但在并發 GC 中,應用程序的 賦值器 (mutator,即用戶 goroutine)會與 GC 的 收集器 (collector)同時運行。我們需要灰色這個中間狀態配合后問提到的寫屏障來保證正確性。

為了防止這種“丟失對象”的情況,三色標記法引入了灰色狀態,并配合 寫屏障 (write barrier)來共同維護一個關鍵的不變性。這個不變性通常是: 不允許黑色對象直接指向白色對象,除非該白色對象能夠通過其他灰色對象間接可達,或者該白色對象本身即將被標記為灰色。 更嚴格地說,Go 的寫屏障努力確保在并發標記期間,不會出現一個黑色對象直接指向一個白色對象,而這個白色對象又沒有其他路徑可以被灰色對象發現。

如果賦值器試圖創建一個從黑色對象指向白色對象的指針,寫屏障就會介入。例如,當執行 objBlack.field = objWhite 這樣的操作時,寫屏障會確保 objWhite(或與其相關的對象)被標記為灰色,從而保證它不會被遺漏。例如,在一個掃描順序可能導致問題的場景中:GC 線程掃描了對象 X 并將其標記為黑色。隨后,用戶 goroutine 修改了 X 的一個字段,使其指向了一個白色對象 Y。如果沒有寫屏障,Y 可能永遠不會被掃描。有了寫屏障,這次寫入操作會被攔截,寫屏障會將 Y 標記為灰色,放入待處理隊列,確保其后續會被掃描。

Go 的寫屏障主要有兩種形式(或其變種/組合): 插入寫屏障 (insertion write barrier)和 刪除寫屏障 (deletion write barrier),它們共同服務于維護上述三色不變性。

刪除寫屏障、插入寫屏障以及混合屏障

寫屏障是 Go 并發 GC 的核心機制之一,它通過在指針寫入操作前后插入少量額外代碼來實現。當用戶 goroutine(賦值器)修改堆上對象的指針字段時,這些由編譯器自動插入的屏障代碼會被執行,以通知 GC 發生了可能影響對象可達性的變化。

插入寫屏障(Dijkstra-style Insertion Write Barrier)

插入寫屏障的核心思想是:當一個指針從對象 A 指向對象 B 時(A.ptr = B),如果對象 B 是白色的,那么寫屏障會強制將對象 B 標記為灰色。這樣做的目的是防止一個已經被掃描過的黑色對象(比如 A,如果它已經是黑色的)指向一個尚未被發現的白色對象 B,從而避免 B 被遺漏。

具體來說,當賦值器執行 *slot = ptr (其中 slot 是堆上一個指針的地址,ptr 是新的指針值)時:

  1. 如果 ptr 指向的對象是白色的,則將其標記為灰色并加入 GC 的工作隊列。
  2. 然后才實際執行 *slot = ptr 的賦值。

這種屏障確保了所有新建立的引用,如果指向的是白色對象,那么該白色對象都會被“保護”起來(變為灰色),等待 GC 的后續處理。Go 在早期版本中主要依賴這種類型的屏障。它的優點是實現相對簡單,能夠保證正確性(不丟失存活對象)。但它可能導致一些已經死亡的對象(在被標記為灰色后,又因為其他引用的斷開而變得不可達)仍然被當作存活對象處理,直到下一輪 GC,這被稱為“浮動垃圾”(floating garbage)。

刪除寫屏障(Yuasa-style Deletion Write Barrier)

刪除寫屏障關注的是被覆蓋的舊指針。當一個指針字段 A.ptr 原本指向對象 B,現在要改為指向對象 C(或者 nil)時(oldVal = A.ptr; A.ptr = C),刪除寫屏障會在賦值發生 之前 對 oldVal(即 B)進行處理。如果 oldVal 指向的對象是白色的,并且它可能因為這次引用斷開而失去唯一的灰色或黑色先行者,那么刪除寫屏障會將 oldVal 指向的對象標記為灰色。

這種屏障主要用于增量式或并發 GC 中,以確保在并發刪除引用時,不會意外地使一個本應存活的對象(因為其他路徑仍然存在,只是 GC 尚未掃描到)變成不可達。它在處理并發場景下對象圖的動態變化時,能夠提供更強的保障,有助于提高回收的精度。

混合寫屏障(Hybrid Write Barrier)

Go 從 1.8 版本開始引入了一種 混合寫屏障 (hybrid write barrier)。這種屏障結合了插入寫屏障和刪除寫屏障的特性,其主要目的是 消除在標記終止(Mark Termination)STW 階段對所有 goroutine 棧進行重新掃描的需求,從而顯著縮短 STW 的時間。

混合寫屏障的工作方式大致如下:

  1. 堆指針寫入 :當向堆對象的指針字段寫入新值時(*slot = ptr),屏障會先將 *slot 原本指向的對象(如果它是白色的)標記為灰色。這類似于刪除寫屏障的思路,即保護即將被覆蓋的指針所指向的對象。然后才執行指針的寫入。
  2. 棧指針寫入:棧上的指針寫入不使用這種復雜的屏障,因為棧會在標記階段被特殊處理(例如,初始掃描和可能的STW期間的最終處理)。

混合寫屏障的核心保證是:任何在并發標記期間被黑色對象引用的白色對象,都會通過某種方式被 GC 發現。具體來說:

  • 如果一個黑色對象在堆上創建了一個指向白色對象的指針,那么被該指針槽位 覆蓋 的舊對象(如果是白色)會被著色為灰色。新指向的白色對象 ptr 如果沒有其他路徑,其可達性依賴于其宿主對象(即包含 slot 的對象)的狀態。如果宿主是黑色,且 ptr 是白色,這個場景正是寫屏障要處理的。混合寫屏障通過“保護舊值”的方式,間接保證了當宿主對象被掃描時,即使新值 ptr 是白色,也不會立即丟失。
  • 棧上的指針變化由棧掃描覆蓋(主要是在 GC 開始時的 標記準備 STW 階段 ,會對所有 goroutine 的棧進行掃描,這是識別從棧出發的根引用的必要步驟);在 標記終止 STW 階段 ,不再需要對所有 goroutine 的棧進行第二次完整的、地毯式的掃描。

這其中,最核心的原因是 混合寫屏障加強了對從棧流向堆的指針的追蹤能力 。過去,如果一個指針只存在于棧上,并且在并發標記期間棧發生了復雜的變化,GC 可能會“跟丟”這個指針。引入混合寫屏障后,如果這個指針要“逃逸”到堆上并可能導致問題(比如被一個黑色堆對象引用),混合寫屏障會介入。

因此,不再需要通過一個全局的、重量級的“所有棧都停下來重新徹底掃描一遍”的操作來“查漏補缺”。系統相信,通過初始的棧掃描,結合并發標記期間混合寫屏障在堆上的工作,以及運行時對 goroutine 棧的常規管理,已經足以保證所有存活對象都能被找到。

通過這種設計,Go 的混合寫屏障確保了在并發標記期間,即使賦值器在堆上創建了從黑色對象到白色對象的引用,或者刪除了這樣的引用,三色不變性也能被維護,而無需在標記結束時進行昂貴的完整棧重掃。這使得 Go 的 GC 停頓時間,特別是標記終止階段的 STW,能夠控制在非常低的水平。

STW, Stop the World

現在我們對 Go GC 的流程和寫屏障有了更深入的理解,可以再次審視 STW(Stop The World) 在 GC 周期中的角色、發生的時機以及其必要性。

Go 的 GC 周期中包含兩次主要的 STW 停頓:

標記準備階段(Mark Setup)的 STW

  • 何時發生 :在 GC 周期開始時。
  • 為何需要 :此階段非常短暫。它的主要任務是啟用寫屏障,并準備 GC 所需的各種數據結構。暫停所有 P(邏輯處理器)和用戶 goroutine 是為了確保在一個一致的程序狀態下安全地開啟寫屏障。如果在此期間用戶 goroutine 仍在運行并修改指針,那么寫屏障的啟用過程可能會出現競態條件,或者遺漏在屏障完全生效前發生的指針修改。此外,此階段還會進行一些根對象的初始掃描準備工作,比如掃描全局變量和準備掃描 goroutine 棧(現代 Go GC 對棧的初始處理更輕量)。
  • 如果不 STW 會怎樣 :無法保證寫屏障的一致啟用,可能導致在并發標記初期就丟失對象。根集合的快照也可能不準確。

標記終止階段(Mark Termination)的 STW

  • 何時發生 :在并發標記工作基本完成之后,清掃開始之前。
  • 為何需要 :此階段用于完成所有并發標記的收尾工作。例如,處理在并發標記期間由寫屏障記錄下來的、需要進一步檢查的指針。在引入混合寫屏障之前,這個階段一個非常重要的任務是重新掃描所有 goroutine 的棧,因為在并發標記期間,棧上的指針可能發生了變化,而這些變化可能沒有被寫屏障完全覆蓋(早期寫屏障主要針對堆指針)。引入混合寫屏障后,棧重掃的負擔大大減輕,甚至在很多情況下被消除了,使得這個 STW 時間顯著縮短。此 STW 確保了在進入清掃階段之前,所有存活對象都已被正確標記為黑色,所有垃圾對象都保持白色。這也是一個同步點,確保所有 GC 工作線程和輔助標記的 goroutine 都已完成其標記任務。
  • 如果不 STW 會怎樣 :如果并發標記結束后不進行最終的同步和檢查,可能會有少量本應存活的對象因為并發修改而未被標記,導致被錯誤回收。或者,某些由寫屏障延遲處理的任務沒有完成,標記結果不完整。

為什么需要兩次 STW,而不是一次長時間的 STW?

如果 GC 從頭到尾都是 STW,那么應用程序的響應會受到極大影響,長時間的停頓對于許多在線服務是不可接受的。Go GC 的設計哲學是將盡可能多的工作并發化,只在絕對必要的同步點進行短暫的 STW。這兩次 STW 將整個 GC 周期劃分為幾個階段,使得主要的標記工作(最耗時)可以與用戶 goroutine 并行執行。

GC Pacer 與 heapGoal

Go 的 GC 觸發和步調是由一個稱為 Pacer 的機制來控制的。Pacer 的目標是根據 GOGC 環境變量(默認為 100)設定的比率來決定何時啟動下一輪 GC。GOGC=100 意味著當堆大小增長到上一次 GC 結束后存活堆大小的兩倍時,就應該觸發新的 GC。計算公式為:heapGoal = heap_live * (1 + GOGC/100)其中 heap_live 是上一輪 GC 結束時測得的存活對象總大小。Pacer 會監控當前的堆分配情況,并在接近 heapGoal 時啟動 GC,力求平滑地完成回收任務,避免突兀的性能抖動。

GC 工作的公平性與標記輔助

為了確保 GC 工作能夠及時完成,尤其是在分配速率非常高的 goroutine 存在的情況下,Go 引入了 標記輔助(Mark Assist) 機制。當一個用戶 goroutine 嘗試在堆上分配新內存時,如果此時 GC 的標記階段正在進行中,并且 GC 的進度落后于預期的步調(即分配速度超過了 GC 標記的速度),那么這個正在分配內存的 goroutine 會被要求“幫助”GC 完成一部分標記工作,然后才能繼續其自身的內存分配。這種機制確保了所有 goroutine 都為 GC 貢獻力量,防止某些高分配率的 goroutine “餓死”GC 或導致堆內存失控增長。這不是嚴格意義上 P 之間的公平性,而是確保分配者也承擔 GC 責任,從而間接促進整體進度。GC 的后臺標記任務則由專門的 GC worker goroutine(通常占 GOMAXPROCS 的 25%)執行,它們之間也存在工作竊取機制以平衡負載。

更加細致的 GC 流程

為了更清晰地理解 Go 的垃圾回收過程,我們將其細化為一系列明確的階段和狀態。Go 的運行時內部使用如 _GCoff_GCmark_GCmarktermination 等狀態來管理 GC 周期。以下是一輪典型 GC 周期的詳細流程:

**當前狀態: _GCoff (GC 關閉)**
   - 描述: GC 當前未激活。應用程序 (mutators) 正常運行。
   - 內存分配: 新分配的對象被標記為白色。
   - 后臺清掃: 上一個 GC 周期的清掃工作可能仍在并發進行中 (由 gcBgMarkWorker 或按需觸發)。
            一個 mspan 必須先被清掃干凈 (即回收其中上一周期標記為白色的對象),其空閑槽位才能用于新的分配。
            一旦一個 mspan 被清掃過,在本輪 GC 的后續掃描標記完成前,它不會被再次清掃。
   - 觸發條件: 當堆分配的總大小達到根據 GOGC 計算出的 heapGoal 時,準備啟動新一輪 GC。

▼ ▼ ▼ GC 觸發 ▼ ▼ ▼

**階段 1: 標記準備 (Mark Setup) - STW (Stop The World)**
   - 運行時狀態轉換: 從 _GCoff 進入 _GCmark 階段。
   - 動作:
      1. STW 開始: 暫停所有用戶 goroutine 和 P。
      2. 設置 gcphase = _GCmark。
      3. 啟用寫屏障 (write barrier): 確保并發標記期間對象指針修改的正確性。
      4. 啟用標記輔助 (mark assists): 分配內存的用戶 goroutine 可能需要協助 GC 進行標記。
      5. 掃描根對象:
         - 掃描所有全局變量中的指針。
         - 掃描所有當前活躍 goroutine 的棧上的指針 (現代 Go 通過混合屏障等優化,此步驟可能更輕量或分階段)。
         - 將從根對象直接可達的對象標記為灰色,并放入待處理工作隊列。
      6. STW 結束: 恢復所有用戶 goroutine 和 P。
   - 后續: GC 工作 goroutine (通常為 GOMAXPROCS 的 25%) 開始并發標記。用戶 goroutine 繼續執行,并在分配時可能參與標記輔助。

**階段 2: 并發標記 (Concurrent Marking)**
   - 運行時狀態: gcphase = _GCmark。
   - 描述: 這是 GC 工作的主要階段,與用戶 goroutine 并發執行。
   - 動作:
      1. GC 工作 goroutine 和標記輔助的 goroutine 從工作隊列中取出灰色對象。
      2. 掃描灰色對象的指針字段:
         - 對于其引用的每個白色對象,將其標記為灰色并加入工作隊列。
      3. 當一個灰色對象的所有指針字段都被掃描后,將其標記為黑色。
      4. 寫屏障持續工作: 攔截用戶 goroutine 對指針的修改,以維護三色不變性 (例如,防止黑色對象指向白色對象而未將白色對象置灰)。
      5. 持續處理工作隊列,直到隊列為空或滿足特定終止條件。

**階段 3: 標記終止 (Mark Termination) - STW (Stop The World)**
   - 運行時狀態轉換: 從 _GCmark 進入 _GCmarktermination 階段。
   - 動作:
      1. STW 開始: 再次暫停所有用戶 goroutine 和 P。
      2. 設置 gcphase = _GCmarktermination。
      3. 完成剩余標記工作:
         - 處理所有寫屏障記錄的待處理指針。
         - 重新檢查某些根集合或特定條件下的對象,確保沒有遺漏 (混合屏障顯著減少了棧重掃的需求)。
         - 確保所有可達對象均已標記為黑色。此時,所有仍為白色的對象被確認為垃圾。
      4. 禁用寫屏障。
      5. 禁用標記輔助。
      6. 準備清掃階段: 初始化清掃所需的狀態。
      7. STW 結束: 恢復所有用戶 goroutine 和 P。

**階段 4: 并發清掃 (Concurrent Sweeping)**
   - 運行時狀態轉換: 從 _GCmarktermination 回到 _GCoff (清掃在 _GCoff 狀態下進行)。
   - 描述: 回收在標記階段被識別為垃圾 (白色) 的對象所占用的內存。此階段與用戶 goroutine 并發執行。
   - 動作:
      1. 設置 gcphase = _GCoff。
      2. 清掃器 (sweeper) 開始工作:
         - 后臺 GC 工作 goroutine (gcBgMarkWorker) 會主動遍歷所有 mspan,回收其中標記為白色的對象,并將其占用的槽位標記為空閑。
         - 按需清掃: 當用戶 goroutine 嘗試分配內存,且所需的 mspan 尚未被清掃時,該 goroutine 可能會先觸發對該 mspan 的清掃。
      3. 對于一個 mspan:
         - 一旦被清掃,其內部的白色對象所占用的空間被回收。
         - 該 mspan 的空閑槽位可立即用于新的對象分配 (新分配的對象為白色)。
         - 此 mspan 在下一次 GC 的標記階段完成之前,不會被再次清掃 (即,當前分配到其中的新白色對象不會被本輪 GC 的清掃器誤回收)。
      4. 更新堆的統計數據 (如 live heap 大小)。
      5. 根據新的 live heap 大小和 GOGC 設置,計算下一次 GC 的 heapGoal。

▼ ▼ ▼ GC 周期結束,系統返回 _GCoff 狀態,等待下一次觸發 ▼ ▼ ▼

關于 _GCoff 階段新分配對象的處理

在 _GCoff 階段,GC 的標記工作并未激活。當用戶 goroutine 在此階段申請內存并分配新對象時,這些新對象默認被標記為 白色 。

你可能會問,如果這些新分配的白色對象在 _GCoff 期間(此時上一周期的清掃可能還在進行),它們會不會被錯誤地清掃掉?答案是 不會 。原因在于:

  1. 清掃針對的是上一周期的垃圾 :GC 的清掃階段是針對 上一輪 標記結束后被識別為白色(即垃圾)的對象。例如,第 N 輪 GC 的清掃器只會回收在第 N 輪標記中最終仍為白色的對象。
  2. mspan 先清掃后分配 :一個 mspan(內存管理的基本單元)在能夠用于分配新的對象之前,必須確保它已經被上一輪 GC 的清掃器處理完畢。也就是說,如果一個 mspan 中含有第 N 輪 GC 判定的垃圾,這些垃圾必須被清理掉,相應的槽位變為空閑,然后這個 mspan 才能用來分配第 N+1 輪(或 _GCoff 期間)的新對象。
  3. 新對象等待下一輪 GC :在 _GCoff 期間分配到已清掃 mspan 上的新白色對象,它們是“干凈”的,它們是否存活將由 下一輪(即第 N+1 輪)GC 的標記階段來判斷。如果它們在第 N+1 輪標記開始時仍然存活(即從根可達),它們會被標記為灰色,然后黑色;如果不可達,則在第 N+1 輪標記結束后它們依然是白色,并將在第 N+1 輪的清掃階段被回收。

總結來說, “對于一個 mspan,需要先清掃完(上一周期的垃圾),再用于(為新對象)分配。下次 GC 掃描標記完成前,(這個 mspan 中新分配的對象)不會被再次清掃” 。這個機制確保了新分配的對象不會被當前(或剛結束的)GC 周期的清掃過程錯誤回收。

內存碎片與對象復用

長時間運行的程序,尤其是那些頻繁進行內存分配和釋放的程序,常常會面臨 內存碎片 (memory fragmentation)的問題。內存碎片分為內部碎片(因分配單元大于實際需求導致的空間浪費)和外部碎片(空閑內存被分割成許多不連續的小塊,導致雖然總空閑內存足夠,但無法滿足較大的單次分配請求)。

一些其他語言的 GC,例如 Java 中的 HotSpot 虛擬機,采用了 分代收集 (generational collection)的策略。這基于一個常見的“弱分代假說”:大多數對象在年輕時(剛分配后不久)就會死亡,而存活時間較長的對象則傾向于繼續存活更久。因此,Java 將堆分為新生代和老年代。新生代中的對象回收頻繁且通常采用 復制算法 (copying algorithm),這種算法在回收的同時會將存活對象復制到另一塊內存區域,從而自然地整理了內存,消除了碎片。但復制算法的代價是需要額外的空間(通常是可用空間的一半),并且移動對象也會帶來開銷。老年代則采用標記-清除或標記-整理算法。

Go 語言的 GC 并沒有采用分代收集,也沒有使用會移動對象的整理(compacting)算法。這意味著 Go 的 GC 本身不直接通過移動對象來消除外部碎片。然而,Go 的內存分配器通過一系列精巧的設計來緩解內存碎片問題,并促進內存的高效復用:

多級緩存與大小類(Size Classes & Caching Hierarchy)

  • Go 的內存分配器為小對象(通常 < 32KB)預定義了大約 67 個 大小類 (size classes)。當程序請求分配一個小對象時,分配器會將其向上取整到最接近的大小類進行分配。這有助于標準化內存塊的大小,減少內部碎片。
  • 內存分配通過一個分層緩存系統進行:mcache(P 本地緩存) -> mcentral(全局大小類緩存) -> mheap(全局堆)。
  • mcache 為每個 P 提供了各種大小類的 mspan 列表。當 goroutine 在其 P 上分配小對象時,它可以直接從 mcache 中獲取對應大小類的空閑槽位,這個過程通常是無鎖的,非常迅速。當對象被釋放時,其占用的槽位也會返回到 mcache 中對應的 mspan,以便快速復用。這種設計極大地提高了小對象的分配和回收效率,并鼓勵了內存的本地化復用。

mspan 的管理與復用

  • 一個 mspan 管理著一連串相同大小類的對象槽位。當一個 mspan 中的所有對象都被 GC 回收后,這個 mspan 就變為空閑狀態。
  • 空閑的 mspan 會被 mcentral 回收,并可以被重新用于服務于相同大小類的分配請求,或者如果 mheap 需要,在某些情況下,一個完全空閑的 mspan(由多個頁組成)的內存頁甚至可以被拆分或重新組合用于其他大小類或大對象的分配,雖然這不如直接復用高效。

大對象直接分配

對于大對象(> 32KB),它們直接從 mheap 中分配,通常會獨占一個或多個 mspan。當這些大對象被回收后,它們所占用的 mspan 也會被釋放回 mheap

向操作系統歸還內存(madvise

  • 當 mheap 中積累了大量連續的空閑頁(通常是由于大量 mspan 被完全釋放)時,Go 的運行時會周期性地掃描并嘗試將這些未使用的物理內存歸還給操作系統。這是通過調用操作系統提供的機制(如 Linux 上的 madvise 系統調用,使用 MADV_DONTNEED 或 MADV_FREE 等參數)來實現的。這并不會改變進程的虛擬地址空間大小,但會減少其實際占用的物理內存,從而降低整體系統內存壓力。雖然這不是內存整理,但它能有效減少程序在空閑時的內存足跡。

通過上述機制,Go 語言在不移動對象(避免了移動帶來的復雜性和開銷)的前提下,力求通過精細化的內存管理、高效的本地緩存和及時的內存歸還,來最大限度地減少內存碎片的影響并提高內存的復用率。特別是對于小對象的分配和回收,Go 的性能表現非常出色。

內存感知型垃圾回收的探索:Green Tea GC

隨著 CPU 核心數量的增加和內存架構(如 NUMA,Non-Uniform Memory Access)的日益復雜,內存訪問的延遲和帶寬正成為高性能系統的主要瓶頸。傳統的垃圾回收算法,包括 Go 此前版本的并行標記算法,在進行對象圖遍歷時,其內存訪問模式往往缺乏良好的 空間局部性 (spatial locality,即連續訪問物理上相鄰的內存)和 時間局部性 (temporal locality,即短時間內重復訪問同一內存區域),也未充分考慮內存拓撲結構。這會導致大量的 CPU 周期浪費在等待內存訪問上(即所謂的 memory stalls)。據統計,在 Go 的傳統 GC 掃描循環中,超過 35% 的 CPU 周期可能僅僅是由于內存停頓造成的。

為了應對這一挑戰,Go 團隊一直在探索更具內存感知能力的垃圾回收算法。在 Go 語言的 Issue #73581 中,一個名為 Green Tea ?? Garbage Collector 的新設計被提出,并計劃作為 Go 1.25 版本中的一個可選實驗性功能。

Green Tea GC 的核心思想與優勢

Green Tea GC 的核心理念是: 與其掃描單個孤立的對象,不如按更大的、連續的內存塊(spans)進行掃描。

解決的問題 : 傳統 GC 的對象圖遍歷可能在內存中“跳躍”,導致緩存未命中率高。Green Tea 試圖通過按塊處理來改善內存訪問模式。

原理簡介

  1. GC 的共享工作隊列不再追蹤單個待掃描的對象,而是追蹤內存 塊(spans) 。在原型實現中,這些主要是指包含小對象(例如,最大 512 字節)的 8KB 大小的 mspan
  2. 當 GC 發現一個指向某個塊內對象的指針時,它會標記該對象“需要掃描”(例如,在該塊的元數據中設置一個“灰色位”),并且如果該塊尚未被加入工作隊列,則將其加入。
  3. GC 工作線程從隊列中取出一個塊。核心假設是,在該塊等待被處理的過程中,可能會有更多的位于該塊內的對象被其他并發掃描的路徑發現并標記為“需要掃描”。
  4. 當工作線程實際處理這個塊時,它可以一次性掃描該塊內所有被標記為“需要掃描”的對象。由于這些對象物理上位于同一個內存塊(span)中,連續處理它們將顯著提高空間局部性,從而提升緩存利用率并減少內存訪問延遲。

帶來的優勢

  • 改善局部性 :通過集中處理同一內存塊中的對象,大幅提升了內存訪問的局部性,減少了緩存不命中和 CPU 等待內存的時間。
  • 降低開銷 :與每個小對象都入隊出隊相比,按塊(span)進行調度和管理,均攤了這部分開銷。
  • 更好的并發擴展性 :工作分配基于改進的、類似 goroutine 調度器使用的分布式工作竊取隊列,追蹤的是塊而非海量的小對象,這有助于減少全局鎖的競爭。

原型實現細節

Green Tea 的原型主要針對 小對象 span 。這是因為小對象的掃描本身耗時很短,傳統 GC 為每個小對象進行獨立調度和元數據訪問的開銷占比更高,因此從按塊掃描中獲益最大。較大的對象則可能繼續使用原有的掃描算法。

為了支持這種按塊掃描,每個 span 會存儲其內部對象的標記位(例如,每個對象對應一個灰色位和一個黑色位)。當 GC 掃描到一個指向小對象的指針時,它會設置該對象在其 span 內的灰色位。如果這個 span 尚未在掃描隊列中,它會被加入。當一個 span 從隊列中被取出進行處理時,GC 會查找該 span 內所有灰色但非黑色的對象,掃描它們,然后將它們標記為黑色。

為了優化只有一個對象需要掃描的 span(這種情況下新算法的額外開銷可能使其比老算法慢),Green Tea 實現了一些技巧:比如追蹤最初導致 span 入隊的那個對象作為“代表對象”,并使用一個“命中標志”來指示該 span 在排隊期間是否有其他對象也被標記。如果取出 span 時命中標志未設置,GC 就可以直接掃描代表對象,避免遍歷整個 span 的元數據。

Green Tea GC 的目標是使 Go 的垃圾回收器更加“內存感知”,雖然它可能不是完全以內存為中心重新設計的,但它確實朝著更有效地利用現代計算機內存層次結構邁出了重要一步。實驗結果表明,這種新算法在 GC 密集型工作負載上顯著降低了 GC 的 CPU 成本,并為未來進一步的優化(如使用 SIMD指令加速掃描)打開了大門。

責任編輯:武曉燕 來源: Piper蛋窩
相關推薦

2016-08-11 14:26:29

Java垃圾回收機制內存分配

2016-08-11 14:49:34

Java垃圾回收機制異常

2016-08-11 15:46:58

Java垃圾回收機制原理

2019-08-19 12:50:00

Go垃圾回收前端

2016-08-11 15:02:54

Java垃圾回收機制內存

2011-07-04 16:48:56

JAVA垃圾回收機制GC

2017-06-12 17:38:32

Python垃圾回收引用

2017-03-03 09:26:48

PHP垃圾回收機制

2010-09-25 15:33:19

JVM垃圾回收

2017-08-17 15:40:08

大數據Python垃圾回收機制

2009-06-23 14:15:00

Java垃圾回收

2010-10-13 10:24:38

垃圾回收機制JVMJava

2021-12-07 08:01:33

Javascript 垃圾回收機制前端

2021-11-05 15:23:20

JVM回收算法

2021-06-09 06:24:03

java垃圾回收機Java語言

2010-09-16 15:10:24

JVM垃圾回收機制

2021-05-27 21:47:12

Python垃圾回收

2010-09-25 15:26:12

JVM垃圾回收

2017-10-12 12:41:11

PHP圾回收機制變量容器

2015-06-04 09:38:39

Java垃圾回收機
點贊
收藏

51CTO技術棧公眾號

中文字幕人妻互换av久久| 手机在线播放av| 川上优的av在线一区二区| 日本欧美一区二区三区乱码| 久久国产一区二区三区| 国内自拍偷拍视频| 日韩成人影音| 亚洲男女一区二区三区| 久久精品综合一区| 伊人精品在线视频| 亚洲国内精品| 色老头一区二区三区在线观看| 师生出轨h灌满了1v1| 在线国产成人影院| 亚洲v精品v日韩v欧美v专区| 视频一区视频二区视频| 欧美一级在线免费观看| 美女视频黄a大片欧美| 97久久精品人人澡人人爽缅北| 精品无码人妻一区| 99精品中文字幕在线不卡 | 亚洲国产高清在线观看| 欧美体内she精视频| 拔插拔插海外华人免费| 黄网站在线免费看| 国产日韩精品一区二区三区| 国产精品国模大尺度私拍| 在线观看免费高清视频| 日韩精品欧美精品| 69视频在线播放| 国产精品老熟女一区二区| 欧美色婷婷久久99精品红桃| 日韩一区二区三区电影在线观看| 九热视频在线观看| 成人免费看黄| 污片在线观看一区二区| 久久久久久久香蕉| 成人短视频在线观看| 国产精品午夜免费| 视频一区视频二区视频三区高| 国产xxxx孕妇| 国产原创一区二区| 成人h片在线播放免费网站| 久久久精品视频网站| 国产精品久久国产愉拍| 97视频免费观看| 成年人午夜视频| 日韩午夜一区| 97在线看免费观看视频在线观看| 久久久无码精品亚洲国产| 中文字幕亚洲精品乱码| 中文字幕亚洲色图| 国产jizz18女人高潮| 第一会所亚洲原创| 色妞一区二区三区| 亚洲精品久久久久久国| 亚洲情侣在线| 欧美国产日韩一区| av资源吧首页| 亚洲永久在线| 国产精品aaaa| 亚洲怡红院av| 国产麻豆视频一区二区| 91情侣在线视频| 丰满人妻av一区二区三区| 成人h版在线观看| 久久久久久草| wwwxxx在线观看| 国产精品福利影院| 日本一道在线观看| 麻豆免费版在线观看| 色94色欧美sute亚洲线路一ni| 日韩精品免费播放| 四虎影视国产精品| 日韩欧美在线1卡| 亚洲国产第一区| 欧州一区二区| 精品综合久久久久久97| 国产成人一区二区三区影院在线| 久久国产欧美| 成人国产在线激情| 高清毛片aaaaaaaaa片| 91视视频在线观看入口直接观看www | 欧美中文字幕在线观看视频| 天堂中文av在线资源库| 欧美日韩专区在线| 被黑人猛躁10次高潮视频| 日韩伦理一区二区三区| 色偷偷av亚洲男人的天堂| 久操视频免费在线观看| 老牛嫩草一区二区三区日本| 国产欧美一区二区三区在线| 亚洲欧美激情在线观看| 欧美国产日韩a欧美在线观看| 日本免费黄色小视频| 午夜欧美激情| 欧美一级夜夜爽| 在线 丝袜 欧美 日韩 制服| 99视频精品全部免费在线视频| 久久久久久这里只有精品| 国产真人无遮挡作爱免费视频| 国产精品一品视频| 日韩欧美一区二区三区四区五区| 手机在线免费av| 欧美专区日韩专区| 国产草草浮力影院| 99免费精品| 日韩免费av一区二区| 亚洲高清视频在线播放| 国产精品青草久久| 免费观看日韩毛片| 一区二区视频| 日韩中文在线视频| 日韩av一二三区| 韩国成人在线视频| 日本不卡二区高清三区| 2021天堂中文幕一二区在线观| 欧美三级日韩三级| 欧美黄色激情视频| 国产精品日韩| 国产伦精品一区二区三区免费视频 | 亚洲日本中文| 一区国产精品视频| 国产剧情在线视频| av亚洲精华国产精华精| 国产精品一二三在线观看| 福利一区二区| 国产一区二区久久精品| 国产情侣自拍av| av一区二区三区在线| 成年在线观看视频| 精品一区二区三区中文字幕在线| 中文字幕亚洲欧美日韩2019| 日韩精品一区不卡| 久久奇米777| 欧美爱爱视频免费看| 成人直播在线观看| 欧美激情奇米色| www.久久色| 一区二区三区在线视频观看58| 蜜臀一区二区三区精品免费视频 | 五十路熟女丰满大屁股| 亚洲伊人影院| 欧美精品久久久久a| 亚洲AV午夜精品| 亚洲图片自拍偷拍| 国产伦精品一区二区三区精品| 红桃视频国产精品| 国产欧美日韩视频一区二区三区| 青青草原国产在线| 精品国产一区二区在线观看| 久久久久久久极品内射| 国产成人av资源| 日本中文字幕网址| 亚洲人成亚洲精品| 国产国语videosex另类| 啊v在线视频| 69成人精品免费视频| 欧美成人777| 丁香婷婷综合激情五月色| 大西瓜av在线| 亚洲福利网站| 国产精品女主播视频| 日韩黄色影院| 欧美成人女星排名| 少妇一级淫片免费放中国 | 日韩国产一区| 国产成人综合精品| 91短视频版在线观看www免费| 欧美日韩亚洲丝袜制服| 五月天色婷婷丁香| 成人一区二区三区视频在线观看| 国产免费黄色一级片| 综合伊思人在钱三区| 国产精品视频网| 青春草在线免费视频| 亚洲精品视频免费| 在线亚洲欧美日韩| 亚洲午夜三级在线| 天天躁日日躁aaaxxⅹ| 狠狠色狠狠色合久久伊人| www.夜夜爱| 欧州一区二区| 国产精品久久7| 欧美日韩在线精品一区二区三区激情综合 | 色悠悠在线视频| 麻豆精品网站| 日韩欧美一级在线| 奇米亚洲欧美| 北条麻妃高清一区| 欧美日韩尤物久久| 久久久久久久网站| 波多野结衣在线影院| 欧美大胆人体bbbb| 中国老头性行为xxxx| 亚洲国产aⅴ天堂久久| 国产在线免费av| 波波电影院一区二区三区| 九色porny自拍| av不卡在线看| 中文字幕色呦呦| 日韩精品免费一区二区三区| 国产精品初高中精品久久| 韩日精品一区| 2020久久国产精品| 中文字幕有码在线观看| 亚洲人高潮女人毛茸茸| 亚洲国产综合一区| 欧美日本乱大交xxxxx| www.国产一区二区| 亚洲成a人片综合在线| 激情五月激情综合| 国产亚洲欧美一区在线观看| 免费看91视频| 黄网站免费久久| 亚洲老女人av| 久久久成人网| 免费成人在线视频网站| 欧美全黄视频| 国产免费xxx| 91综合视频| 天堂精品视频| 久久99国产精一区二区三区| 九色一区二区| 日韩av不卡一区| 国产日本一区二区三区| 精品亚洲a∨一区二区三区18| 国产精品欧美日韩久久| 成人在线网站| 国产精品成人一区二区三区吃奶| 女厕盗摄一区二区三区| 久久久久久久国产精品视频| 污视频网站在线免费| 久久亚洲精品成人| 黄网站app在线观看| 亚洲精选中文字幕| 外国精品视频在线观看 | 日本三级在线视频| 伊人久久综合97精品| 北岛玲日韩精品一区二区三区| 亚洲午夜av电影| 国产三级视频在线播放线观看| 亚洲欧洲日本专区| 黄色片视频在线观看| 亚洲色图25p| 国产福利在线观看| 伊人青青综合网站| 午夜视频在线免费观看| 日韩在线视频网| av在线播放国产| 欧美激情欧美狂野欧美精品| 久久大胆人体| 97超视频免费观看| 中文字幕av一区二区三区佐山爱| 国产精品91在线观看| 成人涩涩视频| 91精品啪aⅴ在线观看国产| 国产精品一区二区三区四区在线观看 | 久久久噜噜噜久久中文字幕色伊伊 | 超碰在线97观看| 精品视频免费在线| 国产剧情久久久| 精品1区2区在线观看| 视频二区在线| 尤物精品国产第一福利三区 | 国内精品一区二区三区四区| av资源中文在线| 国产精品成人国产乱一区| 国产精品日韩精品在线播放 | 一区二区电影在线观看| 国产爆乳无码一区二区麻豆 | 一区二区三区精品久久久| 国产真实乱人偷精品视频| 欧美午夜精品久久久久久人妖| 无码久久精品国产亚洲av影片| 9191久久久久久久久久久| 亚洲精品97久久中文字幕| 亚洲男人天堂视频| 免费高清完整在线观看| 韩国精品美女www爽爽爽视频| 久久uomeier| 99久久综合狠狠综合久久止| 日韩大胆成人| 尤物国产精品| 99精品国产在热久久婷婷| 538在线视频观看| 大桥未久av一区二区三区中文| www.av欧美| 亚洲免费观看高清完整| 亚洲黄色免费观看| 日韩写真欧美这视频| 欧美色视频免费| 久久成年人视频| 校园春色亚洲色图| 99视频日韩| 欧美肉体xxxx裸体137大胆| 欧洲精品在线播放| 蜜臀久久久久久久| 亚洲成人av免费在线观看| 日韩美女视频一区二区| 国产精品久久久久久久久久久久久久久久久 | 久久久噜噜噜久久| 色999韩欧美国产综合俺来也| 成人黄色在线免费观看| 久久五月天小说| 欧美日韩一区二区在线免费观看| 国产精品一区二区91| 摸摸摸bbb毛毛毛片| 天天操天天色综合| 精品人妻一区二区三区浪潮在线 | 免费久久99精品国产自| 欧美日韩91| 一级片视频免费观看| 久久这里只有精品首页| 日本少妇性生活| 日韩一区二区三区精品视频| 日本在线观看| 国产成人精品电影久久久| 国产精品极品国产中出| av不卡在线免费观看| 免费人成网站在线观看欧美高清| 国产老熟女伦老熟妇露脸| 亚洲综合成人在线| 99久久精品国产色欲| 色噜噜狠狠色综合网图区| 成人自拍视频网| 日韩精品伦理第一区| 日一区二区三区| 亚洲成人网在线播放| 欧美日韩国产一区中文午夜| 日本国产在线观看| 久久久久久亚洲| 激情亚洲另类图片区小说区| 久久99久久久久久| 国产成人综合在线播放| 成人性生活毛片| 欧美一区二区国产| 国产cdts系列另类在线观看| 91精品国产综合久久香蕉的用户体验| 欧美一区二区三| 午夜国产一区二区三区| 国产精品久久久久永久免费观看 | 国产综合成人久久大片91| jizzjizzjizz国产| 欧美精品久久99久久在免费线| 亚洲图片88| 亚洲一区美女视频在线观看免费| 香蕉视频国产精品| 亚洲一区二区图片| 亚洲一区精品在线| 刘亦菲久久免费一区二区| 性欧美暴力猛交69hd| 日本午夜精品| 美女网站视频黄色| 亚洲色图欧美激情| 亚洲精品97久久中文字幕无码| 国内精品一区二区三区四区| 丝袜美腿一区二区三区动态图| aaaaaa亚洲| 1000部国产精品成人观看| 精品国产九九九| 久久久久中文字幕| 国产真实有声精品录音| 欧美日韩中文不卡| 亚洲日本护士毛茸茸| 丰满人妻妇伦又伦精品国产| 96精品视频在线| 欧美精品一二| 色偷偷中文字幕| 五月婷婷激情综合网| 国内精品在线视频| 91色视频在线导航| 国产日韩欧美一区| 亚洲女人毛茸茸高潮| 日韩免费看网站| 国偷自产一区二区免费视频| 亚洲午夜精品一区二区| 国产成人精品免费一区二区| 日本高清不卡码| 裸体女人亚洲精品一区| 免费观看成人www动漫视频| 色综合天天色综合| 亚洲国产一区在线观看| 国产在线观看黄| 91免费版网站在线观看| 久久婷婷影院| 久久精品一级片| 亚洲网站在线看| 一区二区三区高清在线观看| 成人羞羞国产免费网站| 亚洲精品美国一| 东热在线免费视频| 国产日韩二区| 国产一区三区三区| 亚洲午夜18毛片在线看| 插插插亚洲综合网| 欧美日韩国产在线观看网站| 少妇精品无码一区二区三区| 欧美日韩性生活| 天堂在线中文网官网|