Visual Basic, .NET, ASP, VBScript
 

   
 
Описание для автора не найдено
 
     
   
 
Автор: Игорь Ткачев
The RSDN Group

Источник: RSDN Magazine #1
Опубликовано: 06.12.2002
Версия текста: 1.0
Сборщик мусора
Тест производительности
Освобождение ресурсов в CLR
Интерфейс IDisposable
Слабые ссылки
Класс System.GC

Аннотация

Алгоритм работы сборщика мусора (garbage collector, далее просто GC), являющегося частью CLR, подробно описан в книге Джефри Рихтера (Jeffrey Richter) «Applied Microsoft .NET Framework Programming». Мы не будем приводить здесь столь же подробное описание этого алгоритма, но обязательно остановимся на некоторых ключевых моментах.

Демонстрационное приложение

Проблема управления ресурсами вообще и памятью в частности является одной из наиболее животрепещущих при разработке программного обеспечения. Сколько раз после успешного завершения работы над очередным шедевром ваше хорошее настроение портилось из-за того, что служба тестирования выявляла утечку памяти (кто её только просил) Или вдруг заказчик начинал жаловаться на то, что последняя версия вашего редактора через час работы начинает рисовать не то, что ему хочется видеть, и вскоре вообще перестаёт подавать признаки жизни (подумаешь, всего-то забыл удалить кисточку и карандаш). Ещё хуже, когда приходится перелопачивать мегабайты исходного кода в поисках незакрытых соединений, т.к. с увеличением числа рабочих станций сервер базы данных начинает нещадно тормозить из-за нехватки ресурсов. В конце концов, выясняется, что с закрытыми соединениями программа местами вообще отказывается работать, и принимается «мудрое» решение переписать всё заново.

Безусловно, с опытом разработчики вырабатывают свои правила написания программ и приёмы борьбы с утечкой ресурсов. В таких языках, как C++ и Object Pascal, этому способствует наличие конструкторов и, главным образом, деструкторов объектов. Наиболее продвинутые программисты вообще не используют в своих программах выделение памяти или других ресурсов без использования специальных классов-обёрток. В частности, для работы с памятью используются так называемые «умные» указатели (smart pointers), которые управляют временем жизни объектов, созданных в динамической памяти.

В .NET необходимость в smart-указателях отпадает, так как в CLR используется автоматическое управление памятью. Для программиста это должно означать, что о памяти можно больше не заботиться. Я бы даже мог произнести такую крамольную фразу как

ПАМЯТЬ БОЛЬШЕ НЕ РЕСУРС!!!

поставить жирную точку и закончить написание этой статьи. Но, к сожалению, не всё так просто, хотя и довольно близко к истине. И, дабы избежать неожиданностей и сюрпризов в будущем, давайте вместе разберёмся, как работает сборщик мусора и зачем придумана конструкция using, выясним, почему не стоит злоупотреблять деструкторами в C# и MC++, и какая разница между Finalize и Dispose.

Сборщик мусора

Алгоритм работы сборщика мусора (garbage collector, далее просто GC), являющегося частью CLR, подробно описан в книге Джефри Рихтера (Jeffrey Richter) «Applied Microsoft .NET Framework Programming». Мы не будем приводить здесь столь же подробное описание этого алгоритма, но обязательно остановимся на некоторых ключевых моментах.

Для хранения объектов CLR использует хип, подобный хипу C++, за тем важным исключением, что хип CLR не фрагментирован. Выделение объектов производится всегда один за другим в последовательных адресах памяти, что позволяет весьма существенно повысить производительность всего приложения (рис. 1-1).


Рис.1. Начальное состояние хипа.

Когда память заканчивается, в дело вступает процесс, который мы опишем ниже. Фактически, основная задача этого процесса сводится к тому, чтобы освободить место в хипе путём дефрагментации неиспользуемой памяти.


Рис. 2. Хип после завершения работы с некоторыми объектами.

