博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[No0000147]深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing)理解堆与栈4/4
阅读量:5260 次
发布时间:2019-06-14

本文共 14811 字,大约阅读时间需要 49 分钟。

前言

 
虽然在.Net Framework 中我们不必考虑内在管理和垃圾回收(GC),但是为了优化应用程序性能我们始终需要了解内存管理和垃圾回收(GC)。另外,了解内存管理可以帮助我们理解在每一个程序中定义的每一个变量是怎样工作的。
 

简介

这一节我们将介绍垃圾回收机制GC以及一些提搞程序性能的技巧。
 
 

绘图Graphing

 
让我们站在GC的角度研究一下。如果我们负责“扔垃圾”,我们需要制定一个有效的“扔垃圾”计划。显然,我们需要判断哪些是垃圾,哪些不是。
 
为了决定哪些需要保留,我们假设任何没有正在被使用的东西都是垃圾(如角落里堆积的破旧纸张,阁楼里一箱箱没有用的过时产品,柜子里不用的衣服)。想像一下我们跟两个好朋友生活在一起:JIT 和CLR。JIT和CLR不断的跟踪他们正在使用的东西,并给我们一个他们需要保留的东西列表。这个初始列表我们叫它“根(root)”列表。因为我们用它做起点。我们将保持一个主列表去绘制一张图,图中分布着所有我们在房子中需要保留东西。任何与主列表中有关联的东西也被画入图中。如,我们保留电视就不要扔掉电视遥控器,所以电视遥控器也会被画入图中。我们保留电脑就不能扔掉显示器键盘鼠标,同样也把它们画入图中。
 
这就是GC怎么决定去保留对象的。GC会保留从JIT和CLR那收到的一个根(root)对象引用列表,然后递归搜索对象引用并决定什么需要保留。
 
这个根的构成如下:
  • 全局/静态 指针。通过以静态变量的方式保持对象的引用,来确保对象不会被GC回收。
  • 栈里的指针。为了程序的执行,我们不想扔掉那些程序线程始终需要的对象。
  • CPU寄存器指针。托管堆里任何被CPU内存地址指向的对象都需要被保留。
 
在上面的图中,托管堆中的对象1,5被根Roots引用,3被1引用。对象1,5是被直接引用,3是通过递归查询找到。如果关联到我们之前的假设,对象1是我们的电视,对象3则是电视遥控器。当所有对象画完后,我们开始进行下一阶段:垃圾清理。
 

GC垃圾清理Compacting

现在我们有了一张需要保留对象的关系图,接下来进行GC的清理。
 
图中对象2和4被认定为垃圾将被清理。清理对象2,复制(memcpy )对象3到2的位置。
由于对象3的地址变了,GC需要修复指针(红色箭头)。然后清理对象4,复制(memcpy )对象5到原来3的位置(译外话:GC原则:堆中对象之间是没有间隙的,以后会有文章专门介绍GC原理)。
 
 
最后清理完毕,新对象将被放到对象5的上面(译外话:GC对一直管理一个指针指向新对象将被放置的地址,如黄色箭头,以后会有文章专门介绍)。
 
了解GC原理可以帮助我们理解GC清理(复制memcpy ,指针修复等)是怎么消耗掉很多资源的。很明显,减少托管堆里对象的移动(复制memcpy )可以提高GC清理的效率。
 

托管堆之外的终止化队列Finalization Queue和终止化-可达队列Freachable Queue

有些情况下,GC需要执行特定代码去清理非托管资源,如文件操作,数据库连接,网络连接等。一种可行性方案是使用析构函数(终结器):
[csharp]   
 
  1. class Sample  
  2. {  
  3.           ~Sample()  
  4.           {  
  5.                     // FINALIZER: CLEAN UP HERE 终结器:在这里清理  
  6.           }  
  7. }  
译外话:析构函数会被内部转换成终结器override Finializer()
 
有终结器的对象在创建时,同时在Finalization Queue里创建指向它们的指针(更正原文说的把对象放到Finalization Queue里):
上图对象1,4,5实现了终结器,因此在Finalization Queue里创建指向它们的指针。让我们看一下,当对象2和4没有被程序引用要被GC清理时会发生什么情况。
对象2会被以常规模式清理掉(见文章开始部分)。GC发现对象4有终结器,则会把Finalization Queue里指向它的指针移到Freachable Queue中,如下图:
但是对象4并不被清理掉。有一个专门处理Freachable Queue的线程,当它处理完对象4在Freachable Queue里的指针后,会把它移除。
这时对象4可以被清理了。当下次GC清理时会把它移除掉。换句话说,至少执行两次GC清理才能把对象4清理掉,显然会影响程序性能。
 
