值类型:值类型的实例一般在线程的栈上分配
引用类型:引用类型的实例在线程的托管堆上分配
引用类型变量的Equals比较的是二者的引用地址而不是内部的值,值类型变量的Equals方法比较的是二者的值。
堆栈与托管堆
线程堆栈(Thread Stack):先进后出 (Last-In/First-Out)
堆栈中存储值类型。
堆栈实际上是向下填充,即由高内存地址指向地内存地址填充。
堆栈的工作方式是先分配内存的变量后释放(先进后出原则)。
堆栈中的变量是从下向上释放,这样就保证了堆栈中先进后出的规则不与变量的生命周期起冲突!
堆栈的性能非常高,但是对于所有的变量来说还不太灵活,而且变量的生命周期必须嵌套。
通常我们希望使用一种方法分配内存来存储数据,并且方法退出后很长一段时间内数据仍然可以使用。此时就要用到堆(托管堆)!
托管堆(Managed Heap):托管堆分配在被操作系统保留的一段内存区域中,这段内存区域是由CLR来管理的,这段内存称之为托管堆。
堆(托管堆)存储引用类型。
此堆非彼堆,.NET中的堆由垃圾收集器自动管理。
与堆栈不同,堆是从下往上分配,所以自由的空间都在已用空间的上面。
每个正在运行的程序都对应着一个进程(process),在一个进程内部,可以有一个或多个线程(thread),每个线程都拥有一块“自留地”,称为“线程堆栈”,大小为1M,用于保存自身的一些数据,比如函数中定义的局部变量、函数调用时传送的参数值等,这部分内存区域的分配与回收不需要程序员干涉。所有值类型的变量都是在线程堆栈中分配的。读取速度快。
另一块内存区域称为“堆(heap)”,在.NET 这种托管环境下,堆由CLR 进行管理,所以又称为“托管堆(managed heap)”。CLR的垃圾回收机制负责定期清理。读取速度慢。
堆栈和堆(托管堆)都在进程的虚拟内存中。(在32位处理器上每个进程的虚拟内存为4GB,64位Windows的虚拟内存为16 TB)
虚拟内存(VirtualMemory)是Windows管理所有可用内存的方式。对于32位Windows系统,每个进程所用到的虚拟内存地址从0到2^32-1,总容量4GB,其中2GB是与操作系统以及其他所有进程所共享,另外2GB分派给进程独占(这就是常说的32位Windows中一个进程最多能用2G内存的由来)。
进程:是操作系统结构的基础;是一个正在执行的程序;
线程:有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。
线程与进程的区别可以归纳为以下4点:
1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
4)在多线程OS中,进程不是一个可执行的实体。
线程安全: 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
类型安全:类型安全应该算是CLR最重要的特性之一了,在运行时,CLR总是知道一个对象的类型。在C#中可以调用GetType()来返回调用对象的类型,并且由于GetType()继承于System.Object对象,并且为非虚的方法,所以一个类型不可能通过重写此方法而伪装成另一种类型。C++不是类型安全的,因为两个不同类型的指针之间可以强制转换。
is的规则如下:检查对象类型的兼容性,并返回结果,true或者false;不会抛出异常;如果对象为null,则返回值永远为false。
as的规则如下:检查对象类型的兼容性,并返回结果,如果不兼容就返回null;不会抛出异常; 如果结果判断为空,则强制执行类型转换将抛出NullReferenceException异常。
null:在.NET中,null表示一个对象引用是无效的。
Nullable<T>(可空类型):
int? i = null;
Nullable<int> i = null;
Nullable本质上仍是一个struct为值类型,其实例对象仍然分配在线程栈上。其中的value属性封装了具体的值类型,Nullable<T>进行初始化时,将值类型赋给value
int? a = 100;
Int32 b = (Int32)a;
a = null;
泛型:泛型(generic)是CLR和编程语言提供的一种特殊机制,它支持一种形式的代码重用,即“算法重用”。
1.性能高:定义数据类型,不需要类型转换,避免拆装箱带来的性能损失;
2.类型安全:定义允许使用的数据类型,在编译时检查类型错误,及早发现错误。
概述:
对象哈希码:
Equals的作用是比较对象实例是否相等。MSDN中提到,Object默认支持引用相等,如果想要判断值相等,就要重写Equals。判断两个实例是否是一个引用时,通常是通过object.ReferenceEquals(obj1, obj2);
GetHashCode就是获取这个对象的实例唯一标示,当你重写Equals时,如果不重写GetHashCode,生成时就会有警告。
1. 不建议使用值类型对象的GetHashCode函数返回值来作为HashTable对象的Key;
2. 引用类型是可以使用的,但是要注意如果重写了Equals函数,一定要重写GetHashCode函数来达到一致;
再说说重写此函数时需要注意的。
1. 不管是值类型还是引用类型,要保证产生HashCode的成员不能被修改;
2. 对于产生HashCode的成员修改,要以产生新对象进行处理,同时要在使用端作相应的修改,即先删除旧的在添加新的。
使用类型的GetHashCode有微妙的危险,所以使用的时候要特别注意。
dynamic基元类型:
dynamic是FrameWork4.0的新特性。dynamic的出现让C#具有了弱语言类型的特性。编译器在编译的时候不再对类型进行检查,编译期默认dynamic对象支持你想要的任何特性。dynamic可以简化反射。
大对象:在.Net 1.0和2.0中,如果一个对象的大小超过85000byte,就认为这是一个大对象。这个数字是根据性能优化的经验得到的。当一个对象申请内存大小达到这个阀值,它就会被分配到大对象堆上。大对象属于2代对象,因为只有GC在2代回收时才会处理大对象。
扩展方法(Extension Method):扩展方法是C# 3.0中新加入的特性。MSDN中对扩展方法的定义是:扩展方法使您能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。
using:在程序编译阶段,编译器会自动将using语句生成为try-finally语句,并在finally块中调用对象的Dispose方法,来清理资源。所以,using语句等效于try-finally语句
使用规则
①using只能用于实现了IDisposable接口的类型,禁止为不支持IDisposable接口的类型使用using语句,否则会出现编译错误;
②using语句适用于清理单个非托管资源的情况,而多个非托管对象的清理最好以try-finnaly来实现,因为嵌套的using语句可能存在隐藏的Bug。内层using块引发异常时,将不能释放外层using块的对象资源;
③using语句支持初始化多个变量,但前提是这些变量的类型必须相同,例如:
using(Pen p1 = new Pen(Brushes.Black), p2 = new Pen(Brushes.Blue))
{
//
}
④针对初始化对个不同类型的变量时,可以都声明为IDisposable类型,例如:
using (IDisposable font = new Font("Verdana", 12), pen = new Pen(Brushes.Black))
{
float size = (font as Font).Size;
Brush brush = (pen as Pen).Brush;
}
Dictionary和 Hashtable的区别:
1:单线程程序中推荐使用 Dictionary, 有泛型优势, 且读取速度较快, 容量利用更充分。
2:多线程程序中推荐使用 Hashtable, 默认的 Hashtable 允许单线程写入, 多线程读取, 对 Hashtable 进一步调用 Synchronized() 方法可以获得完全线程安全的类型。 而Dictionary 非线程安全, 必须人为使用 lock 语句进行保护, 效率大减。
3:Dictionary 有按插入顺序排列数据的特性 (注: 但当调用 Remove() 删除过节点后顺序被打乱), 因此在需要体现顺序的情境中使用 Dictionary 能获得一定方便.
对于值类型,特定类型(不包括 Object)的 Dictionary<(Of <(TKey, TValue>)>) 的性能优于 Hashtable,这是因为 Hashtable 的元素属于 Object 类型,所以在存储或检索值类型时通常发生装箱和取消装箱操作。
垃圾回收(GC):GC如其名,就是垃圾收集。Garbage Collector(垃圾收集器)以应用程序的root为基础,遍历应用程序在Heap上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡的、哪些仍需要被使用。已经不再被应用程序的root或者别的对象所引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收。这就是GC工作的原理。
托管推分为3个代龄区域,相应的GC有3种方式: # Gen 0 collections, # Gen 1 collections, #Gen 2 collections。如果Gen 0 heap内存达到阀值,则触发0代GC,0代GC后Gen 0中幸存的对象进入Gen1。如果Gen 1的内存达到阀值,则进行1代GC,1代GC将Gen 0 heap和Gen 1 heap一起进行回收,幸存的对象进入Gen2。2代GC将Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收,Gen 0和Gen 1比较小,这两个代龄加起来总是保持在16M左右;Gen2的大小由应用程序确定,可能达到几G,因此0代和1代GC的成本非常低,2代GC称为full GC,通常成本很高。粗略的计算0代和1代GC应当能在几毫秒到几十毫秒之间完成,Gen 2 heap比较大时,full GC可能需要花费几秒时间。大致上来讲.NET应用运行期间,2代、1代和0代GC的频率应当大致为1:10:100。
首先,GC并不是能释放所有的资源。它不能自动释放非托管资源。
第二,GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。
C#线程同步的几种方法:参见:http://www.cnblogs.com/michaelxu/archive/2008/09/20/1293716.html