Первое, что делает GC во время сборки мусора – это принимает решение о том, что все выделенные блоки памяти в вашей программе - это как раз и есть мусор, и они вам больше не нужны ;o) К счастью, на этом GC не прекращает свою работу. Далее начинается утомительный поиск «живых» указателей на объекты по всем закоулкам приложения. Microsoft называет эти указатели «roots». В процессе поиска GC сканирует глобальную память программы (на рисунках обозначено как Global), стек (Stack – локальные переменные) и даже регистры процессора (CPU). Как мы знаем, каждый поток в программе имеет свой собственный стек, и CLR приходится сканировать их все. Кроме того, интересен тот факт, что CLR умеет работать даже с потоками, которые создавались в неуправляемом коде с использованием функций WinAPI. По крайней мере, следующий тест на MC++ отработал правильно, выдав на консоль результат – «212».

#define IServiceProvider IServiceProviderX
#include <windows.h>

__gc class CTest
{
  ~CTest() { System::Console::Write("2"); }
};

DWORD WINAPI Thread(LPVOID)
{
  CTest *ptr1 = new CTest();
  CTest *ptr2 = new CTest();
  ptr2 = 0; // деструктор ptr2 должен вызваться при следующей сборке мусора
  System::GC::Collect(); // сборка мусора
  System::Console::Write("1");
  // деструктор ptr1 вызовется при завершении работы программы
  return 0;
}

#pragma unmanaged
void fun()
{
  HANDLE h = ::CreateThread(NULL,0,Thread,0,0,NULL);
  ::WaitForSingleObject(h,INFINITE);
  ::CloseHandle(h);
}
#pragma managed

Если бы CLR не знал ничего о созданном без его участия потоке и не просканировал бы его стек, то указатель ptr1 был бы не найден и не расценен как «живой». В этом случае деструктор объекта, на который указывает ptr1, так же был бы вызван во время сборки мусора, то есть до вывода на консоль единицы.

Найдя «живой» указатель, сборщик мусора помечает объект, на который этот указатель указывает, как всё ещё используемый программой и запрашивает информацию о его структуре, чтобы в свою очередь просканировать уже этот объект на наличие указателей на другие объекты. И так далее, пока все указатели в программе не будут обработаны.

Теперь GC знает, какие объекты можно удалить, а какие пока не стоит. Сама по себе задача дефрагментации памяти является тривиальной, и все сложности начинаются позже, когда приходит время для корректировки всех указателей, которые существуют в программе, и присвоения им новых значений, получившихся после дефрагментации. Ну что же, GC только что просканировал всю память программы, почему бы не сделать это ещё раз...


Рис. 3. Хип после сборки мусора.

Тест производительности

Наверняка у вас возникли сомнения в эффективности подобного решения, по крайней мере, у меня они точно были, в связи с чем я решил провести небольшой эксперимент и проверить, как будет себя вести GC в сравнении с хипом C++ при выделении большого числа объектов.

Ниже приведён упрощенный текст тестовой программы на C#. Тест на C++ отличается только лишь наличием вызова деструкторов.

class TestObject2
{
  byte [] ar = new byte[20];
}

class TestObject1
{
  byte [] ar;
  TestObject2 obj2;

  public TestObject1(int n)
  {
    ar = new byte[n];
    obj2 = new TestObject2();
  }
}

static void DoTest(int maxObjSize,int maxListSize)
{
  TestObject1 [] coll = new TestObject1[maxListSize];
  int n=0;
  for (int i1=0; i1<10; i1++) 
  {
    for (int i2=0; i2<1000000; i2++) 
    {
      coll[n] = new TestObject1(maxObjSize);
      if (++n >= maxListSize)
      {
        n=0; // для C++ здесь вызываются деструкторы
           // созданных объектов
      }
    }
  }
  GC.Collect();
  GC.WaitForPendingFinalizers();
}

Здесь мы создаём 10 миллионов объектов, каждый из которых создаёт в свою очередь массив байт заданного (maxObjSize) размера, и ещё один объект, который также создаёт массив размером 20 байт. В общей сложности мы имеем 40 миллионов создаваемых объектов. Число одновременно живущих базовых (содержащих в своём составе ещё три) объектов задаётся значением переменной maxListSize, и в нашем тесте будет равняться 1,000, 10,000 и 100,000 элементов. Значением maxObjSize мы будем регулировать размер создаваемого в первом объекте массива.

