首先我們先來了解下什么是垃圾回收。
什么是垃圾回收?
內存管理是程序員開發應用的一大難題。傳統的系統級編程語言(主要指C/C++)中,程序開發者必須對內存小心的進行管理操作,控制內存的申請及釋放。因為稍有不慎,就可能產生內存泄露問題,這種問題不易發現并且難以定位,一直成為困擾程序開發者的噩夢。
如何解決這個頭疼的問題呢?
過去一般采用兩種辦法:
* 內存泄露檢測工具。這種工具的原理一般是靜態代碼掃描,通過掃描程序檢測可能出現內存泄露的代碼段。然而檢測工具難免有疏漏和不足,只能起到輔助作用。
* 智能指針。這是 c++ 中引入的自動內存管理方法,通過擁有自動內存管理功能的指針對象來引用對象,是程序員不用太關注內存的釋放,而達到內存自動釋放的目的。這種方法是采用最廣泛的做法,但是對程序開發者有一定的學習成本(并非語言層面的原生支持),而且一旦有忘記使用的場景依然無法避免內存泄露。
為了解決這個問題,后來開發出來的幾乎所有新語言(java,python,php等等)都引入了語言層面的自動內存管理 – 也就是語言的使用者只用關注內存的申請而不必關心內存的釋放,內存釋放由虛擬機(virtual machine)或運行時(runtime)來自動進行管理。而這種對不再使用的內存資源進行自動回收的行為就被稱為垃圾回收。
常用的垃圾回收的方法:
* 引用計數(reference counting)
這是最簡單的一種垃圾回收算法,和之前提到的智能指針異曲同工。對每個對象維護一個引用計數,當引用該對象的對象被銷毀或更新時被引用對象的引用計數自動減一,當被引用對象被創建或被賦值給其他對象時引用計數自動加一。當引用計數為0時則立即回收對象。
這種方法的優點是實現簡單,并且內存的回收很及時。這種算法在內存比較緊張和實時性比較高的系統中使用的比較廣泛,如`ios cocoa`框架,php,python等。
但是簡單引用計數算法也有明顯的缺點:
1. 頻繁更新引用計數降低了性能。
一種簡單的解決方法就是編譯器將相鄰的引用計數更新操作合并到一次更新;還有一種方法是針對頻繁發生的臨時變量引用不進行計數,而是在引用達到0時通過掃描堆棧確認是否還有臨時對象引用而決定是否釋放。等等還有很多其他方法,具體可以參考這里。
2. 循環引用。
當對象間發生循環引用時引用鏈中的對象都無法得到釋放。最明顯的解決辦法是避免產生循環引用,如cocoa引入了strong指針和weak指針兩種指針類型。或者系統檢測循環引用并主動打破循環鏈。當然這也增加了垃圾回收的復雜度。
* 標記-清除(mark and sweep)
標記-清除(mark and sweep)分為兩步,標記從根變量開始迭代得遍歷所有被引用的對象,對能夠通過應用遍歷訪問到的對象都進行標記為“被引用”;標記完成后進行清除操作,對沒有標記過的內存進行回收(回收同時可能伴有碎片整理操作)。
這種方法解決了引用計數的不足,但是也有比較明顯的問題:每次啟動垃圾回收都會暫停當前所有的正常代碼執行,回收時,系統響應能力大大降低!當然后續也出現了很多`mark&sweep`算法的變種(如三色標記法)優化了這個問題。
* 分代搜集(generation)
java的jvm 就使用的分代回收的思路。在面向對象編程語言中,絕大多數對象的生命周期都非常短。分代收集的基本思想是,將堆劃分為兩個或多個稱為代(generation)的空間。 新創建的對象存放在稱為新生代(young generation)中(一般來說,新生代的大小會比 老年代小很多),隨著垃圾回收的重復執行,生命周期較長的對象會被提升(promotion)到老年代中(這里用到了一個分類的思路,這個是也是科學思考的一個基本思路)。
因此,新生代垃圾回收和老年代垃圾回收兩種不同的垃圾回收方式應運而生,分別用于對各自空間中的對象執行垃圾回收。新生代垃圾回收的速度非常快,比老年代快幾個數量級,即使新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是因為大多數對象的生命周期都很短,根本無需提升到老年代。
Golang GC 時會發生什么?
`Golang 1.5`后,采取的是“非分代的、非移動的、并發的、三色的”標記清除垃圾回收算法。
golang 中的 gc 基本上是標記清除的過程:
[](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/images/2.jpg)
golang 的垃圾回收是基于標記清掃算法,這種算法需要進行 STW(stop the world),這個過程就會導致程序是卡頓的,頻繁的 GC 會嚴重影響程序性能.
golang 在此基礎上進行了改進,通過三色標記清掃法與寫屏障來減少 STW 的時間.
gc的過程一共分為四個階段:
1. 棧掃描(開始時STW),所有對象最開始都是白色.
2. 從 root開始找到所有可達對象(所有可以找到的對象),標記為灰色,放入待處理隊列。
3. 遍歷灰色對象隊列,將其引用對象標記為灰色放入待處理隊列,自身標記為黑色。
4. 清除(并發) 循環步驟3直到灰色隊列為空為止,此時所有引用對象都被標記為黑色,所有不可達的對象依然為白色,白色的就是需要進行回收的對象。 三色標記法相對于普通標記清掃,減少了 STW 時間. 這主要得益于標記過程是 "on-the-fly" 的,在標記過程中是不需要 STW 的,它與程序是并發執行的,這就大大縮短了STW的時間.
Golang gc 優化的核心就是盡量使得 STW(Stop The World) 的時間越來越短。
詳細的Golang的GC介紹可以參看[Golang垃圾回收](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/spec/02.0.md).
寫屏障:
當標記和程序是并發執行的,這就會造成一個問題. 在標記過程中,有新的引用產生,可能會導致誤清掃.
清掃開始前,標記為黑色的對象引用了一個新申請的對象,它肯定是白色的,而黑色對象不會被再次掃描,那么這個白色對象無法被掃描變成灰色、黑色,它就會最終被清掃,而實際它不應該被清掃.
這就需要用到屏障技術,golang采用了寫屏障,其作用就是為了避免這類誤清掃問題. 寫屏障即在內存寫操作前,維護一個約束,從而確保清掃開始前,黑色的對象不能引用白色對象.
- Golang基礎
- Go中new與make的區別
- Golang中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量
- 無緩沖Chan的發送和接收是否同步
- Golang并發機制以及它所使用的CSP并發模型.
- Golang中常用的并發模型
- Go中對nil的Slice和空Slice的處理是一致的嗎
- 協程和線程和進程的區別
- Golang的內存模型中為什么小對象多了會造成GC壓力
- Go中數據競爭問題怎么解決
- 什么是channel,為什么它可以做到線程安全
- Golang垃圾回收算法
- GC的觸發條件
- Go的GPM如何調度
- 并發編程概念是什么
- Go語言的棧空間管理是怎么樣的
- Goroutine和Channel的作用分別是什么
- 怎么查看Goroutine的數量
- Go中的鎖有哪些
- 怎么限制Goroutine的數量
- Channel是同步的還是異步的
- Goroutine和線程的區別
- Go的Struct能不能比較
- Go的defer原理是什么
- Go的select可以用于什么
- Context包的用途是什么
- Go主協程如何等其余協程完再操作
- Go的Slice如何擴容
- Go中的map如何實現順序讀取
- Go中CAS是怎么回事
- Go中的逃逸分析是什么
- Go值接收者和指針接收者的區別
- Go的對象在內存中是怎樣分配的
- 棧的內存是怎么分配的
- 堆內存管理怎么分配的
- 在Go函數中為什么會發生內存泄露
- G0的作用
- Go中的鎖如何實現
- Go中的channel的實現
- 棧的內存是怎么分配的2
- 堆內存管理怎么分配的2
- Go中的map的實現
- Go中的http包的實現原理
- Goroutine發生了泄漏如何檢測
- Go函數返回局部變量的指針是否安全
- Go中兩個Nil可能不相等嗎
- Goroutine和KernelThread之間是什么關系
- 為何GPM調度要有P
- 如何在goroutine執行一半就退出協程
- Mysql基礎
- Mysql索引用的是什么算法
- Mysql事務的基本要素
- Mysql的存儲引擎
- Mysql事務隔離級別
- Mysql高可用方案有哪些
- Mysql中utf8和utf8mb4區別
- Mysql中樂觀鎖和悲觀鎖區別
- Mysql索引主要是哪些
- Mysql聯合索引最左匹配原則
- 聚簇索引和非聚簇索引區別
- 如何查詢一個字段是否命中了索引
- Mysql中查詢數據什么情況下不會命中索引
- Mysql中的MVCC是什么
- Mvcc和Redolog和Undolog以及Binlog有什么不同
- Mysql讀寫分離以及主從同步
- InnoDB的關鍵特性
- Mysql如何保證一致性和持久性
- 為什么選擇B+樹作為索引結構
- InnoDB的行鎖模式
- 哈希(hash)比樹(tree)更快,索引結構為什么要設計成樹型
- 為什么索引的key長度不能太長
- Mysql的數據如何恢復到任意時間點
- Mysql為什么加了索引可以加快查詢
- Explain命令有什么用
- Redis基礎
- Redis的數據結構及使用場景
- Redis持久化的幾種方式
- Redis的LRU具體實現
- 單線程的Redis為什么快
- Redis的數據過期策略
- 如何解決Redis緩存雪崩問題
- 如何解決Redis緩存穿透問題
- Redis并發競爭key如何解決
- Redis的主從模式和哨兵模式和集群模式區別
- Redis有序集合zset底層怎么實現的
- 跳表的查詢過程是怎么樣的,查詢和插入的時間復雜度
- 網絡協議基礎
- TCP和UDP有什么區別
- TCP中三次握手和四次揮手
- TCP的LISTEN狀態是什么
- 常見的HTTP狀態碼有哪些
- 301和302有什么區別
- 504和500有什么區別
- HTTPS和HTTP有什么區別
- Quic有什么優點相比Http2
- Grpc的優缺點
- Get和Post區別
- Unicode和ASCII以及Utf8的區別
- Cookie與Session異同
- Client如何實現長連接
- Http1和Http2和Grpc之間的區別是什么
- Tcp中的拆包和粘包是怎么回事
- TFO的原理是什么
- TIME_WAIT的作用
- 網絡的性能指標有哪些