创建终结器,意味着创建了更多的工作给GC,也就会消耗更多资源影响程序性能。因此,当你使用终结器时一定要确保你确实需要使用它。
更好的方法是使用IDisposable接口。
[csharp]   
 
  1. public class ResourceUser : IDisposable  
  2. {  
  3.           #region IDisposable Members  
  4.    
  5.           public void Dispose()  
  6.           {  
  7.                     // 在这里清理!!!  
  8.           }  
  9.   
  10.           #endregion  
  11. }  
实现IDisposable接口的对象可以使用using关键字:
[csharp]   
 
  1. using (ResourceUser rec = new ResourceUser())  
  2. {  
  3.                 // 具体实现。。。  
  4.    
  5. // 这里调用DISPOSE方法  
变量rec的作用域是大括号内,大括号外不可访问。

静态变量

[csharp]   
 
  1. class Olympics  
  2. {  
  3.           public static Collection<Runner> TryoutRunners;  
  4. }  
  5.    
  6. class Runner  
  7. {  
  8.           private string _fileName;  
  9.           private FileStream _fStream;  
  10.    
  11.           public void GetStats()  
  12.           {  
  13.                     FileInfo fInfo = new FileInfo(_fileName);  
  14.                     _fStream = _fileName.OpenRead();  
  15.           }  
  16. }  
如果你初始化了TryoutRunners,那么它将永远不会被GC清理,因为有静态指针一直指向初始化的对象。一旦调用了Runner里GetStats()方法,因为GetStats()里面没有文件关闭操作,它将永远被打开也不会被GC清理。我们可以看到程序的崩溃即将来临。
 

总结

一些良好的操作可以提高程序的性能:
  1. 清理。不要打开资源而不关闭它。关闭所有你打开的连接。尽可能快的清理所有非托管资源。一般规则:使用非托管对象,初始化越晚越好,清理越早越好。
  2. 不要过度引用。合理使用引用对象。如果某一个对象还存在没有被GC清理,所有它引用的对象都将不会被GC清理,如此递归下去。。。当我们完成使用一个引用对象时,把它设为NULL(视你的情况而定,注意不要产生空引用异常)。当引用少了,GC开始创建清理关系图graphing时过程就简单一些了,进而提高程序性能。
  3. 谨慎使用终结器Finalizaer或析构函数。能使用IDisposible代替就使用IDisposible。
  4. 保持对象及其成员的紧凑。如果声明一个对象并且它由多个子对象组成,尽可能的把它们放在一起初始化,好让它们所在的内存空间紧凑。GC复制这样的一大块内存比复制分散的内存碎片要容易。
译外话:
我会在以后的文章里更详细的介绍GC垃圾回收机制,包括GC划分的0代generation 0,1代generation 1,2代generation 2。有时只有一篇文章或一种图解还是会让人迷惑,所以下一篇介绍GC垃圾回收的内容更详细,图解也有不同。

Even though with the .NET framework we don't have to actively worry about memory management and garbage collection (GC), we still have to keep memory management and GC in mind in order to optimize the performance of our applications. Also, having a basic understanding of how memory management works will help explain the behavior of the variables we work with in every program we write.  In this article we'll look into Garbage Collection (GC) and some ways to keep our applications running efficiently.

Graphing

Let's look at this from the GC's point of view. If we are responsible for "taking out the trash" we need a plan to do this effectively. Obviously, we need to determine what is garbage and what is not (this might be a bit painful for the pack-rats out there). 

In order to determine what needs to be kept, we'll first make the assumption that everything not being used is trash (those piles of old papers in the corner, the box of junk in the attic, everything in the closets, etc.)  Imagine we live with our two good friends: Joseph Ivan Thomas (JIT) and Cindy Lorraine Richmond (CLR). Joe and Cindy keep track of what they are using and give us a list of things they need to keep. We'll call the initial list our "root" list because we are using it as a starting point.  We'll be keeping a master list to graph where everything is in the house that we want to keep. Anything that is needed to make things on our list work will be added to the graph (if we're keeping the TV we don't throw out the remote control for the TV, so it will be added to the list. If we're keeping the computer the keyboard and monitor will be added to the "keep" list).

This is how the GC determines what to keep as well. It receives a list of "root" object references to keep from just-in-time (JIT) compiler and common language runtime (CLR) (Remember Joe and Claire?) and then recursively searches object references to build a graph of what should be kept. 