Сразу отметим, что любой тест в достаточной степени субъективен. Наш – не исключение, его результаты ни в коей мере не претендуют на абсолютную точность и отражают лишь общую картину. В процессе подготовки теста было испробовано множество его вариаций, как то создание объектов различного псевдослучайного размера в заданном диапазоне, освобождение объектов в различном порядке для C++-тестов, принудительный вызов GC для C#, усложнение структуры и увеличение числа создаваемых объектов. Все эти изменения не оказали существенного влияния на результаты, поэтому сам тест был максимально упрощён.

Кроме того, практический интерес представляет только первая диаграмма, так как именно на ней протекает реальная жизнь 99% всех программ. Создание же такого количества одновременно живущих объектов и таких размеров, как на последующих диаграммах вряд ли имеет место в реальной жизни, поэтому эти результаты для нас будут представлять больше познавательный интерес.

Кроме CLR (GC) в тесте будут участвовать хип C++ (C++), хип Windows (Win) и QuickHeap (QH).

Далее приведены результаты тестов для различного количества одновременно живущих объектов. По горизонтали отмечен размер создаваемого в первом объекте массива (maxObjSize), по вертикали – время выполнения теста.


Рис. 4. Тест для 1,000 объектов.

На первой диаграмме хорошо видно, что автоматическое управление памятью в несколько раз опережает хип Windows и C++ при выделении небольших фрагментов. На отметке 700 байт это опережение заканчивается, и далее уверенно лидируют хипы.

ПРИМЕЧАНИЕ

Безусловный лидер QuickHeap в наших тестах учитываться не будет ввиду своей специфики, и приведён здесь лишь для сравнения.


Рис. 5. Тест для 10,000 объектов.

Увеличим число одновременно живущих объектов в 10 раз. В принципе, картина изменилась не сильно, за исключением того, что на отметке в 400 байт GC начал резко сдавать. И это вполне объяснимо.

Дело в том, что в некоторых случаях сборщик мусора может быть не вызван ни разу за всё время работы программы. В том числе и при её завершении. Посудите сами, зачем убирать мусор в доме, предназначенном на снос. Мы в нашем тесте подбирали число создаваемых объектов таким образом, чтобы сборщик мусора вызывался минимум 2-4 раза.

Аномалия на второй диаграмме обусловлена тем, что число вызовов GC резко возрастает почти до 400! на отметке 400 байт, и более чем до 900 раз при размере массива в 1000 байт.

Размеробъекта Время теста Вызовы GC
10 0:00:06 3
100 0:00:11 3
200 0:00:16 3
300 0:00:22 4
400 0:00:40 394
500 0:00:53 498
600 0:01:03 584
700 0:01:08 529
800 0:01:19 597
900 0:01:31 665
1000 0:01:57 932

Продолжим наш тест.


Рис. 6. Тест для 100,000 объектов.

При увеличении числа одновременно живущих объектов ещё на порядок ситуация стала ещё более печальной для GC. Теперь выигрыш заканчивается на 75 байтах, и далее GC начинает отставать, проигрывая в десятки раз, а при размере объектов в 5000 байт GC вообще впал в анабиоз часа на три, в то время как хипы уверенно справились с задачей за несколько минут. Хотя, конечно, этот случай уже совсем клинический. Посудите сами, программа за время своей работы выделяет более 50 гигабайт памяти мелкими кусочками, причём в один и тот же момент времени ей необходимо 400,000 объектов с суммарным размером более 500 мегабайт. Возможно, такие задачи и встречаются в жизни, но наверняка в них используется другой, менее универсальный, но более эффективный механизм работы с памятью. В общем, реально это или не реально, но мы теперь знаем, что нас ожидает в подобном случае.

В тоже время, это совсем не означает, что CLR не может работать с объектами большого размера. Очень даже может, вопрос лишь в том, как много таких объектов в вашей программе. Более того, работа с большими объектами в CLR специально оптимизирована. Для объектов размером более 20 килобайт используется отдельный хип, который отличается от обычного тем, что он никогда не дефрагментируется.

