亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

c#如何用好垃圾回收機制GC

 更新時間:2020年08月25日 10:50:12   作者:Jeff Wong  
這篇文章主要介紹了c# 如何用好垃圾回收機制GC,幫助大家更好的理解和學(xué)習(xí)c#,感興趣的朋友可以了解下

一、為什么需要GC

應(yīng)用程序?qū)Y源操作,通常簡單分為以下幾個步驟:

1、為對應(yīng)的資源分配內(nèi)存

2、初始化內(nèi)存

3、使用資源

4、清理資源

5、釋放內(nèi)存

應(yīng)用程序?qū)Y源(內(nèi)存使用)管理的方式,常見的一般有如下幾種:

1、手動管理:C,C++

2、計數(shù)管理:COM

3、自動管理:.NET,Java,PHP,GO…

但是,手動管理和計數(shù)管理的復(fù)雜性很容易產(chǎn)生以下典型問題:

1.程序員忘記去釋放內(nèi)存

2.應(yīng)用程序訪問已經(jīng)釋放的內(nèi)存

產(chǎn)生的后果很嚴(yán)重,常見的如內(nèi)存泄露、數(shù)據(jù)內(nèi)容亂碼,而且大部分時候,程序的行為會變得怪異而不可預(yù)測,還有Access Violation等。

.NET、Java等給出的解決方案,就是通過自動垃圾回收機制GC進行內(nèi)存管理。這樣,問題1自然得到解決,問題2也沒有存在的基礎(chǔ)。

總結(jié):無法自動化的內(nèi)存管理方式極容易產(chǎn)生bug,影響系統(tǒng)穩(wěn)定性,尤其是線上多服務(wù)器的集群環(huán)境,程序出現(xiàn)執(zhí)行時bug必須定位到某臺服務(wù)器然后dump內(nèi)存再分析bug所在,極其打擊開發(fā)人員編程積極性,而且源源不斷的類似bug讓人厭惡。

二、GC是如何工作的

GC的工作流程主要分為如下幾個步驟:

1、標(biāo)記(Mark)

2、計劃(Plan)

3、清理(Sweep)

4、引用更新(Relocate)

5、壓縮(Compact)

(一)、標(biāo)記

目標(biāo):找出所有引用不為0(live)的實例

方法:找到所有的GC的根結(jié)點(GC Root), 將他們放到隊列里,然后依次遞歸地遍歷所有的根結(jié)點以及引用的所有子節(jié)點和子子節(jié)點,將所有被遍歷到的結(jié)點標(biāo)記成live。弱引用不會被考慮在內(nèi)

(二)、計劃和清理

1、計劃

目標(biāo):判斷是否需要壓縮

方法:遍歷當(dāng)前所有的generation上所有的標(biāo)記(Live),根據(jù)特定算法作出決策

2、清理

目標(biāo):回收所有的free空間

方法:遍歷當(dāng)前所有的generation上所有的標(biāo)記(Live or Dead),把所有處在Live實例中間的內(nèi)存塊加入到可用內(nèi)存鏈表中去

(三)、引用更新和壓縮

1、引用更新

目標(biāo): 將所有引用的地址進行更新

方法:計算出壓縮后每個實例對應(yīng)的新地址,找到所有的GC的根結(jié)點(GC Root), 將他們放到隊列里,然后依次遞歸地遍歷所有的根結(jié)點以及引用的所有子節(jié)點和子子節(jié)點,將所有被遍歷到的結(jié)點中引用的地址進行更新,包括弱引用。

2、壓縮

目標(biāo):減少內(nèi)存碎片

方法:根據(jù)計算出來的新地址,把實例移動到相應(yīng)的位置。

三、GC的根節(jié)點

本文反復(fù)出現(xiàn)的GC的根節(jié)點也即GC Root是個什么東西呢?

每個應(yīng)用程序都包含一組根(root)。每個根都是一個存儲位置,其中包含指向引用類型對象的一個指針。該指針要么引用托管堆中的一個對象,要么為null。

在應(yīng)用程序中,只要某對象變得不可達,也就是沒有根(root)引用該對象,這個對象就會成為垃圾回收器的目標(biāo)。

