簡體   English   中英

Gen2集合並不總是收集死對象?

[英]Gen2 collection not always collecting dead objects?

通過在過去幾天監視全新.NET 4.5服務器應用程序的CLR #Bytes in all Heaps性能計數器中的CLR #Bytes in all Heaps ,我可以注意到一種模式讓我認為Gen2集合並不總是收集死對象,但我遇到了麻煩了解到底發生了什么。

服務器應用程序使用Server GC / Background.NET Framework 4.5.1中運行

這是一個托管為Windows服務的控制台應用程序(在Topshelf框架的幫助下)

服務器應用程序正在處理消息,並且吞吐量在某種程度上是不變的。

我可以看到CLR #Bytes in all HeapsCLR #Bytes in all Heaps圖表是內存開始大約為18MB,然后在大約20-24小時內增長到35MB(在此時間范圍內有20-30個Gen2集合),然后全部突然回落到18MB的名義價值,然后在20-24小時內再次增長到~35MB並回落到18MB,依此類推(我可以看到應用程序現在正在運行的過去6天重復的模式) ...內存的增長不是線性的,大約需要5個小時才能增長10MB,然后剩下的10 MB大約需要15-17個小時。

我可以通過查看#Gen0/#Gen1/#Gen2 collections perfmon計數器來看到,在20-24小時內(可能是30 #Gen0/#Gen1/#Gen2 collections ,一堆Gen2集合正在進行,並且沒有一個會使內存下降回到名義上的18MB。 然而,奇怪的是通過使用外部工具強制GC(在我的情況下為Perfview),然后我可以看到#Induced GC上升1(GC.Collect被調用,所以這是正常的)並且內存即將進行回到名義上的18MB。

這讓我想到#Gen2系列的perfmon計數器不對,只有一個Gen2系列在20-22小時后發生(meeehhh我真的不這么認為)或者Gen2系列並不總是收集死亡對象(看起來似乎更合理)......但在這種情況下,為什么會通過GC.Collect強制GC執行操作,在應用程序的生命周期中明確調用GC.Collect與自動觸發的集合之間的區別是什么。

我確信有一個非常好的解釋但是從我發現的關於GC的文檔的不同來源很少:( - Gen2集合在任何情況下都收集死對象。所以也許文檔不是最新的或者我讀錯了...歡迎任何解釋。謝謝!

編輯:請在4天內#Bytes in all heaps圖中#Bytes in all heaps屏幕截圖

(點擊查看大圖)
圖形

這比試圖描繪你頭腦中的東西更容易。 您在圖表上看到的就是我上面所說的...內存增加超過20-24小時(以及在此時間范圍內20-30 Gen2集合),直到達到~35MB然后突然下降。 您將在圖表的末尾注意到,通過外部工具觸發的誘導GC I會立即將內存降回到標稱值。

編輯#2:我在代碼中做了很多清理,主要是關於終結器。 我有很多關於一次性類型的類,所以我必須在這些類型上實現IDisposable 然而,在任何情況下,我都被一些文章誤導為使用Finalizer實現Diposable模式。 在閱讀了一些MSDN文檔之后,我開始明白只有當類型本身擁有本機資源時才需要終結器(在這種情況下,使用SafeHandle可以避免這種情況)。 所以我從所有這些類型中刪除了所有終結器。 代碼中還有一些其他的修改,但主要是業務邏輯,沒有“.NET框架”相關。 現在圖表非常不同,這是一個平坦的線路,現在已經持續了20天......正是我期待看到的! 所以這個問題現在已經解決了,但是我仍然不知道是什么問題導致......似乎它可能與終結器有關,但仍然無法解釋我注意到的內容,即使我們沒有調用Dispose (true) - 壓縮終結器 - 終結器線程應該在收集之間啟動而不是每20-24小時? 考慮到我們現在已經擺脫了問題,需要時間回到“越野車”版本並再次重現它。 我可能會嘗試做一段時間,然后深究它。

編輯:添加了Gen2集合圖(點擊查看大圖)

圖形

http://msdn.microsoft.com/en-us/library/ee787088%28v=VS.110%29.aspx#workstation_and_server_garbage_collection

垃圾收集的條件

當滿足下列條件之一時,將發生垃圾收集:

  • 系統物理內存較低。

  • 托管堆上已分配對象使用的內存超過可接受的閾值。 在該過程運行時不斷調整該閾值。

  • 調用GC.Collect方法。 幾乎在所有情況下,您都不必調用此方法,因為垃圾收集器會持續運行。 此方法主要用於獨特的情況和測試。

看來你正在擊中第二個,35是門檻。 如果35很大,您應該能夠將閾值配置為其他值。


gen2集合沒有任何特殊之處會導致它們偏離這些規則。 (比照https://stackoverflow.com/a/8582251/215752

你的任何物體都是“大”物體嗎? 有一個單獨的“大對象堆”,它有不同的規則

大對象堆碎片

盡管如此,它在4.5.1中得到了改進:

http://blogs.msdn.com/b/dotnet/archive/2011/10/04/large-object-heap-improvements-in-net-4-5.aspx

如果啟用了gcTrimCommitOnLowMemory則可以很容易地解釋這一點。 通常,GC會為進程分配一些額外的內存。 但是,當內存達到某個閾值時,GC將“修剪”額外的內存。

來自文檔:

啟用gcTrimCommitOnLowMemory設置后,垃圾回收器將評估系統內存負載,並在負載達到90%時進入修剪模式。 它保持修整模式,直到負載降至85%以下。

這可以很容易地解釋你的場景 - 在你的應用程序達到某個點之前保留(和使用)內存儲備,這似乎是每20-24小時一次,此時檢測到90%的負載,並且內存是修剪到最低要求(18mb)。

閱讀你的第一個版本我會說這是一種正常行為。

...但是在這種情況下,為什么會通過GC.Collect強制GC執行操作,在應用程序的生命周期中明確調用GC.Collect與自動觸發的集合之間的區別是什么。

有兩種類型的集合,完整集合和部分集合。 自動觸發的功能是部分集合,但在調用GC.Collect時,它將執行完整集合。

同時,我可能有理由說你現在告訴我們你在所有對象上都使用了終結器。 如果出於任何原因將其中一個對象提升為#2 Gen,則終結器僅在執行#2 Gen集合時運行。

以下示例將演示我剛才所說的內容。

public class ClassWithFinalizer 
{
    ~ClassWithFinalizer()
    {
        Console.WriteLine("hello from finalizer");
        //do nothing
    }
}

static void Main(string[] args)
{
    ClassWithFinalizer a = new ClassWithFinalizer();
    Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));
    GC.Collect();
    Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));
    GC.Collect();
    Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));

    a = null;

    Console.WriteLine("Collecting 0 Gen");
    GC.Collect(0);
    GC.WaitForPendingFinalizers();

    Console.WriteLine("Collecting 0 and 1 Gen");
    GC.Collect(1);
    GC.WaitForPendingFinalizers();

    Console.WriteLine("Collecting 0, 1 and 2 Gen");
    GC.Collect(2);
    GC.WaitForPendingFinalizers();

    Console.Read();
}