Вообще, алгоритмы работы GC построены во многом исходя из правил, полученных статистическим и опытным путём. В частности, одно из таких правил утверждает, что только что созданные «молодые» объекты имеют наиболее короткое время жизни, а живущие уже давно будут жить ещё долго. Именно в соответствии с этим правилом в CLR существуют понятие поколений (generations). Объекты, выжившие после первой сборки мусора и дефрагментации, объявляются первым поколением. В следующий раз, те объекты из первого поколения, которые опять смогли выжить, перемещаются во второе поколение и уже больше не трогаются, выжившие объекты из нулевого поколения перемещаются в первое. Другими словами, объекты, пережившие две сборки мусора, остаются в хипе навсегда и GC не занимается их дефрагментацией.

Теперь, зная этот факт, мы можем легко объяснить причину резкого падения производительности GC на второй и третьей диаграммах. Всё дело в том, что хип просто был перенаселён вторым поколением и для новых объектов оставалось совсем немного места, из-за чего сборщик мусора вызывался всё чаще и чаще.

Освобождение ресурсов в CLR

Давайте теперь разберёмся с тем, как мы подсчитывали количество вызовов GC, а заодно рассмотрим природу деструкторов и механизм освобождения ресурсов, принятый в CLR.

Для подсчёта вызовов GC мы воспользовались следующим классом, объект которого необходимо создать только один раз:

class GCCounter 
{
  public static int n = 0;
  ~GCCounter ()
  {
    n++;
    GC.ReRegisterForFinalize(this);
  }
}

void InitializeApp()
{
  new GCCounter();
  // doing something useful
}

Наверняка вы уже знаете, что как таковых деструкторов у объектов в CLR нет. Компиляторы C# и MC++ просто переименовывают их в метод Finalize, который вызывается сборщиком мусора перед тем, как объект будет уничтожен или при завершении программы, независимо от того, существуют ссылки на данный объект или нет. При этом вы не знаете когда, в каком порядке и в каком потоке будут вызваны методы Finalize. Кроме того, они могут быть вообще не вызваны, например, если по какой-либо причине не будет вызван завершающий код процесса.

Для работы с объектами, имеющими метод Finalize, CLR использует следующий механизм. При создании объекта, если он содержит метод Finalize, ссылка на него помещается в специальный список, называемый Finalization Queue (рис. 7).


Рис. 7. Управляемый хип и Finalization Queue.


Рис. 8. Хип после завершения работы с объектами A, D, E, H.

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


Рис. 9. Хип после первой сборки мусора.

Далее этим списком занимается специально созданный для этого поток, который по очереди вызывает методы Finalize для объектов из F-reachable Queue, а затем удаляет их и из этого списка.


Рис. 10. GC вызвал методы Finalize для объектов D и H и теперь они просто мусор.


Рис. 11. Хип после второй сборки мусора.

Зачем нужны такие сложности? Дело в том, что по идее деструктор вызывается после того, как объект уже никто не использует, но нам никто не мешает прямо в деструкторе сохранить указатель на наш объект в какой-нибудь глобальной переменной и использовать его в дальнейшем без особых угрызений совести. При описанной выше схеме с объектом ничего не случится, и им можно будет спокойно пользоваться сколь угодно долго. Единственное отличие заключается в том, что после того, как наш объект станет не нужен, метод Finalize для него вызван уже не будет, так как ссылка на наш объект уже отсутствует в Finalization Queue. Но, как вы уже догадались, и эта ситуация исправима. Метод ReRegisterForFinalize, который мы используем в нашем примере, как раз и позволяет вернуть наш объект обратно в Finalization Queue.

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

Всё это, конечно, не означает, что деструкторы – это зло, специально добавленное в CLR, чтобы усложнить нам жизнь. Ими можно и нужно пользоваться для освобождения unmanaged-ресурсов, таких, как файловые дескрипторы, соединения с БД и COM-объекты, но в тоже время нужно чётко понимать, что за этим стоит, и уж тем более не следует добавлять их к классам «просто так на всякий случай».

Интерфейс IDisposable

Лично мне вся эта кухня с Finalize представляется некой попыткой сделать хоть что-то для автоматического освобождения ресурсов в .NET, и, скажем прямо, эта попытка не выглядит слишком убедительной. Так как объекты не удаляются сразу же после того, как в них отпадает необходимость, то и собственно освобождение ресурсов происходит с задержкой, и, чаще всего, эта задержка является фатальной. Попробуйте вызвать вот такую процедуру дважды:

void Test()
{
  TextWriter tw = new StreamWriter("test.txt",true);
  tw.Write("123");
}