用一句簡潔的英文描述就是:GC roots are not objects in themselves but are instead references to objects.而且,Any object referenced by a GC root will automatically survive the next garbage collection.

.NET中可以當(dāng)作GC Root的對象有如下幾種:

1、全局變量

2、靜態(tài)變量

3、棧上的所有局部變量(JIT)

4、棧上傳入的參數(shù)變量

5、寄存器中的變量

注意,只有引用類型的變量才被認(rèn)為是根,值類型的變量永遠不被認(rèn)為是根。只有深刻理解引用類型和值類型的內(nèi)存分配和管理的不同,才能知道為什么root只能是引用類型。

順帶提一下JAVA,在Java中,可以當(dāng)做GC Root的對象有以下幾種:

1、虛擬機(JVM)棧中的引用的對象

2、方法區(qū)中的類靜態(tài)屬性引用的對象

3、方法區(qū)中的常量引用的對象(主要指聲明為final的常量值)

4、本地方法棧中JNI的引用的對象

四、什么時候發(fā)生GC

1、當(dāng)應(yīng)用程序分配新的對象,GC的代的預(yù)算大小已經(jīng)達到閾值,比如GC的第0代已滿

2、代碼主動顯式調(diào)用System.GC.Collect()

3、其他特殊情況,比如,windows報告內(nèi)存不足、CLR卸載AppDomain、CLR關(guān)閉,甚至某些極端情況下系統(tǒng)參數(shù)設(shè)置改變也可能導(dǎo)致GC回收

五、GC中的代

代(Generation)引入的原因主要是為了提高性能(Performance),以避免收集整個堆(Heap)。一個基于代的垃圾回收器做出了如下幾點假設(shè):

1、對象越新,生存期越短

2、對象越老,生存期越長

3、回收堆的一部分,速度快于回收整個堆

.NET的垃圾收集器將對象分為三代(Generation0,Generation1,Generation2)。不同的代里面的內(nèi)容如下:

1、G0 小對象(Size<85000Byte)

2、G1:在GC中幸存下來的G0對象

3、G2:大對象(Size>=85000Byte);在GC中幸存下來的G1對象

 object o = new Byte[85000]; //large object
 Console.WriteLine(GC.GetGeneration(o)); //output is 2,not 0

ps,這里必須知道,CLR要求所有的資源都從托管堆(managed heap)分配,CLR會管理兩種類型的堆,小對象堆(small object heap,SOH)和大對象堆(large object heap,LOH),其中所有大于85000byte的內(nèi)存分配都會在LOH上進行。一個有趣的問題是為什么是85000字節(jié)?

代收集規(guī)則:當(dāng)一個代N被收集以后,在這個代里的幸存下來的對象會被標(biāo)記為N+1代的對象。GC對不同代的對象執(zhí)行不同的檢查策略以優(yōu)化性能。每個GC周期都會檢查第0代對象。大約1/10的GC周期檢查第0代和第1代對象。大約1/100的GC周期檢查所有的對象。

六、謹(jǐn)慎顯式調(diào)用GC

GC的開銷通常很大,而且它的運行具有不確定性,微軟的編程規(guī)范里是強烈建議你不要顯式調(diào)用GC。但你的代碼中還是可以使用framework中GC的某些方法進行手動回收,前提是你必須要深刻理解GC的回收原理,否則手動調(diào)用GC在特定場景下很容易干擾到GC的正?;厥丈踔烈氩豢深A(yù)知的錯誤。

比如如下代碼:

void SomeMethod()
  {
   object o1 = new Object();
   object o2 = new Object();

   o1.ToString();
   GC.Collect(); // this forces o2 into Gen1, because it's still referenced
   o2.ToString();
  }

如果沒有GC.Collect(),o1和o2都將在下一次垃圾自動回收中進入Gen0,但是加上GC.Collect(),o2將被標(biāo)記為Gen1,也就是0代回收沒有釋放o2占據(jù)的內(nèi)存

還有的情況是編程不規(guī)范可能導(dǎo)致死鎖,比如流傳很廣的一段代碼:

 public class MyClass
 {
  private bool isDisposed = false;

  ~MyClass()
  {
   Console.WriteLine("Enter destructor...");

   lock (this) //some situation lead to deadlock
   {
    if (!isDisposed)
    {
     Console.WriteLine("Do Stuff...");
    }
   }
  }
 }