Roots consist of:

  • Global/Static pointers. One way to make sure our objects are not garbage collected by keeping a reference to them in a static variable.
  • Pointers on the stack. We don't want to throw away what our application's threads still need in order to execute.
  • CPU register pointers. Anything in the managed heap that is pointed to by a memory address in the CPU should be preserved (don't throw it out).

 

In the above diagram, objects 1, 3, and 5 in our managed heap are referenced from a root 1 and 5 are directly referenced and 3 is found during the recursive search.  If we go back to our analogy and object 1 is our television, object 3 could be our remote control. After all objects are graphed we are ready to move on to the next step, compacting.

Compacting

Now that we have graphed what objects we will keep, we can just move the "keeper objects" around to pack things up.

 

Fortunately, in our house we don't need to clean out the space before we put something else there. Since Object 2 is not needed, as the GC we'll move Object 3 down and fix the pointer in Object 1.

 

Next, as the GC, we'll copy Object 5 down

 

Now that everything is cleaned up we just need to write a sticky note and put it on the top of our compacted heap to let Claire know where to put new objects.

 

Knowing the nitty-gritty of CG helps in understanding that moving objects around can be very taxing. As you can see, it makes sense that if we can reduce the size of what we have to move, we'll improve the whole GC process because there will be less to copy.

What about things outside the managed heap?

As the person responsible for garbage collection, one problem we run into in cleaning house is how to handle objects in the car. When cleaning, we need to clean everything up. What if the laptop is in the house and the batteries are in the car?

There are situations where the GC needs to execute code to clean up non-managed resources such as files, database connections, network connections, etc. One possible way to handle this is through a finalizer.

class Sample

{

          ~Sample()

          {

                    // FINALIZER: CLEAN UP HERE

          }

}

During object creation, all objects with a finalizer are added to a finalization queue. Let's say objects 1, 4, and 5 have finalizers and are on the finalization queue.  Let's look at what happens when objects 2 and 4 are no longer referenced by the application and ready for garbage collection.

 
 

Object 2 is treated in the usual fashion. However, when we get to object 4, the GC sees that it is on the finalization queue and instead of reclaiming the memory object 4 owns, object 4 is moved and it's finalizer is added to a special queue named freachable. 

 
 

There is a dedicated thread for executing freachable queue items. Once the finalizer is executed by this thread on Object 4, it is removed from the freachable queue. Then and only then is Objet 4 ready for collection.

 

So Object 4 lives on until the next round of GC.

Because adding a finalizer to our classes creates additional work for GC it can be very expensive and adversely affect the performance garbage collection and thus our program. Only use finalizers when you are absolutely sure you need them.

A better practice is to be sure to clean up non-managed resources. As you can imagine, it is preferable to explicitly close connections and use the IDisposable interface for cleaning up instead of a finalizer where possible.

IDisposaible

Classes that implement IDisposable perform clean-up in the Dispose() method (which is the only signature of the interface). So if we have a ResouceUser class instead of using a finalizer as follows:

public class ResourceUser

{

          ~ResourceUser() // THIS IS A FINALIZER

          {

                    // DO CLEANUP HERE

          }

}

We can use IDisposable as a better way to implement the same functionality:

public class ResourceUser : IDisposable

{

          #region IDisposable Members

 

          public void Dispose()

          {

                    // CLEAN UP HERE!!!

          }

 

          #endregion

}

 

IDisposable in integrated with the using keyword. At the end of the using block Dispose() is called on the object declared in using(). The object should not be referenced after the using block because it should be essentially considered "gone" and ready to be cleaned up by the GC.

public static void DoSomething()

{

ResourceUser rec = new ResourceUser();

 

using (rec)

{

                // DO SOMETHING

 

} // DISPOSE CALLED HERE

 

            // DON'T ACCESS rec HERE

 

}

 

I like putting the declaration for the object in the using block because it makes more sense visabally and rec is no longer available outside of the scope of the using block. Whis this pattern is more in line with the intention of the IDisposible interface, it is not required.

public static void DoSomething()

{

using (ResourceUser rec = new ResourceUser())

{

                // DO SOMETHING

 

} // DISPOSE CALLED HERE

}

 

By using using() with classes that implement IDisposible we can perform our cleanup without putting additional overhead on the GC by forcing it to finalize our objects.

Static Variables: Watch Out!

class Counter

{

          private static int s_Number = 0;

 

          public static int GetNextNumber()

          {

                    int newNumber = s_Number;

 

                    // DO SOME STUFF

           

                    s_Number = newNumber + 1;

 

                    return newNumber;

          }

}

 

If two threads call GetNextNumber() at the same time and both are assigned the same value for newNumber before s_Num}

If two threads call GetNextNumber() at the same time and both are assigned the same value for newNumber before s_Number is incremented they will return the same result!word is one way to ensure only one thread can access a block of code at a time. As a best practice, you should lock as little code as possible because threads have to wait in a queue to execute the code in the lock()  block and it can be inefficient.

