2012-03-12 40 views
28

Tôi có một ứng dụng WPF đang gặp phải rất nhiều vấn đề về hiệu năng. Điều tồi tệ nhất là đôi khi ứng dụng chỉ đóng băng trong vài giây trước khi chạy lại.Giám sát Thùng rác trong C#

Tôi hiện đang gỡ lỗi ứng dụng để xem những gì đóng băng này có thể liên quan đến, và tôi tin rằng một trong những điều có thể gây ra nó là Bộ sưu tập rác. Vì ứng dụng của tôi đang chạy trong một môi trường rất hạn chế, tôi tin rằng Garbage Collector có thể sử dụng tất cả các tài nguyên của máy khi nó được chạy và không để lại cho ứng dụng của chúng ta.

Để kiểm tra giả thuyết này, tôi tìm thấy các bài viết sau: Garbage Collection NotificationsGarbage Collection Notifications in .NET 4.0, giải thích cách ứng dụng của tôi có thể được thông báo khi Bộ thu gom rác sẽ bắt đầu chạy và khi nó kết thúc.

Vì vậy, dựa trên những bài báo tôi đã tạo các lớp dưới đây để nhận được thông báo:

public sealed class GCMonitor 
{ 
    private static volatile GCMonitor instance; 
    private static object syncRoot = new object(); 

    private Thread gcMonitorThread; 
    private ThreadStart gcMonitorThreadStart; 

    private bool isRunning; 

    public static GCMonitor GetInstance() 
    { 
     if (instance == null) 
     { 
      lock (syncRoot) 
      { 
       instance = new GCMonitor(); 
      } 
     } 

     return instance; 
    } 

    private GCMonitor() 
    { 
     isRunning = false; 
     gcMonitorThreadStart = new ThreadStart(DoGCMonitoring); 
     gcMonitorThread = new Thread(gcMonitorThreadStart); 
    } 

    public void StartGCMonitoring() 
    { 
     if (!isRunning) 
     { 
      gcMonitorThread.Start(); 
      isRunning = true; 
      AllocationTest(); 
     } 
    } 

    private void DoGCMonitoring() 
    { 
     long beforeGC = 0; 
     long afterGC = 0; 

     try 
     { 

      while (true) 
      { 
       // Check for a notification of an approaching collection. 
       GCNotificationStatus s = GC.WaitForFullGCApproach(10000); 
       if (s == GCNotificationStatus.Succeeded) 
       { 
        //Call event 
        beforeGC = GC.GetTotalMemory(false); 
        LogHelper.Log.InfoFormat("===> GC <=== " + Environment.NewLine + "GC is about to begin. Memory before GC: %d", beforeGC); 
        GC.Collect(); 

       } 
       else if (s == GCNotificationStatus.Canceled) 
       { 
        LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event was cancelled"); 
       } 
       else if (s == GCNotificationStatus.Timeout) 
       { 
        LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event was timeout"); 
       } 
       else if (s == GCNotificationStatus.NotApplicable) 
       { 
        LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event was not applicable"); 
       } 
       else if (s == GCNotificationStatus.Failed) 
       { 
        LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC about to begin event failed"); 
       } 

       // Check for a notification of a completed collection. 
       s = GC.WaitForFullGCComplete(10000); 
       if (s == GCNotificationStatus.Succeeded) 
       { 
        //Call event 
        afterGC = GC.GetTotalMemory(false); 
        LogHelper.Log.InfoFormat("===> GC <=== " + Environment.NewLine + "GC has ended. Memory after GC: %d", afterGC); 

        long diff = beforeGC - afterGC; 

        if (diff > 0) 
        { 
         LogHelper.Log.InfoFormat("===> GC <=== " + Environment.NewLine + "Collected memory: %d", diff); 
        } 

       } 
       else if (s == GCNotificationStatus.Canceled) 
       { 
        LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event was cancelled"); 
       } 
       else if (s == GCNotificationStatus.Timeout) 
       { 
        LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event was timeout"); 
       } 
       else if (s == GCNotificationStatus.NotApplicable) 
       { 
        LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event was not applicable"); 
       } 
       else if (s == GCNotificationStatus.Failed) 
       { 
        LogHelper.Log.Info("===> GC <=== " + Environment.NewLine + "GC finished event failed"); 
       } 

       Thread.Sleep(1500); 
      } 
     } 
     catch (Exception e) 
     { 
      LogHelper.Log.Error(" ******************** Garbage Collector Error ************************ "); 
      LogHelper.LogAllErrorExceptions(e); 
      LogHelper.Log.Error(" ------------------- Garbage Collector Error --------------------- "); 
     } 
    } 

