The GC keeps track of which objects are being referenced.
The GC always has a low-priority thread scanning its objects to determine when an object is no longer referenced.
A second low-priority thread is then responsible for cleanup. This thread calls the object's Finalize method.
This approach does solve the circular reference problem, but it causes other problems. For example, there's no guarantee that the Finalize method will ever be called by the runtime! This is what is meant by deterministic in deterministic finalization. So the question becomes, "How can I do necessary cleanup and know that my cleanup code is called every time?" -
In the case of releasing scarce resources, Microsoft proposes a solution based on the Dispose design pattern. This design pattern recommends that the object expose a public method, called something generic like Cleanup or Dispose,that the user is then instructed to call when finished using the object. It's then up to the class's designer to do any necessary cleanup in that method.
In fact, you'll see that many of the classes in the .NET Framework class library implement a Dispose method for this purpose. As an example, the documentation for the System.Winforms.TrayIcon class says, "Call Dispose when you are finished using the TrayIcon." -