class Counter

{

          private static int s_Number = 0;

 

          public static int GetNextNumber()

          {

                    lock (typeof(Counter))

                    {

                             int newNumber = s_Number;

 

                             // DO SOME STUFF

 

                             newNumber += 1;

                             s_Number = newNumber;

 

                             return newNumber;

                    }

          }

}

Static Variables: Watch Out... Number 2!

The next thing we have to watch out for objects referenced by static variables.  Remember, how anything that is referenced by a "root" is not cleaned up. Here's one of the ugliest examples I can come up with:

class Olympics

{

          public static Collection<Runner> TryoutRunners;

}

 

class Runner

{

          private string _fileName;

          private FileStream _fStream;

 

          public void GetStats()

          {

                    FileInfo fInfo = new FileInfo(_fileName);

                    _fStream = _fileName.OpenRead();

          }

}

Because the Runner Collection is static for the Olympics class, not only will objects in the collection will not be released for garbage collection (they are all indirectly referenced through a root), but as you probably noticed, every time we  run GetStats() the stream is opened to the file. Because it is not closed and never released by GC this code is effectively a disaster waiting to happen. Imagine we have 100,000 runners trying out for the Olympics.  We would end up with that many non-collectable objects each with an open resource.  Ouch! Talk about poor performance!

Singleton

One trick to keep things light is to keep only one instance of a class in memory at all times. To do this we can use the GOF Singleton Pattern.

One trick to keep things light is to keep only one instance of a utility class in memory at all times. One easy way to  do this we can use the GOF Singleton Pattern. Singletons should be used with caution because they are really "global variables" and cause us much headached and "strange" behavior in multi-threaded applications where different threads could be altering the state of the object.  If we are using the singleton pattern (or any global variable) we should be able to justify it (in other words... don't do it without a good reason).

public">{

          private static Earth _instance = new Earth();

 

          private Earth() { }

 

          public static Earth GetInstance() { return _instance; }

}

We have a private constructor so only Earth can execute it's constructor and make an Earth. We have a static instance of Earth and a static method to get the instance. This particular implementation is thread safe because the CLR ensures thread safe creation of static variables. This is the most elegant way I have found to implement the singleton pattern in C#.

In Conclusion...

So to wrap up, some things we can do to improve GC performance are:

  1. Clean up. Don't leave resources open!  Be sure to close all connections that are opened and clean up all non-managed objects as soon as possible. As a general rule when using non-managed objects, instantiate as late as possible and clean up as soon as possible.
  2. Don't overdo references.  Be reasonable when using references objects.  Remember, if our object is alive, all of it's referenced objects will not be collected (and so on, and so on). When we are done with something referenced by class, we can remove it by either setting the reference to null.  One trick I like to do is setting unused references to a custom light weight NullObject to avoid getting null reference exceptions. The fewer references laying about when the GC kicks off, the less pressure the mapping process will be. 
  3. Easy does it with finalizers. Finalizers are expensive during GC we should ONLY use them if we can justify it. If we can use IDisposible instead of a finalizer, it will be more efficient because our object can be cleaned up in one GC pass instead of two.
  4. Keep objects and their children together. It is easier on the GC to copy large chunks of memory together instead of having to essentially de-fragment the heap at each pass, so when we declare a object composed of many other objects, we should instantiate them as closely together as possible.

Next time we'll look even more closely at the GC process and look into ways to check under the hood as your program executes to discover problems that may need to be cleaned up.

Until then,

-Happy coding

转载于:https://www.cnblogs.com/Chary/p/No0000147.html

你可能感兴趣的文章
Visual Studio 2005 Web Application Projects正式发布!
查看>>
servlet知识点总结
查看>>
U盘安装Win7
查看>>
Android 中的监听器模式与观察者模式
查看>>
pageadmin CMS自助建站系统教程:模板中执行sql语句
查看>>
Spinner的级联效果实现
查看>>
115-PHP实例化和不实例化输出类常量
查看>>
P3810 陌上花开 CDQ分治
查看>>
【python】-- socketserver
查看>>
JDBC事务和JTA事务的区别
查看>>
创建web项目后生成的配置文件解析
查看>>
pcl之kdtree的使用
查看>>
android 两种定时器的实现
查看>>
第三次作业
查看>>
java设计模式之代理模式
查看>>
rocketMq指定broker ip地址,适合解决云主机部署问题
查看>>
基于NodeJS的全栈式开发
查看>>
windows程序设计(三)
查看>>
CCF认证真题-(201403-2)-窗口(模拟)
查看>>
片上系统(SOC)设计流程及其集成开发环境[zz]
查看>>