    private void AllocationTest() 
    { 
     // Start a thread using WaitForFullGCProc. 
     Thread stress = new Thread(() => 
     { 
      while (true) 
      { 
       List<char[]> lst = new List<char[]>(); 

       try 
       { 
        for (int i = 0; i <= 30; i++) 
        { 
         char[] bbb = new char[900000]; // creates a block of 1000 characters 
         lst.Add(bbb);    // Adding to list ensures that the object doesnt gets out of scope 
        } 

        Thread.Sleep(1000); 
       } 
       catch (Exception ex) 
       { 
        LogHelper.Log.Error(" ******************** Garbage Collector Error ************************ "); 
        LogHelper.LogAllErrorExceptions(e); 
        LogHelper.Log.Error(" ------------------- Garbage Collector Error --------------------- "); 
       } 
      } 


     }); 
     stress.Start(); 
    } 
} 

Và tôi đã thêm các tùy chọn gcConcurrent đến file app.config của tôi (dưới đây):

<?xml version="1.0"?> 
<configuration> 
    <configSections> 
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net-net-2.0"/> 
    </configSections> 

    <runtime> 
    <gcConcurrent enabled="false" /> 
    </runtime> 

    <log4net> 
    <appender name="Root.ALL" type="log4net.Appender.RollingFileAppender"> 
     <param name="File" value="../Logs/Root.All.log"/> 
     <param name="AppendToFile" value="true"/> 
     <param name="MaxSizeRollBackups" value="10"/> 
     <param name="MaximumFileSize" value="8388608"/> 
     <param name="RollingStyle" value="Size"/> 
     <param name="StaticLogFileName" value="true"/> 
     <layout type="log4net.Layout.PatternLayout"> 
     <param name="ConversionPattern" value="%date [%thread] %-5level - %message%newline"/> 
     </layout> 
    </appender> 
    <root> 
     <level value="ALL"/> 
     <appender-ref ref="Root.ALL"/> 
    </root> 
    </log4net> 

    <appSettings> 
    <add key="setting1" value="1"/> 
    <add key="setting2" value="2"/> 
    </appSettings> 
    <startup> 
    <supportedRuntime version="v2.0.50727"/> 
    </startup> 

</configuration> 

Tuy nhiên, bất cứ khi nào ứng dụng được thực hiện, có vẻ như không có thông báo nào được gửi mà Bộ thu gom rác sẽ chạy. Tôi đã đặt breakpoint trong DoGCMonitoring và nó xuất hiện rằng các điều kiện (s == GCNotificationStatus.Succeeded) và (s == GCNotificationStatus.Succeeded) không bao giờ hài lòng, do đó nội dung của các câu lệnh ifs không bao giờ được thực hiện.

Tôi đang làm gì sai?

Lưu ý: Tôi đang sử dụng C# với WPF và Khuôn khổ .NET 3.5.

CẬP NHẬT 1

cập nhật thử nghiệm GCMonitor tôi với phương pháp AllocationTest. Phương pháp này chỉ dành cho mục đích thử nghiệm. Tôi chỉ muốn đảm bảo rằng đủ bộ nhớ đã được cấp phát để buộc Garbage Collector chạy.

UPDATE 2

Cập nhật phương pháp DoGCMonitoring, với kiểm tra mới về sự trở lại của các phương pháp WaitForFullGCApproach và WaitForFullGCComplete. Từ những gì tôi đã thấy cho đến nay ứng dụng của tôi sẽ trực tiếp đến điều kiện (s == GCNotificationStatus.NotApplicable). Vì vậy, tôi nghĩ rằng tôi có một số cấu hình sai ở đâu đó ngăn tôi lấy kết quả mong muốn.

Tài liệu cho GCNotificationStatus enum có thể được tìm thấy here.

+3

Bạn đã thử thực sự lược tả nó với một công cụ, nói điều gì đó giống như màn hình hiệu suất cửa sổ hoặc windbg - thay vì cố gắng để viết một wrapper GC? –

+0

Có thể GC chưa chạy (chưa). Bạn có thể hiển thị AllocationTest() không? –

+0

Xin chào, tôi thực sự có một công cụ định hình, tuy nhiên vấn đề đóng băng mà tôi đã đề cập trước đó đang xảy ra trong môi trường sản xuất chứ không phải trên máy của tôi (tôi không thể tái tạo nó). Và thật không may cho tôi, tôi không thể chạy công cụ lược tả trong môi trường sản xuất. – Felipe

Trả lời

38