Думаю, исключение System.IO.IOException вам обеспечено по причине «The process cannot access the file "test.txt" because it is being used by another process.» (Процесс не может достучаться до файла, потому что до него уже достучался другой процесс). В общем-то, ничего удивительного, файлы за собой нужно закрывать, но в данном случае это ещё не всё. Даже написав код закрытия файла, нет никакой гарантии, что он точно будет выполнен. Что будет, если между открытием и закрытием файла произойдёт исключение? Даже если мы и обработаем это исключение позже, то наш файл опять не будет вовремя закрыт. Чтобы в данном случае обеспечить корректную работу, нам нужен примерно следующий код:

void Test()
{
  TextWriter tw = null;
  try 
  {
    tw = new StreamWriter("test.txt",true);
    tw.Write("123");
  }
  finally
  {
    if (tw != null)
      tw.Close();
  }
}

Вот теперь всё будет работать как надо. Для этого нам всего лишь понадобилось добавить ещё девять строчек кода к исходным двум. Не много ли для такой простой задачи? Тем более что на C++ такого же результата вполне можно добиться всё теми же двумя строчками, где и закрывать ничего не надо, и никакие исключения не страшны:

void Test()
{
  ofstream out("test.txt");
  out << "123";
}

Видимо, всё это прекрасно понимают и в самой Microsoft. Поэтому, в конце концов, для решения этой проблемы в C# было введено ключевое слово using, представляющее собой языковую конструкцию, заменяющую блок try/finally.

Похоже, using появилась в языке перед самым его выходом, по крайней мере, ничего подобного нет больше ни в одном другом языке, поддерживающем CLR. Да и в мегабайтах исходного кода, который предоставила Microsoft как CLI, эта конструкция встречается всего пару раз, в то время как её аналог в виде try/finally сплошь и рядом. К тому же в статьях Джефри Рихтера об алгоритмах работы GC, которые были опубликованы полтора года назад в MSDN Magazine и которые затем аккуратно в полном объёме перекочевали в его книгу, нет никакого упоминания о using. В самой же книге этому уже посвящён целый небольшой раздел. Вряд ли человек, который имеет неограниченный «доступ к телу», включая самые интимные места, не знал об этом ранее.

  • Всё это вселяет определённую надежду на то, что в будущем проблема освобождения ресурсов если и не будет решена так же изящно, как в C++, то, по крайней мере, работы в этом направлении будут вестись.
  • Перепишем наш пример с использованием using:
void Test()
{
  using (TextWriter tw = new StreamWriter("test.txt",true))
  {
    tw.Write("123");
  }
}
  • Это уже гораздо лучше. Конструкция using генерирует код, аналогичный нашему примеру с try/finally, за тем исключением, что вместо метода Close она вызывает Dispose. Вполне логично предположить, что объект должен иметь в своём составе такой метод. Так и есть, мы можем использовать с этой конструкцией только те объекты, которые реализуют интерфейс IDisposable, содержащий в своём составе один единственный метод:
void Dispose();
  • Интерфейс IDisposable призван формализовать освобождение ресурсов в CLR. Он реализуется многими классами .NET Framework, в том числе и для уведомления клиентов об уничтожении объекта (Component, Container, Control).
  • Соответственно, если вы создаёте класс, использующий какие-либо ресурсы, вы также должны реализовать этот интерфейс и метод Dispose в соответствии со следующими требованиями (эти правила никак не обозначены в MSDN и взяты мной из комментариев к исходным текстам CLI):
  1. Метод Dispose должен иметь возможность быть безопасно вызванным несколько раз.
  2. Освобождать любые ресурсы, ассоциированные с экземпляром объекта.
  3. Вызывать метод Dispose базового класса при необходимости.
  4. Подавлять вызов метода Finalize путём удаления ссылки на объект из Finalization Queue.
  5. Не должен генерировать исключения, кроме очень серьёзных случаев наподобие OutOfMemoryException. В идеале с вашим объектом не должно происходить ничего особенного в процессе вызова Dispose.
  6. Давайте напишем класс, который бы соответствовал данным требованиям, и который можно было бы использовать в дальнейшем как базовый.
using System;

