簡體   English   中英

GC.AddMemoryPressure()不足以按時觸發Finalizer隊列

[英]GC.AddMemoryPressure() not enough to trigger the Finalizer queue execution on time

我們為用C#編寫的多媒體匹配項目編寫了一個自定義索引引擎。

索引引擎是用非托管C++編寫的,可以以std:: collections和容器的形式保存大量非托管內存。

每個非托管索引實例都由托管對象包裝; unamanaged索引的生存期由托管包裝器的生命周期控制。

我們已經確保(通過自定義,跟蹤C ++分配器)正在考慮索引內部消耗的每個字節,並且我們使用此值的增量更新(每秒10次)托管垃圾收集器的內存壓力值(正增量調用GC.AddMemoryPressure() ,負增量調用GC.RemoveMemoryPressure() )。

這些索引是線程安全的,並且可以由許多C#worker共享,因此可能有多個引用用於同一索引。 出於這個原因,我們不能自由地調用Dispose() ,而是依賴垃圾收集器來跟蹤引用共享,並最終在工作進程未使用它們時觸發索引的最終確定。

現在,問題是我們的內存不足 事實上,完整的集合通常是相對經常執行的,但是,在內存分析器的幫助下,我們可以發現大量的“死”索引實例被保存在完成隊列中,在這個過程中,當進程耗盡內存后分頁文件。

如果我們在低內存條件下添加一個調用GC::WaitForPendingFinalizers()后跟GC::Collect()的監視程序線程,我們實際上可以避免這個問題,但是,從我們讀過的,手動調用GC::Collect()嚴重破壞垃圾收集效率,我們不希望如此。

我們甚至添加了一個悲觀的壓力因素(嘗試高達4倍)來誇大報告給.net端的非托管內存量,看看我們是否可以哄騙垃圾收集器來更快地清空隊列。 似乎處理隊列的線程完全沒有意識到內存壓力。

此時我們覺得我們需要在計數達到零時實現一個手動引用計數Dispose() ,但這似乎是一種矯枉過正,特別是因為內存壓力API的整個目的正是為了解決像我們的。

一些事實:

  • .Net版本是4.5
  • 應用程序處於64位模式
  • 垃圾收集器以並發服務器模式運行。
  • 索引的大小是~800MB的非托管內存
  • 在任何時間點都可以有多達12個“活着”的索引。
  • 服務器有64GB的RAM

歡迎任何想法或建議

好吧,沒有答案,但“如果你想明確地處理外部資源,你必須自己做”。

AddMemoryPressure()方法不保證立即觸發垃圾回收。 相反,CLR使用非托管內存分配/釋放統計來調整它自己的gc閾值,只有在認為合適的情況下才會觸發GC。

請注意, RemoveMemoryPressure()根本不會觸發GC(理論上它可以通過設置GCX_PREEMP等操作的副作用實現它,但為了簡潔起見,我們跳過它)。 相反,它降低了當前的壓力值,僅此而已(再次簡化)。

實際算法沒有記錄,但您可以從CoreCLR查看實現。 簡而言之,您的bytesAllocated值必須超過某個動態計算的限制,然后CLR bytesAllocated觸發GC。

現在壞消息:

  • 在真實應用程序中,由於每個GC集合和每個第三方代碼都會對GC限制產生影響,因此該過程完全不可預測。 可以調用GC,可以稍后調用,可能根本不調用

  • GC調整它限制嘗試最小化昂貴的GC2集合(當你使用長壽命的索引對象時,你對它們感興趣,因為終結器它們總是被提升到下一代)。 因此,DDOS運行時具有巨大的內存壓力值可能會反擊,因為您將提高標准值以使(幾乎)沒有機會通過設置內存壓力來觸發GC。 注意:最后一個問題將使用新的AddMemoryPressure()實現修復,但不是今天,絕對是)。

UPD:更多細節。

好的,讓我們繼續:)

第2部分,或“更新低估_udocumented_的意思”

正如我上面所說,當您使用長壽命對象時,您對GC 2集合感興趣。

眾所周知,終結器在對象進行GC編輯后幾乎立即運行(假設終結器隊列未填充其他對象)。 作為證明:只需運行這個要點

索引未被釋放的真正原因非常明顯:對象所屬的生成不是GCed。 現在我們回到最初的問題。 您如何看待,您需要分配多少內存才能觸發GC2集合?

正如我上面所說,實際數字沒有記錄。 理論上,在消耗非常大的內存塊之前,可能根本不會調用GC2。 而現在真正的壞消息是:對於服務器GC“在理論上”和“真正發生的事情”是相同的。

還有一個要點 ,在.Net4.6 x64上,輸出結果與此類似:

GC low latency:
Allocated, MB:   512.19          GC gen 0|1|2, MB:   194.19 |   317.81 |     0.00        GC count 0-1-2: 1-0-0
Allocated, MB: 1,024.38          GC gen 0|1|2, MB:   421.19 |   399.56 |   203.25        GC count 0-1-2: 2-1-0
Allocated, MB: 1,536.56          GC gen 0|1|2, MB:   446.44 |   901.44 |   188.13        GC count 0-1-2: 3-1-0
Allocated, MB: 2,048.75          GC gen 0|1|2, MB:   258.56 | 1,569.75 |   219.69        GC count 0-1-2: 4-1-0
Allocated, MB: 2,560.94          GC gen 0|1|2, MB:   623.00 | 1,657.56 |   279.44        GC count 0-1-2: 4-1-0
Allocated, MB: 3,073.13          GC gen 0|1|2, MB:   563.63 | 2,273.50 |   234.88        GC count 0-1-2: 5-1-0
Allocated, MB: 3,585.31          GC gen 0|1|2, MB:   309.19 |   723.75 | 2,551.06        GC count 0-1-2: 6-2-1
Allocated, MB: 4,097.50          GC gen 0|1|2, MB:   686.69 |   728.00 | 2,681.31        GC count 0-1-2: 6-2-1
Allocated, MB: 4,609.69          GC gen 0|1|2, MB:   593.63 | 1,465.44 | 2,548.94        GC count 0-1-2: 7-2-1
Allocated, MB: 5,121.88          GC gen 0|1|2, MB:   293.19 | 2,229.38 | 2,597.44        GC count 0-1-2: 8-2-1

沒錯,在最壞的情況下你必須分配~3.5 gig來觸發GC2集合。 我很確定你的分配要小得多:)

注意:請注意,處理GC1生成中的對象並不會讓它變得更好。 GC0段的大小可能超過500mb。 你必須非常努力地在ServerGC上觸發垃圾收集:)

簡介:使用Add / RemoveMemoryPressure的方法(幾乎)不會影響垃圾收集頻率,至少在服務器GC上是這樣。

現在,問題的最后一部分:我們有什么可能的解決方案? 簡而言之,最簡單的方法是通過一次性包裝進行重新計數。

未完待續

我們可以在終結隊列中找到大量的“死”索引實例

沒有任何意義,這些“死”的實例沒有最終確定。 畢竟,你發現GC :: WaitForPendingFinalizers()實際上是有效的。 所以這里必須要做的是它們實際上是最終確定的,它們只是在等待下一個集合運行以便它們可以被銷毀。 這需要一段時間。 是的,這並非不可能,畢竟你已經為他們調用了GC :: RemoveMemoryPressure()。 並且,希望為他們發布了大量的非托管分配。

所以這肯定只是一個錯誤的信號,這些對象只占用GC堆,而不是非托管堆和GC堆不是你的問題。

我們確保(通過自定義,跟蹤C ++分配器)每個字節......

我不太喜歡那種聲音。 非常重要的是GC調用與實際創建和完成托管對象有一些對應關系。 很簡單,在調用C ++ delete操作符之后,在構造函數中調用AddMemoryPressure,在終結器中調用RemoveMemoryPressure。 您傳遞的值只需要是對應的C ++非托管分配的估計值,它不必精確到字節,關閉2倍並不是一個嚴重的問題。 C ++分配稍后發生也沒關系。

手動調用GC :: Collect()會嚴重破壞垃圾回收效率

不要驚慌。 非常高的賠率,因為你的非托管分配如此之大,你很少“自然地”收集並且實際上需要強制分配。 就像GC :: AddMemoryPressure()觸發的類型一樣,它就像調用GC :: Collect()一樣“強制”。 雖然它有一個啟發式,避免過於頻繁收集,你現在可能不會特別關心:)

垃圾收集器以並發服務器模式運行

不要,使用工作站GC,它對堆段大小更加保守。

我想建議簡要介紹一下“ 終結器不能保證運行 ”。 您可以通過自己連續生成好的舊Bitmap來輕松測試它:

private void genButton_Click(object sender, EventArgs e)
{
    Task.Run(() => GenerateNewBitmap());
}

private void GenerateNewBitmap()
{
    //Changing size also changes collection behavior
    //If this is a small bitmap then collection happens
    var size = picBox.Size;
    Bitmap bmp = new Bitmap(size.Width, size.Height);
    //Generate some pixels and Invoke it onto UI if you wish
    picBox.Invoke((Action)(() => { picBox.Image = bmp; }));
    //Call again for an infinite loop
    Task.Run(() => GenerateNewBitmap());
}

在我的機器上,如果我生成超過500K像素,我無法永遠生成,.NET給了我一個OutOfMemoryException 這個關於Bitmap類的東西在2005年是真的,它在2015年仍然是正確的。 Bitmap類很重要,因為它存在於庫中很長一段時間。 有錯誤修復,一路上的性能改進,我認為如果它不能做我需要的東西,那么我需要改變我的需要

首先,關於一次性物體的事情是你需要自己調用Dispose 不,你真的需要自己打電話。 說真的 我建議在VisualStudio的代碼分析和適當using等方面啟用相關規則。

其次,調用Dispose方法並不意味着在非托管端調用delete (或free )。 我所做的,我認為你應該使用引用計數 如果您的非托管端使用C ++,那么我建議使用shared_ptr 自VS2012以來,據我所知,VisualStudio支持shared_ptr

因此,通過引用計數,在托管對象上調用Dispose減少非托管對象的引用計數,並且只有在引用計數減少到零時才會刪除非托管內存。

暫無
暫無

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

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