通過如下代碼進行調(diào)用:

var instance = new MyClass();

   Monitor.Enter(instance);
   instance = null;

   GC.Collect();
   GC.WaitForPendingFinalizers();
   
   Console.WriteLine("instance is gabage collected");

上述代碼將會導(dǎo)致死鎖。原因分析如下:

1、客戶端主線程調(diào)用代碼Monitor.Enter(instance)代碼段lock住了instance實例

2、接著手動執(zhí)行GC回收,主(Finalizer)線程會執(zhí)行MyClass析構(gòu)函數(shù)

3、在MyClass析構(gòu)函數(shù)內(nèi)部,使用了lock (this)代碼,而主(Finalizer)線程還沒有釋放instance(也即這里的this),此時主線程只能等待

雖然嚴(yán)格來說,上述代碼并不是GC的錯,和多線程操作似乎也無關(guān),而是Lock使用不正確造成的。

同時請注意,GC的某些行為在Debug和Release模式下完全不同(Jeffrey Richter在<<CLR Via C#>>舉過一個Timer的例子說明這個問題)。比如上述代碼,在Debug模式下你可能發(fā)現(xiàn)它是正常運行的,而Release模式下則會死鎖。

七、當(dāng)GC遇到多線程

這一段主要參考<<CLR Via C#>>的線程劫持一節(jié)。

前面討論的垃圾回收算法有一個很大的前提就是:只在一個線程運行。而在現(xiàn)實開發(fā)中,經(jīng)常會出現(xiàn)多個線程同時訪問托管堆的情況,或至少會有多個線程同時操作堆中的對象。一個線程引發(fā)垃圾回收時,其它線程絕對不能訪問任何線程,因為垃圾回收器可能移動這些對象,更改它們的內(nèi)存位置。CLR想要進行垃圾回收時,會立即掛起執(zhí)行托管代碼中的所有線程,正在執(zhí)行非托管代碼的線程不會掛起。然后,CLR檢查每個線程的指令指針,判斷線程指向到哪里。接著,指令指針與JIT生成的表進行比較,判斷線程正在執(zhí)行什么代碼。

如果線程的指令指針恰好在一個表中標(biāo)記好的偏移位置,就說明該線程抵達了一個安全點。線程可在安全點安全地掛起,直至垃圾回收結(jié)束。如果線程指令指針不在表中標(biāo)記的偏移位置,則表明該線程不在安全點,CLR也就不會開始垃圾回收。在這種情況下,CLR就會劫持該線程。也就是說,CLR會修改該線程棧,使該線程指向一個CLR內(nèi)部的一個特殊函數(shù)。然后,線程恢復(fù)執(zhí)行。當(dāng)前的方法執(zhí)行完后,他就會執(zhí)行這個特殊函數(shù),這個特殊函數(shù)會將該線程安全地掛起。然而,線程有時長時間執(zhí)行當(dāng)前所在方法。所以,當(dāng)線程恢復(fù)執(zhí)行后,大約有250毫秒的時間嘗試劫持線程。過了這個時間,CLR會再次掛起線程,并檢查該線程的指令指針。如果線程已抵達一個安全點,垃圾回收就可以開始了。但是,如果線程還沒有抵達一個安全點,CLR就檢查是否調(diào)用了另一個方法。如果是,CLR再一次修改線程棧,以便從最近執(zhí)行的一個方法返回之后劫持線程。然后,CLR恢復(fù)線程,進行下一次劫持嘗試。所有線程都抵達安全點或被劫持之后,垃圾回收才能使用。垃圾回收完之后,所有線程都會恢復(fù),應(yīng)用程序繼續(xù)運行,被劫持的線程返回最初調(diào)用它們的方法。

實際應(yīng)用中,CLR大多數(shù)時候都是通過劫持線程來掛起線程,而不是根據(jù)JIT生成的表來判斷線程是否到達了一個安全點。之所以如此,原因是JIT生成表需要大量內(nèi)存,會增大工作集,進而嚴(yán)重影響性能。

概念敘述到此結(jié)束,手都抄軟了^_^,這書賣的貴和書里面的理論水平一樣有道理。

這里再說一個真實案例。某web應(yīng)用程序中大量使用Task,后在生產(chǎn)環(huán)境發(fā)生莫名其妙的現(xiàn)象,程序時靈時不靈,根據(jù)數(shù)據(jù)庫日志(其實還可以根據(jù)Windows事件跟蹤(ETW)、IIS日志以及dump文件),發(fā)現(xiàn)了Task執(zhí)行過程中有不規(guī)律的未處理的異常,分析后懷疑是CLR垃圾回收導(dǎo)致,當(dāng)然這種情況也只有在高并發(fā)條件下才會暴露出來。

八、開發(fā)中的一些建議和意見

由于GC的代價很大,平時開發(fā)中注意一些良好的編程習(xí)慣有可能對GC有積極正面的影響,否則有可能產(chǎn)生不良效果。

1、盡量不要new很大的object,大對象(>=85000Byte)直接歸為G2代,GC回收算法從來不對大對象堆(LOH)進行內(nèi)存壓縮整理,因為在堆中下移85000字節(jié)或更大的內(nèi)存塊會浪費太多CPU時間

2、不要頻繁的new生命周期很短object,這樣頻繁垃圾回收頻繁壓縮有可能會導(dǎo)致很多內(nèi)存碎片,可以使用設(shè)計良好穩(wěn)定運行的對象池(ObjectPool)技術(shù)來規(guī)避這種問題

3、使用更好的編程技巧,比如更好的算法、更優(yōu)的數(shù)據(jù)結(jié)構(gòu)、更佳的解決策略等等

update:.NET4.5.1及其以上版本已經(jīng)支持壓縮大對象堆,可通過System.Runtime.GCSettings.LargeObjectHeapCompactionMode進行控制實現(xiàn)需要壓縮LOH。可參考這里

根據(jù)經(jīng)驗,有時候編程思想里的空間換時間真不能亂用,用的不好,不但系統(tǒng)性能不能保證,說不定就會導(dǎo)致內(nèi)存溢出(Out Of Memory),關(guān)于OOM,可以參考我之前寫過的一篇文章有效預(yù)防.NET應(yīng)用程序OOM的經(jīng)驗備忘。

之前在維護一個系統(tǒng)的時候,發(fā)現(xiàn)有很多大數(shù)據(jù)量的處理邏輯,但竟然都沒有批量和分頁處理,隨著數(shù)據(jù)量的不斷膨脹,隱藏的問題會不斷暴露。然后我在重寫的時候,都按照批量多次的思路設(shè)計實現(xiàn),有了多線程、多進程和分布式集群技術(shù),再大的數(shù)據(jù)量也能很好處理,而且性能不會下降,系統(tǒng)也會變得更加穩(wěn)定可靠。

九、GC線程和Finalizer線程

GC在一個獨立的線程中運行來刪除不再被引用的內(nèi)存。

Finalizer則由另一個獨立(高優(yōu)先級CLR)線程來執(zhí)行Finalizer的對象的內(nèi)存回收。

對象的Finalizer被執(zhí)行的時間是在對象不再被引用后的某個不確定的時間,并非和C++中一樣在對象超出生命周期時立即執(zhí)行析構(gòu)函數(shù)。

GC把每一個需要執(zhí)行Finalizer的對象放到一個隊列(從終結(jié)列表移至freachable隊列)中去,然后啟動另一個線程而不是在GC執(zhí)行的線程來執(zhí)行所有這些Finalizer,GC線程繼續(xù)去刪除其他待回收的對象。

在下一個GC周期,這些執(zhí)行完Finalizer的對象的內(nèi)存才會被回收。也就是說一個實現(xiàn)了Finalize方法的對象必需等兩次GC才能被完全釋放。這也表明有Finalize的方法(Object默認(rèn)的不算)的對象會在GC中自動“延長”生存周期。

特別注意:負責(zé)調(diào)用Finalize的線程并不保證各個對象的Finalize的調(diào)用順序,這可能會帶來微妙的依賴性問題(見<<CLR Via C#>>一個有趣的依賴性問題)。

以上就是c#如何用好垃圾回收機制GC的詳細內(nèi)容,更多關(guān)于C# 垃圾回收機制GC的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論