namespace RSDN
{
  public abstract class DisposableType: IDisposable
  {
    bool disposed = false;

    ~DisposableType()
    {
      if (!disposed) 
      {
        disposed = true;
        Dispose(false);
      }
    }

    public void Dispose()
    {
      if (!disposed) 
      {
        disposed = true;
        Dispose(true);
        GC.SuppressFinalize(this);
      }
    }

    public void Close()
    {
      Dispose();
    }

    protected virtual void Dispose(bool disposing)
    {
      if (disposing) 
      {
        // managed objects
      }
      // unmanaged objects and resources
    }
  }
}

Теперь можно просто наследоваться от RSDN.DisposableType и всего лишь перекрывать метод Dispose(bool), в котором можно освобождать ресурсы:

class MyClass: DisposableType
{
  public TextWriter tw = new StreamWriter("test.txt");

  protected override void Dispose(bool disposing)
  {
    tw.Dispose();
    base.Dispose(disposing);
  }
}

Именно такой способ используется в иерархии классов .NET Framework, поэтому, если вы наследуетесь от существующих классов, то прежде чем перекрывать Dispose посмотрите, нет ли у этого класса чего-нибудь наподобие Dispose(bool).

Тем не менее, и этот способ недостаточно хорош для настоящих пуристов от C++. Так в процессе обсуждения этой темы на форумах RSDN родилась идея использовать какой-либо атрибут, с помощью которого можно было бы помечать нужные члены класса и свести процедуру очистки ресурсов к следующему виду:

class MyClass: DisposableType
{
  [Using]
  protected TextWriter tw = new StreamWriter("test.txt");
}

Это вполне осуществимо. Добавим в наш класс реализацию этой возможности:

using System;
using System.Reflection;
using System.Runtime.InteropServices;

namespace RSDN
{
  [AttributeUsage(AttributeTargets.Field)]
  public class UsingAttribute : Attribute 
  {
  }

  public abstract class DisposableType: IDisposable
  {
    bool disposed = false;

    ~DisposableType()
    {
      if (!disposed) 
      {
        disposed = true;
        Dispose(false);
      }
    }

    public void Dispose()
    {
      if (!disposed) 
      {
        disposed = true;
        Dispose(true);
        GC.SuppressFinalize(this);
      }
    }

    protected virtual void Dispose(bool disposing)
    {
      DisposeFields(this);
    }

    static public void DisposeFields(object obj)
    {
      foreach (
        FieldInfo field in 
        obj.GetType().GetFields(BindingFlags.Public | 
                    BindingFlags.NonPublic |
                    BindingFlags.Instance))
      {
        if (Attribute.IsDefined(field,typeof(UsingAttribute))) 
        {
          object val = field.GetValue(obj);
          if (val != null)
          {
            if (val is IDisposable)
            {
              ((IDisposable)val).Dispose();
            }
            else if (val.GetType().IsCOMObject)
            {
              // на всякий случай добавим сюда 
              // и освобождение COM-объектов
              Marshal.ReleaseComObject(val);
            }
          }
        }
      }
    }
  }
}

К сожалению, этот метод будет прекрасно работать только для классов, которым не нужно наследование. Если же вы наследуетесь, например, от стандартных классов .NET Framework, то вы должны будете самостоятельно позаботиться о вызове метода DisposeFields. Для этого он объявлен как static и public, и может быть вызван из любых других классов.

Было бы совсем здорово, если бы этот способ был встроен в сам компилятор, который мог бы при необходимости автоматически добавлять наследование от IDisposable и генерировать метод Dispose. В этом случае можно было бы использовать всё тот же атрибут или ключевое слово using:

class MyClass
{
  using protected TextWriter tw = new StreamWriter("test.txt");
}

Слабые ссылки

Но вернёмся от мечтаний к реалиям и рассмотрим ещё одну возможность, предоставляемую подсистемой управления памятью CLR. Это, так называемые, «слабые» ссылки (Weak References). «Слабые» ссылки представлены в .NET Framework классом WeakReference. Основным их назначением является кэширование больших объёмов данных, получение которых критично по времени. Например, ваш запрос к базе данных занимает несколько секунд, что, безусловно, может раздражать пользователя. Вы могли бы сохранить эти данные в памяти, но они занимают довольно большой объём, который мог бы пригодиться и для других целей. Вот здесь как нельзя лучше и подойдут «слабые» ссылки. Сборщик мусора удалит эти данные вместе с мусором, когда будет вызван, то есть в момент, когда программе понадобится дополнительная оперативная память. В то же время, до тех пор, пока этого не случится, вы сможете ими спокойно пользоваться, не обращаясь к базе данных лишний раз.

