試著把.net的GC講清楚(1)
什麽是GC?
GC(garbage collection)是對內存管理中回收已經不用的內存的一種機制,我們熟知的java和.net都有自己的GC機制,是內存管理的一部分。
為什麽會有GC呢?是因為動態的內存分配和分布操作系統是不管的,得各類語言自己實現,例如c和c++自己需要手動管理分配的內存資源,如果不手動釋放,那麽會造成已經無用的內存不能被操作系統識別使用,也就是所謂的內存泄漏。
.net的GC都是發生在堆(heap),因為這個動態的內存是在堆上分配的。為什麽.net 沒有像c++一樣提供手動管理內存的操作?因為手工管理內存非常容易出問題,開發人員不應花費時間在這個上面,避免人為問題,就找一個管理內存的“人”來,處理這些事情,於是GC就出現了(說一句話,以前自己是c、c++出身,真的一個語言影響一個人的知識廣度和深度),大家都不用考慮這些事情,集中在重要的事情上,就像自己找了個管家,而且是專業的,不會出錯的那種,省心。
GC有哪些分類?
我了解到的有:Reference Count、Mark and Sweep(升級版Mark and Compact)、Copy and Collection
現在java和.net 使用的是Mark and Compact算法,這個算法是從Mark and Sweep算法演變過來的,下來就講講Mark and Sweep。
Mark and Sweep:分為兩個階段,第一階段標記所有現在還可以使用的對象,第二階段清除標記的對象之外的內存。
在.net中,GC管理了一組root(由全局對象組成),通過遍歷所有的root機器引用的子對象,進行內存中的存活對象的標記,之後就清除未標記為存活的對象。這就是Mark and Sweep算法,但是這個造成了一個問題,就是回收後的內存是成了篩子了,這個時候如果來一個大的對象需要分配內存,那麽空余內存總額大於分配對象的大小,但是找不到一個連續的可以容下這個對象大小的內存,這個時候怎麽辦?其實模擬操作系統,再做一個內存管理的機制就行,在邏輯上看著連續就行了。當然這個不是本次討論的對象,Mark and Compact解決了內存不連續的問題,因為它把內存做了一次整理(把不相鄰的內存移動到一塊,看著就連續了)
Mark and Compact:在Mark and Sweep基礎上做了一次內存整理,因為內存做整理的時候,對象的引用是不能被使用的,引用地址會變,所以啊,GC的時候,使用到這些對象的線程什麽的是會被掛起等待的,也不能經常回收內存,不然性能堪憂,就是因為回收導致掛起了。
啥是0代、1代、2代對象?
要解釋這個問題,還得從內存回收時間說起,這裏有個假設(其實也是規則)回收一個內存中所有對的時間大於內存中部分對象的時間,於是就把內存中對象分成了幾代,0代對象指最新分配內存的對象,一次類推。其實多少代,這個由GC決定,.net中GC中代數是3代(這個值暫時不能確定能不能改)。
GC怎麽管理代對象呢?一般情況下,分配的對象都是0代對象,在分配對象內存時,如果0代對象的內存已經不能容納新對象了(超過0代對象內存的上限),在gc回收一次0代後,這個還存活的對象代數加1(GC.Collect();GC.GetGeneration(obj)
GC.Collect()
,有興趣可以翻翻這個方法。
代數的大小,查了很多資料之後,只發現一篇文章說到,.net中0代和1代之和為16MB,2代內存上限非常的大,具體有framework版本和其他一些因素決定的。
//驗證回收一次,對象就升一代
Object obj=new Object();
Console.WriteLine(GC.GetGeneration(obj));
GC.Collect();
Console.WriteLine(GC.GetGeneration(obj));
GC.Collect();
Console.WriteLine(GC.GetGeneration(obj));
Finalize、Dispose是啥?如何理解?
.net中有托管資源和非托管資源的分類,托管資源.net自己就可以管理,非托管資源,需要特殊的方法,也就是托管資源在GC的時候,.net可以自己識別,但是非托管資源,GC是自動釋放不了的。
什麽是非托管資源?這讓我想起之前用mfc寫windows程序的時候,什麽畫刷、畫筆、com之類的,就是非托管資源,還有數據庫連接、文件、套接字之類的也是,哦,還有流之類的都是。
這些非托管資源,一般都需要自己釋放資源,.net提供了IDsiposable的接口,實現這個接口的方法,在裏面進行資源釋放,使用using語句來簡化這個非托管資源的釋放工作。
Finalize:這個也能用來釋放非托管資源,與IDsiposable接口區別是,它的調用時機是不定的,因為它是由GC調用的,GC調用真的不定的,因為調用一次GC調用會降低程序性能(前面說的,內存壓縮導致引用需要變化,而因為線程掛起),下面來說說為什麽它是由GC調用的。
在創建對象的時候,會把還有析構函數(編譯之後,就是Finalize方法,與c++中的析構函數不同)的對象引用存到一個叫做Finalizer Queue的list中,在GC的時候,如果一個對象是無用的,而且在Finalizer Queue裏面有引用,此次並不回收,並且會把引用從Finalizer Queue移到Freachable Queue的list中,Freachable Queue的list有內容之後會啟動一個線程,然後執行裏面的引用的對象的析構函數,執行完畢後把對象的引用刪除,等待下次GC的時候,才進行回收此對象。
所以Finalize的特點就是:
- 啥時候調用不定
- 這類對象,需要至少兩次GC才能回收。
為什麽至少兩次,而不是兩次,因為.net為我們提供了一個把對象引用放回Finalizer Queue的方法,GC.ReRegisterForFinalize(),如果在Finalize中調用了這個代碼,那麽就死不了了。
微軟不建議使用Finalize方法,就像其他博客中提到的,我們可以把它留作後手,萬一那個非托管資源該釋放沒有釋放,可以在Finalize方法中做為最後的保險(算是避免人為原因)。
我確實釋放完了非托管資源,就是不想執行Finalize方法,微軟也提供了方法了:GC.SuppressFinalize(this),這個方法執行了之後就把這個對象的引用中Finalizer Queue移除了。
LOH是什麽?
LOH(large object heap)是為了大對象而專門設計的一個堆,多大的對象會分配到這個堆裏面?超過85000個字節的就會。其實這個loh產生原因大對象移動非常的耗時,還不如不移動,例如,3個對象ABC,AB對象大約占個80個字節,C對象占個10000個字節,假設AB對象被回收,那麽在移動階段,就要把10000個字節,往前移動80字節,還不如不移動性能高。這個85000字節也是一個經驗值。
既然loh不能移動,那麽肯定不能用Mark and Compact中的移動了(使用什麽算法現在還不清楚,猜測是Mark and Sweep,或許是特例),並且在只有2代對象回收的時候才進行回收。
GC模式?
- workstation mode:用於單處理器的系統中,頻繁回收,從而阻止一次長時間的回收對程序的掛起時間。
- server mode:用於多處理器的系統中,為每個處理器都創建一個GC Heap,該模式特點是,分配內存較大,能不回收就不回收,回收時候耗時太長。
其中有Concurrent GC 工作方式,其中workstation mode和server mode都可以配置,在單處理器上設置為true也不生效,主要用於用戶線程在gc時候可以大部分時間和gc線程並發,詳細可以參考:https://blogs.msdn.microsoft.com/seteplia/2017/01/05/understanding-different-gc-modes-with-concurrency-visualizer/
啥時候需要手動gc?
資源特別緊張的時候,例如之前面試的一家公司,系統是在azure上,內存什麽的特別貴,這個時候手動gc可能其中一種手段了。
最後
其實gc的最耗時還是在算法的選擇上,比如Mark and Compact中的把內存合並成連續的,這個才是耗時的,如果內存足夠多,根本就不需要考慮移動內存。
或者像我之前提的,再在內存上面做一次內存映射的管理,也可以避免內存不連續的問題,當然肯定會遇到各種各樣的問題。
試著把.net的GC講清楚(1)