Tôi không thấy GC.RegisterForFullGCNotification(int,int) ở bất kỳ đâu trong mã của bạn. Có vẻ như bạn đang sử dụng các phương thức WaitForFullGC[xxx] nhưng không bao giờ đăng ký nhận thông báo. Đó có thể là lý do tại sao bạn nhận được trạng thái NotApplicable. Tuy nhiên, tôi nghi ngờ rằng GC là vấn đề của bạn, trong khi có thể, tôi cho rằng nó sẽ là tốt để biết về tất cả các chế độ GC có và cách tốt nhất để xác định những gì đang xảy ra. Có hai chế độ của Bộ sưu tập rác trong .NET: máy chủ và máy trạm. Cả hai đều thu thập cùng một bộ nhớ không sử dụng, tuy nhiên cách nó được thực hiện là bao giờ nên hơi khác nhau.

  • Server phiên bản - Chế độ này kể GC cho bạn đang sử dụng một ứng dụng phía máy chủ, và nó cố gắng để tối ưu hóa bộ sưu tập cho các kịch bản. Nó sẽ chia heap thành nhiều phần, 1 cho mỗi CPU. Khi GC được khởi động, nó sẽ chạy một luồng trên mỗi CPU song song. Bạn thực sự muốn nhiều CPU cho việc này hoạt động tốt. Trong khi phiên bản máy chủ sử dụng nhiều luồng cho GC, nó không giống như chế độ GC máy trạm đồng thời được liệt kê bên dưới. Mỗi chuỗi hoạt động giống như phiên bản không đồng thời.

  • Phiên bản máy trạm - Chế độ này cho biết bạn đang sử dụng ứng dụng phía máy khách. Nó cho biết bạn có nhiều tài nguyên hạn chế hơn phiên bản Server, và do đó chỉ có một luồng GC. Tuy nhiên, có hai cấu hình của phiên bản Workstation: đồng thời và không đồng thời.

    • đồng thời - Đây là phiên bản bật theo mặc định mỗi khi GC máy trạm được sử dụng (điều này sẽ là trường hợp cho các ứng dụng WPF của bạn). GC luôn chạy trên một chuỗi riêng biệt luôn đánh dấu các đối tượng để thu thập khi ứng dụng đang chạy. Hơn nữa, nó chọn có hay không để thu gọn bộ nhớ trong các thế hệ nhất định, và làm cho sự lựa chọn đó dựa trên hiệu suất. Nó vẫn phải đóng băng tất cả các chủ đề để chạy một bộ sưu tập nếu nén được thực hiện, nhưng bạn sẽ hầu như không bao giờ nhìn thấy một ứng dụng không phản hồi khi sử dụng chế độ này. Điều này tạo ra trải nghiệm tương tác tốt hơn cho việc sử dụng và tốt nhất cho các ứng dụng giao diện điều khiển hoặc GUI.
    • Không đồng thời - Đây là phiên bản bạn có thể định cấu hình ứng dụng của mình để sử dụng, nếu bạn muốn. Trong chế độ này, luồng GC ngủ cho đến khi GC bắt đầu, sau đó nó đi và đánh dấu tất cả các cây đối tượng là rác, giải phóng bộ nhớ, nén nó, tất cả trong khi tất cả các chuỗi khác bị treo. Điều này có thể khiến ứng dụng đôi khi trở nên không phản hồi trong thời gian ngắn ngắn.

Bạn không thể đăng ký thông báo trên bộ thu đồng thời vì được thực hiện trong nền. Có thể ứng dụng của bạn không sử dụng trình thu thập đồng thời (tôi nhận thấy bạn đã vô hiệu hóa gcConcurrent trong app.config, nhưng có vẻ như đó chỉ là để thử nghiệm?). Nếu đúng như vậy, bạn chắc chắn có thể thấy ứng dụng của mình bị đóng băng nếu có bộ sưu tập nặng. Đây là lý do tại sao họ tạo ra các nhà sưu tập đồng thời. Loại chế độ GC có thể được thiết lập một phần trong mã và được thiết lập đầy đủ trong cấu hình ứng dụng và cấu hình máy.

Chúng ta có thể làm gì để tìm ra chính xác những gì ứng dụng của chúng ta đang sử dụng? Khi chạy, bạn có thể truy vấn lớp tĩnh GCSettings (trong System.Runtime). GCSettings.IsServerGC sẽ cho bạn biết nếu bạn đang chạy trạm làm việc trên các phiên bản máy chủ và GCSettings.LatencyMode có thể cho bạn biết liệu bạn có đang sử dụng đồng thời, không đồng thời hoặc đặc biệt mà bạn phải đặt trong mã không thực sự áp dụng ở đây không. Tôi nghĩ rằng đó sẽ là một nơi tốt để bắt đầu, và có thể giải thích lý do tại sao nó chạy tốt trên máy tính của bạn, nhưng không phải là sản xuất.

Trong tệp cấu hình, <gcConcurrent enabled="true|false"/> hoặc <gcServer enabled="true|false"/> kiểm soát các chế độ của bộ thu gom rác. Hãy ghi nhớ điều này có thể nằm trong tệp app.config của bạn (nằm bên cạnh cụm lắp ráp) hoặc trong máy.tệp cấu hình, nằm ở %windir%\Microsoft.NET\Framework\[version]\CONFIG\

Bạn cũng có thể sử dụng Windows Monitor Monitor để truy cập các bộ đếm hiệu suất của máy sản xuất để thu gom rác .NET và xem các thống kê đó. Bạn có thể làm tương tự với Event Tracing cho Windows (ETW) tất cả từ xa. Đối với màn hình hiệu suất, bạn muốn đối tượng .NET CLR Memory và chọn ứng dụng của bạn trong hộp danh sách các trường hợp.

+1

Xin chào, tôi đã làm thực sự quên dòng GC.RegisterForFullGCNotification (int, int) ... Tôi cảm thấy một chút ngu ngốc ngay bây giờ. Dù sao cảm ơn cho câu trả lời rất hoàn chỉnh của bạn! – Felipe