Следующий пример демонстрирует возможный вариант применения «слабых» ссылок:

class Class1
{
    class TestClass
    {
        public override string ToString()
        {
            return "Test Object No " + objectNo;
        }
        public TestClass()
        {
            objectNo++;
        }
        static int objectNo = 0;
    }

    // «слабая» ссылка
    WeakReference wr = new WeakReference(null);

    // Возвращает ссылку на объект,
    // при необходимости создаёт новый
    TestClass GetRef()
    {
        // Если объект уже создан и всё ещё жив,
        // то мы можем получить ссылку на него через Target
        TestClass tc = (TestClass)wr.Target;
        if (tc == null)
        {
            tc = new TestClass();
            wr.Target = tc;
        } 
        return tc;
    }

    public void Test()
    {
        // Получаем ссылку на объект
        object obj = GetRef();

        // Вызываем сборщик мусора,
        // но объект не будет удалён, 
        // т.к. существует «сильная» ссылка obj
        GC.Collect();

        // Печатаем "Test Object No 1"
        Console.WriteLine(GetRef());

        // удаляем «сильную» ссылку
        obj = null;

        // Печатаем опять "Test Object No 1",
        // т.к. сборщик мусора не вызывался и мы можем
        // получить объект через «слабую» ссылку
        Console.WriteLine(GetRef());

        // Вызываем сборщик мусора ещё раз
        GC.Collect();

        // На этот раз печатаем "Test Object No 2",
        // сборщик мусора удалил старый объект,
        // и вызоа GetRef создаст новый
        Console.WriteLine(GetRef());
    }
}

Класс System.GC

В заключение разберём класс System.GC, который предоставляет интерфейс к подсистеме сборки мусора. Мы не можем создавать экземпляры этого класса и не можем от него наследоваться, да это и не нужно, так как все методы и свойства этого класса статические и доступны для использования без создания объекта. Рассмотрим их все по порядку.

MaxGeneration

Это свойство возвращает максимальный номер поколения, который поддерживается системой. В настоящий момент это значение равно 2.

int MaxGeneration {get;}

Метод Collect

Принудительный вызов сборщика мусора.

void Collect();
void Collect(int generation);

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

GC.Collect(GC.MaxGeneration);

GetGeneration

Возвращает номер поколения, в котором находится заданный объект.

int GetGeneration(object obj);
int GetGeneration(WeakReference wo);

GetTotalMemory

Возвращает число байт, занимаемых в данный момент управляемым хипом.

long GetTotalMemory(bool forceFullCollection);

Если параметр forceFullCollection равен true, то функция возвращает занимаемую хипом память после вызова сборщика мусора. Как мы уже знаем, GC не гарантирует освобождение абсолютно всей возможной памяти. Данная функция вызывает сборщик мусора несколько (до 20) раз до тех пор пока разница в занимаемой памяти до и после сборки не будет составлять 5%.

KeepAlive

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

void KeepAlive(object obj);

Подобная возможность может понадобитсься, например, в случае если ссылки на ваш объект уже отсутствуют в управляемом коде, но ваш объект всё ещё необходим unmanaged коду.

ReRegisterForFinalize

Добавляет объект в Finalization Queue.

void ReRegisterForFinalize(object obj);

Мы уже пользовались этим методом для подсчёта числа вызовов GC.

SuppressFinalize

Удаляет объект из Finalization Queue.

void SuppressFinalize(object obj);

WaitForPendingFinalizers

Останавливает текущий поток до завершения выполнения методов Finalize для всех объектов из F-reachable Queue (рис. 9).

void WaitForPendingFinalizers();

Этот метод обычно используется совместно с GC.Collect. Например, следующий код удалит все объекты, которые больше не используются программой:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();


Эта статья опубликована в журнале RSDN Magazine #1.
 
     

   
   
     
  VBNet рекомендует