Сборщик мусора c# не очищает память

145
02 августа 2019, 02:50

Есть статичный метод

private static bool GetXml(string date)
{
    try
    {
        //Подключаемся к сервису
        KBODataProviderClient client = new KBODataProviderClient();
        //Получаем данные
        string[] result = client.getKBOContracts("2018-12-25", null);
        //Если данных нет
        if (result == null || result.Length <= 0)
        {
            log.InfoFormat("[{0}]|Pbo return 0 rows", runGuid);
            return false;
        }
        if (File.Exists(filePath))
            File.Delete(filePath);
        //Сохраняем XML
        using (StreamWriter sw = new StreamWriter(filePath, true, Encoding.UTF8))
        {
            //пишем первую запись с заголовков
            sw.WriteLine("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
            sw.WriteLine("<CONTRACTS>");
            for (int i = 0; i < result.Length; i++)
            {
                //удаляем заголовок
                string temp = result[i].Replace("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>", "");
                sw.WriteLine(temp);
            }
            sw.WriteLine("</CONTRACTS>");
        }

        log.InfoFormat("[{0}]|Create XML success", runGuid);
        return true;
    }
    catch (Exception ex)
    {
        log.ErrorFormat("[{0}]|GetXml error {1}", runGuid, ex + Environment.NewLine + ex.StackTrace);
        return false;
    }
}

При вызове метода он подключается к WCF сервису, оттуда приходит массив строк (возвращает string[]), который я потом пишу в файлик. Проблема в том, что при получении данных разумеется съедается память, и после завершения метода память не очищается.

Собственно, я знаю то, что переменная string[] result является временной переменной и после завершения метода она должна собраться сборщиком мусора, но этого не происходит (данные занимают около 400мб в памяти)

Первым, что пришло в голову это проблема из-за статичного метода. Тогда я решил потереть ссылку

result = null;
GC.Collect();

и вызвать сборщик самому. Но и тут эффекта 0. В чем проблема?

upd:

Использование метода идет так:

if (GetXml(date))
{
    //парсинг
}

сам метод дополнил так:

private static bool GetXml(string date)
{
    try
    {
        //Подключаемся к сервису
        KBODataProviderClient client = new KBODataProviderClient();
        //Получаем данные
        string[] result = client.getKBOContracts("2018-12-25", null);
        //Если данных нет
        if (result == null || result.Length <= 0)
        {
            log.InfoFormat("[{0}]|Pbo return 0 rows", runGuid);
            return false;
        }
        if (File.Exists(filePath))
            File.Delete(filePath);
        //Сохраняем XML
        using (StreamWriter sw = new StreamWriter(filePath, true, Encoding.UTF8))
        {
            //пишем первую запись с заголовков
            sw.WriteLine("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
            sw.WriteLine("<CONTRACTS>");
            for (int i = 0; i < result.Length; i++)
            {
                //удаляем заголовок
                string temp = result[i].Replace("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>", "");
                sw.WriteLine(temp);
            }
            sw.WriteLine("</CONTRACTS>");
        }
        log.InfoFormat("[{0}]|Create XML success", runGuid);
        client.Close();
        client = null;
        result = null;
        GC.Collect();
        return true;
    }
    catch (Exception ex)
    {
        log.ErrorFormat("[{0}]|GetXml error {1}", runGuid, ex + Environment.NewLine + ex.StackTrace);
        return false;
    }
}

Клиент сервиса я закрываю и на всякий даже null присваиваю. Далее присваиваю null строке и вызываю сборщик.

В итоге при получении данных память увеличивается до 320мб(+- офк) и после завершения падает до +-211 и продолжает работать.

Метод парсинга не занимает столько памяти, т.к без получения данных при парсинге память остается на уровне +-60мб.

попробовал Array.Clear, так-же 0 эффекта (скрин выше)

Answer 1

Проблема решена.

Я решил проверить все дело на левых проектах потихоньку перенося код и оказалось чудом, когда на новом проекте память очищалась нормально(требовалось лишь вызвать GC.Collect())

Первым делом я решил проверить версии .net, на проекте где все работало был 4.7.1, но изменение версии не помогло. Тогда я начал в буквальном смысле добавлять по строчке и сверять код.

Причина оказалось в app.config, а именно в секции которую генерирует VS при добавлении ссылки на сервис (там указываются таймауты и прочее). Конкретно проблема была в том, что таймауты (CloseTimeout, OpenTimeout, ReceiveTimeout, SendTimeout) указывались в привязке (секция basicHttpBinding).

Если я указывал эти таймауты в этой секции - память не очищалась (ни в основном, ни в каких других проектах), т.е если делал так:

<basicHttpBinding>
  <binding name="KBODataProviderPortBinding" closeTimeout="00:30:00" openTimeout="00:30:00" receiveTimeout="00:30:00" sendTimeout="00:30:00" maxBufferPoolSize="600000000" maxBufferSize="600000000" maxReceivedMessageSize="600000000"/>
</basicHttpBinding>

но если я указывал таймауты через код:

client.Endpoint.Binding.CloseTimeout = new TimeSpan(0, 30, 0);
client.Endpoint.Binding.OpenTimeout = new TimeSpan(0, 30, 0);
client.Endpoint.Binding.ReceiveTimeout = new TimeSpan(0, 30, 0);
client.Endpoint.Binding.SendTimeout = new TimeSpan(0, 30, 0);

то сборщик мусора успешно собирал мусор.

Чтобы сделать настройку времени из конфига я воспользовался ConfigurationManager

<appSettings>
  <add key="timeout" value="30"/>
</appSettings>

тогда в коде я делаю так:

//Получаем время, переводим его в TimeSpan (если указать их в конфиге, то память не будет очищена)
TimeSpan time = new TimeSpan(TimeSpan.TicksPerMinute * int.Parse(ConfigurationManager.AppSettings.Get("timeout")));
client.Endpoint.Binding.CloseTimeout = time;
client.Endpoint.Binding.OpenTimeout = time;
client.Endpoint.Binding.ReceiveTimeout = time;
client.Endpoint.Binding.SendTimeout = time;

и сборщик мусора так-же успешно очищает память

Answer 2

Все языки, использующие сборщик мусора, известны своей нелюбовью возвращать память системе. Проще всего с этим смириться: как правило, неиспользуемая память быстро оседает в файле подкачки и никому там не мешает. Проблемы начинаются только когда уже и файл подкачки заканчивается.

Однако, есть несколько способов, позволяющих принудительно освободить память.

Способ 1 - отдельный процесс.

Можно вынести вызов GetXml в отдельную программу, которая сделает всю тяжелую по памяти работу и завершится.

Это - самый медленный способ (он медленнее даже чем ничего не делать), но он позволяет быстро решить проблему с нехваткой памяти.

Способ 2 - поточная обработка.

Этот способ требует переписывания клиента так, чтобы он не возвращал не список строк, а работал с потоками ввода-вывода. Идея в том, чтобы таскать всюду за собой Stream (или PipeReader, см. библиотеку System.Threading.Pipelines), и никогда не хранить в памяти более, к примеру, четырех килобайтов данных.

Достоинство этого способа - очень низкое потребление памяти если сделать всё правильно. Недостаток же - в том, что такие операции, как "разделить поток на две части" или "выкусить декларацию XML из начала документа", требуют сложных преобразований архитектуры.

Так, к примеру, вполне возможно что вам не удастся получить список потоков от клиента, и вместо этого придется подписываться на событие. Это может выглядеть как-то так:

using (StreamWriter sw = new StreamWriter(filePath, true, Encoding.UTF8))
{
    sw.WriteLine("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
    sw.WriteLine("<CONTRACTS>");
    var client = new KBODataProviderClient();
    client.ProcessResult += stream => 
    {
        sw.Flush();
        stream.Position += Encoding.UTF8.GetByteCount("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
        stream.CopyTo(sw.BaseStream);
    };
    client.getKBOContracts("2018-12-25", null);
    sw.WriteLine("</CONTRACTS>");
}

Способ 3 - неуправляемая память

Этот способ также требует переписывания клиента. Он может оказаться как проще поточной обработки (за счет более простых алгоритмов), так и сложнее (за счет более сложного API). Недостатком этого способа является непереносимость, так как он завязывается на конкретное API платформы.

Нужно выделить кусок неуправляемой памяти через P/Invoke (лучше всего использовать пару функций VirtualAlloc/VirtualFree - они уж точно вернут память системе), и (старых подход) либо обернуть ее в SafeBuffer и дальше в UnmanagedMemoryAccessor + UnmanagedMemoryStream, либо (новый подход) обернуть ее в MemoryManager<byte> из библиотеки System.Memory и далее работать с Memory<byte>

Answer 3

Поведение сборщика мусора недетерминировано и как следствие он запускается тогда, когда считает нужным.

Это сделано в целях оптимизации, так как сборка мусора- это ресурсоемкая операция:

  1. Надо найти объекты без ссылок
  2. Затем освободить память
  3. Выполнить сжатие(дефрагментацию) кучи

В вашем же случае явного вызова GC.Collect(); память все таки должна очищаться, если вы работаете с управляемыми ресурсами и нету никаких утечек.

Память может не очищаться из-за того, что программа запущена в режиме отладки и в этом режиме переменным проливается жизнь, что логично, ведь мусор может собираться еще до выхода из метода, если видно, что переменная не видна, но во время отладки мы можем наблюдать за переменными и как следствии было бы неудобно, если бы из Watch'ей переменные пропадали во время отладки.

Попробуйте запустить в режиме релиза со всевозможными оптимизациями. Подробнее об этом написано тут.

Так же, вроде, Рихетр писал, что сборщик мусора может не сразу отдавать память системе, так как может предполагать, что она понадобится в ближайшее время.

READ ALSO
Laravel Выбор таблицы при авторизации

Laravel Выбор таблицы при авторизации

При авторизации нужно обратится именно в ту таблицу которая была выбрана при входе

122
загрузка видео на хостинг

загрузка видео на хостинг

Почему картинки загружает нормального размера и видео по 8 байтов?

108