輸出將是:

Class a is on #0 generation
Class a is on #1 generation
Class a is on #2 generation
Collecting 0 Gen
Collecting 0 and 1 Gen
Collecting 0, 1 and 2 Gen
hello from finalizer

如您所見,只有在對象生成的集合上進行集合時,才會回收具有終結器的對象的內存。

我只想在這里投入2美分。 我不是這方面的專家,但也許這可能有助於你的調查。

如果您使用的是64位平台,請嘗試將其添加到.config文件中。 我讀到這可能是一個問題。

<configuration>
    <runtime>
        <gcAllowVeryLargeObjects enabled="true" />
    </runtime>
</configuration>

我要指出的唯一另一件事是,如果您控制源代碼,您可以通過內部故障排除來證明您的假設。

按照你的應用程序的主內存消耗類調用某些東西,並將其設置為按時間間隔運行,可以揭示真正發生的事情。

private void LogGCState() {
    int gen = GC.GetGeneration(this);

    //------------------------------------------
    // Comment out the GC.GetTotalMemory(true) line to see what's happening 
    // without any interference
    //------------------------------------------
    StringBuilder sb = new StringBuilder();
    sb.Append(DateTime.Now.ToString("G")).Append('\t');
    sb.Append("MaxGens: ").Append(GC.MaxGeneration).Append('\t');
    sb.Append("CurGen: ").Append(gen).Append('\t');
    sb.Append("CurGenCount: ").Append(GC.CollectionCount(gen)).Append('\t');
    sb.Append("TotalMemory: ").Append(GC.GetTotalMemory(false)).Append('\t');
    sb.Append("AfterCollect: ").Append(GC.GetTotalMemory(true)).Append("\r\n");

    File.AppendAllText(@"C:\GCLog.txt", sb.ToString());

}

也有一個相當不錯的文章在這里使用的GC.RegisterForFullGCNotification方法。 顯然,這將使您能夠包括完整集合的時間跨度,以便您可以根據您的特定需求調整性能和收集頻率。 此方法還允許您指定堆閾值以觸發通知(或集合?)。

也許還有一種方法可以在apps .config文件中設置它,但我還沒看過。 目前,對於服務器應用程序來說,35MB的占用空間非常小。 哎呀,我的網頁瀏覽器有時會達到300-400MB :)所以,框架可能只看到35MB作為釋放內存的一個很好的默認點。

無論如何,我可以通過你的問題的深思熟慮告訴我,我可能只是指出了顯而易見的事實。 但是,值得一提的是。 祝你好運!。

在一個有趣的說明

在這篇文章的頂部,我最初寫的是“if(你使用的是64位平台)”。 那讓我搞砸了。 照顧自己!

我的WPF應用程序中的情況完全相同。 我的代碼中沒有終結器。 然而,正在進行的GC似乎實際上收集了Gen 2對象。 我可以看到GC.GetTotalMemory()結果在Gen2集合觸發后減少了150mb。

所以我的印象是Gen2堆大小不顯示活動對象使用的字節數。 它只是為Gen2目的分配的堆大小或字節數。 那里你可能有足夠的空閑記憶。

在某些條件下(不是每個第2代集合),此堆大小將被修剪。 在這個特殊的時刻,我的應用程序獲得了巨大的性能損失 - 它可能會掛起幾秒鍾。 想知道為什么......

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM