CLR整理

CLR 公共语言运行时
托管执行过程
1.选择编译器
   .net中包括c#,vb,vc++等,选择合适编译器
2.编译为 MSIL
    编译器将源代码转换为 Microsoft 中间语言 (MSIL)  和计算机系统结构有关
3.将 MSIL 编译为本机代码
    根据本机结构编译为本机代码:
     方法1:.net提供.net实时(JIT)编译器 :按需编译代码,边运行边编译,性能会有略微影响;
     方法2:使用 NGen.exe 的安装时代码生成:预编译,在运行前将整个程序集编译,存储;
4.代码验证
     确认代码是否有权访问内存位置;
5.运行代码
    JIT编译:编译运行,编译过的方法,不用再编译,可直接使用;
    NGen.exe编译:将存储的映像中的入口点作为运行时的入口点,开始运行;
自动内存管理
在运行时管理内存的分配和释放:帮助处理常见内存问题,如:忘记释放对象导致内存泄露、尝试访问已释放对象的内存等;
1.分配内存
初始化时,在内存中保留一个连续的地址空间区域(最初时虚拟内存,并没有对应的物理内存),为托管堆,托管堆中有一个指针最初指向托管堆的基址,每存入对象后根据存入对象的地址和存入对象的长度,更新这个指针使之一直指向下一个可用地址;托管堆将进程隔离,进程之间不会互相访问相同地址;在托管堆中, 连续分配的对象可以确保它们在内存中是连续的。托管堆做了一个相当大胆的假设一地址空间和存储是无限的。这个假设显然是荒谬的。托管堆必须通过某种机制来允许它做这样的假设。这个机制就是垃圾回收器。后面讲述它的工作原理 。
在新建对象时,值类型根据类型字节数分配内存字节数,如int分配4个;引用类型内存分配包括两部分,指针占4个字节,内容是地址或者null,引用类型内容占实际需要字节数加上4字节(32位)方法表指针,4字节(32位)同步索引块,在64位机器上方法表指针和同步所以块各占8位;
引用类型一般被如下的对象引用:
● 栈上的一个变量(最常见的情况)。
● P/Invoke 情形下的句柄表。
● Finalizer queue,即终结队列。
● 寄存器。
如创建下面的User对象需要内存:
public class User
{
    public int Age { get; set; }
    public string Name { get; set; }

public string _Name = “123” + “abc”;
    public List _Names;
    public string GetName()
    { 
        Name = _Name;
      return Name;
    }
}
属性Age值类型Int,4字节;
属性Name,引用类型,初始为NULL,4个字节,指向空地址;
字段_Name初始赋值了,代码会被编译器优化为_Name=”123abc”。一个字符两个字节,字符串占用2×6+8(附加成员:4字节TypeHandle地址,4字节同步索引块)=20字节,总共内存大小=字符串对象20字节+_Name指向字符串的内存地址4字节=24字节;
引用类型字段List _Names初始默认为NULL,4个字节;
User对象的初始附加成员(4字节方法表指针,4字节同步索引块)8个字节;
User对象外部定义需要4字节存放引用地址;
所以创建User对象共需要44个字节;
32 位机上,任何对象占据的字节数都必须是 4 的倍数,即使只有1个byte,也要占4个字节,这称为内存的对齐(alignment),如果在 64 位机上,任何对象占据的字节数都必须是 8 的倍数,只有1个byte也占8个;但是默认情况下,CLR 会智能地将可以合并到 4/8 字节的对象尽量合并到一起,以免内存空间浪费,除非显式地阻止它。例如,64 位机器上两个 int,四个 short,8 个 byte 可以合并到一起。32位机器可以将两个short,4个byte合并到一起;所以类型字段最终被创建在内存的顺序不一定就是它在代码中的顺序,这是因为 CLR 会选择一个较好的方式排列这些字段,尽量消除对齐带来的负面影响。
如果对象User还有父类,还需要考虑父类类型的需要的内存;
在计算内存时只考虑了字段和嵌套类型,方法的保存就是在方法表地址和同步块索引;
同步块索引:
同步块索引(synchronization block index)是类的标准配置,它位于类在堆上定义的开头 -4(或 -8)至 0 字节。在对象内存的前面;
在程序运行时,CLR 管理一个同步块数组。它是一个总共 32/64 位的多功能结构,其中,前 6 位的值提示访问者目前同步块索引的功能是什么,高 6 位就像 6 个开关,有的打开(1),有的关闭(0),不同位的打开和关闭有着不同的意义。
它的用处非常广泛,例如线程同步和 GC 都会用到它,它还会储存对象的哈希码。
同步块索引在线程同步中用来判断对象是被使用还是闲置。
默认的情况是,同步块索引被赋予一个特殊的值,此时对象没有被线程独占。当一个线程拿到对象,并打算对其操作时,它会检查对象的同步块索引。
如果索引的值为特殊值,说明没有任何线程正在操作它,此时这个线程获得它的操作权。
同时在 CLR 的同步块数组中分配一个新的同步块,并将该块的索引值写入实例的同步块索引值中。
这时,如果有其他线程来访问该实例,它就不能操作这个实例了,因为它的同步块索引的值不为特殊值。
当独占的线程操作完之后,同步块索引的值被重设回特殊值。
方法表指针:
方法表指针(method table pointer)又叫类型对象指针(TypcHandle)。
类型对象由 CLR 在加载堆中创建,创建时机为加载该程序集时。类型对象最重要的成员为类型的静态字段和方法表,创建完之后就不会改变,通过这个事实,可以验证静态字段的全局性。
因为类型对象存储了静态字段和方法表,它们被所有的该类型实例共享。
因此为了做到这点,需要满足如下条件:
● 一个类型无论有多少个实例,这些实例在堆中的内存的类型对象指针都指向同一个类型对象
● 类型对象的位置在不受 GC 控制的加载堆中。即使没有任何实例类型指向它,它也不会被回收。如果它被回收,下次实例类型的创建会伴随类型对象的创建,而这是没有必要的。
静态字段很好理解,方法表就是类型所有的方法,包括静态方法和实例方法。方法会在初次执行时,经由 JIT 编译为机器码,并将机器码存在内存之中,获得一个入口地址。
此时,方法表中的该方法指向一个 jmp 指令,使得其可以跳跃到该入口地址。在下次调用该方法时,直接跳到入口地址,无需再次编译。
类型对象是反射的重要操作对象。System.Type 类会返回类型对象(包括静态成员和方法表)。获得类型对象之后,就可以得到该对象所有的信息。
注意,类型对象也有类型对象指针,这是因为类型对象本质上也是对象。所有类型对象的“类型对象指针”都指向 System.Type 类型对象。
值得提出的是,System.Type 类型对象本身也是一个对象,内部的“类型对象指针”指向它自己。
静态字段和静态属性:
类型的静态字段和静态属性的支持字段(例如 int)存储在类型对象(加载堆)中。
JIT 会在进行编译时找到这些静态成员的地址,并在之后的编译时硬编码它们,然后写在机器码中。
这样,再次访问静态成员时就不需要通过类型对象了。如果你还不知道属性是什么,这里可以简单地理解为属性等于一个支持字段加两个方法,用来获得和写入属性的值。
程序中所有类型的静态成员组成一个全局的数组,它包括每一个类型中的基元类型静态成员的内存地址。
数组的地址会被钉死 (pinned),使得它不会被 GC 回收掉(除非卸载应用程序域),这样一来,机器码中的硬编码将一直有意义,直到程序终止。
2.内存回收
当满足以下条件之一时将发生垃圾回收:
● 系统具有低的物理内存。 这是通过 OS 的内存不足通知或主机指示的内存不足检测出来。
● 由托管堆上已分配的对象使用的内存超出了可接受的阈值。 随着进程的运行,此阈值会不断地进行调整。
● 调用 GC.Collect 方法。 几乎在所有情况下,你都不必调用此方法,因为垃圾回收器会持续运行。 此方法主要用于特殊情况和测试。
值类型(含所有枚举类型)、集合类型、 String、 Attribute、 Delegate和 Exception 所代表的资源无需执行特殊的清理操作。例如, 只需销毀对象的内存中维护的字符数组, 一个 String资源就会被完全清理。 
如果一个类型代表者(或包装着)一个非托管资源或者本地资源(比如文件、数据库連接、套接字、 mutex、位图、图标等),那么在对象的内存准备回收时,必须执行一些资源清理代码。
在给新对象分配内存时,通过对象的大小加上目前指针的地址,如果大于堆的结尾地址说明托管堆已满,必须执行一次垃圾回收,首先从第0代回收,再第1代,第2代;
每个应用程序都包含一组根(root)。 每个根都是一个存储位置, 其中包含指向引用类型对象的一个指针。该指针要么引用托管堆中的一个对象,要么为 null。例如,类型中定义的任何静态字段被认为是一个根。除此之外,任何方法參数或局部变量也被认为是一个根。只有引用类型的变量才被认为是根;值类型的变量永远不被认为是根。
运行时的垃圾回收算法基于计算机软件行业通过试验垃圾回收方案而发现的几种概括:
● 一、压缩一部分托管堆比压缩整个托管堆要快。
● 二、较新的对象生存期较短,而较旧的对象生存期则较长。
● 三、新对象之间相互关联并且一般在同一时间被应用程序访问(heap分配的对象是连续的,关联度较强有利于提高CPU cache的命中率)。
将新对象存在第0代,当第0代满的时候,回收第0代不用的对象,将仍在使用的对象升级到第1代中;当第0代不足分配的时候,回收第1代不用的对象,将第1代仍在使用的对象升级到第2代中;当第1代不足的时候,回收第2代不用的对象,第2代仍在使用的对象仍在第2代;
运行时的垃圾收集器在0代存储新对象。早期的没有被回收的对象将会从0代升级到1或者2代。由于压缩一部分托管堆比压缩整个托管堆要快,因此垃圾回收方案在执行垃圾回收时允许在一个特定的代中释放空间。
实际上,垃圾收集器在0代满时会执行一次垃圾回收。由于在0代的对象都是生命周期短的对象(在1、2代中都是没有被回收的长期引用的对象。),所以该策略很高效。在回收后,将压缩的可达对象升级为1代。
当0代不足以分配空间时,将会在1、2代中执行垃圾回收。1代中垃圾回收的压缩对象可升级为2代。2代中免于回收的对象仍然在2代中。回收某个代意味着回收此代中的对象及其所有更年轻的代。 第 2 代垃圾回收也称为完整垃圾回收,因为它回收所有代中的对象(即,托管堆中的所有对象)。
大部分情况,GC只需要回收0代即可,这样可以显著提高GC的效率,而且GC使用启发式内存优化算法,自动优化内存负载,自动调整各代的内存大小。
操作系统资源例如文件句柄、window句柄、网络连接都是非托管资源,需要显式去释放。尽管垃圾回收器追踪一个封装非托管资源对象的生命周期,但是没有明确的释放资源的方法。当封装一个非托管资源,建议提供Dispose方法,在该方法中显式释放内存资源。
一旦程序达到某个内存阈值,需要更多的堆空间,GC就会启动。这就是垃圾回收(GC)发挥作用的地方。GC会停止所有正在运行的线程(完全停止),找到堆中所有主程序没有访问的对象并删除它们。
如果托管堆上的某个对象不被任何其他对象关联(即没有任何栈上的对象地址是它的所在地址),则它成为垃圾,就会被垃圾回收器进行回收。然后GC将重新组织堆中剩下的所有对象以留出空间,并在堆栈和堆中调整指向这些对象的所有指针。正如您可以想象的那样,这在性能方面是非常昂贵的,所以现在您可以看到为什么在尝试编写高性能代码时,关注栈和堆中的内容是非常重要的。

非托管资源:
.net对象使用到的非托管资源主要有I/O流、数据库连接、Socket连接、窗口句柄等直接与操作系统操作的相关资源。当一个对象不再使用时,我们应该将它使用的非托管资源释放掉,归还给操作系统,不然等到CLR将它在队中的内存回收之后。即没有对象引用再在指向它,只能等到整个应用程序运行结束后才能归还给系统。所以我们应当在该对象有对象引用指向它时释放非托管资源。
.net中提供释放非托管资源的方式主要是:Finalize 和 Dispose。常用的大多是Dispose模式,主要实现方式就是实现IDisposable接口,IDisposable接口内包含Dispose方法,Dispose需要手动调用,在.NET中有两中调用方式,一种是手动显示调用,但是如果写程序时忘记了手动调用,资源就得不到释放,所以一般使用第二种using语句更好,using语句中可使用的类型必须是实现IDisposable接口的类型,using语句实质是try…finally语句,在finally中调用了IDisposable接口的Dispose方法,所以不用再担心忘记调用Dispose导致资源无法释放的情况;
IDisposable 接口:提供一种用于释放非托管资源的机制。此接口的主要用途是释放非托管资源。 当不再使用对象时,垃圾回收器会自动释放分配给托管对象的内存。 但是,无法预测何时会进行垃圾回收。 而且,垃圾回收器不知道非托管资源,如窗口句柄,或者打开文件和流。使用此接口的方法Dispose可与垃圾回收器一起显式释放非托管资源。 当不再需要对象时,对象的使用者可以调用此方法。几乎每一个使用非托管资源的类型都应该实现这个接口。所以在.net库中实现这个接口的也应该都代表该类型中包含非托管资源;如Form类,Form继承自Contorl类,Contorl类实现了IDisposable接口,所以在临时创建Form对象时可以使用using语句;

问题1:
.net中用来存放局部变量、值对象等的栈在哪?在托管堆内还是外?哪个位置上,大小多少?
栈或多或少地负责跟踪我们的代码中正在执行的内容(或被“调用”的内容)。堆或多或少负责跟踪我们的对象(对象的是数据部分);我们只能用栈顶盒子里的东西。当我们用完顶部的盒子(方法执行完毕),我们就把它扔掉,然后继续使用堆栈顶部的前一个盒子里的东西,栈以此来跟踪应用程序中发生的事情;栈负责跟踪代码执行期间每个线程的位置(或所谓的位置)。可以将其视为一个线程“状态”,每个线程都有自己的栈。当我们的代码调用执行一个方法时,线程开始执行已经JIT编译并驻留在方法表上的指令,它还将方法的参数放在线程堆栈上。然后,当我们浏览代码并遇到方法中的变量时,它们被放在堆栈的顶部。
堆是类似的,除了它的目的是保存信息(大多数时间不是跟踪执行);值类型有时也放在堆上。还记得这个规则(下面两天黄金法则)吗,值类型总是去声明它们的地方?如果值类型在方法外部声明,但在引用类型内部,那么它将被放置在堆上的引用类型中。
两条黄金法则:引用类型总是放在堆上;值类型和指针总是去声明它们的地方;

问题2:
.net托管堆中大对象堆(LOH)在哪?托管堆内还是外?如果在托管堆内是在第0代、第1代、第2代中还是在外面?大对象堆大小多少?方法表指针(类型对象)是不是也是在托管堆内的一个特殊区域?
不少于85000字节的属于大对象,这些对象通常是数组。 非常大的实例对象是很少见的。可以配置阈值大小,以使对象能够进入大型对象堆。.net中所有的大对象将分配在托管堆内一个特殊的区域,在托管堆内部。为了改进性能,运行时单独为堆中的大型对象分配内存。 垃圾回收器会在回收第2代时一起回收大型对象的内存。 但是,为了避免移动内存中的大型对象,通常不会压缩此内存。

问题3:
.net托管堆在内存分配是顺序存储,所以访问会很快,在存的时候根据维护的地址指针会很快,那么访问以前保存的对象是怎么访问的?

问题4:
托管堆第0代、第1代、第2代有多大?当都不够的时候怎么办?如果扩容,扩容的大小是怎么定的?扩容的方式是拷贝吗?

问题5:
在程序运行时如何监视托管堆的大小和使用情况?第0代、第1代、第2代的大小和使用情况?

.Net 内存对象分析文章
https://www.cnblogs.com/tianqing/p/7630636.html

https://www.cnblogs.com/anding/p/5260319.html

上一篇:wpf中的属性,依赖属性和依赖对象


下一篇:Leetcode 1578. 避免重复字母的最小删除成本(DAY 120) ---- 贪心算法学习期