.Net Discovery系列文章是讲述.Net平台机制的文章,目前已有12篇,分别讲述了.Net垃圾收集、实时编译、字符串等部件的机制,现在推出1周年之际总结文章阅读索引,希望对大家有所帮助。
string--.Net平台永恒的话题。这是一种比较特殊的数据类型,它是基元类型,也是引用类型,在编译以及运行时,.Net平台都对它做了一些优化工作,正是这些优化工作有时会迷惑编程人员,使string看起来难以琢磨,这篇文章分上下两章,共四节,来讲讲关于string的陌生一面。
重点回顾: 在C#中,如果用new关键字实例化一个类,对应是由IL指令newobj来完成的;而创建一个字符串,则由ldstr指令完成,看到ldstr指令,我们即可认为,IL希望创建一个新的字符串 。 从某些方面讲,正是字符串的恒定性,才造就了字符串的驻留机制,也为字符串的线程同步工作大开方便之门(同一个字符串对象可以在不同的应用程序域中被访问,所以驻留的字符串是进程级的,垃圾回收不能释放这些字符串对象,只有进程结束这些对象才被释放)。 在.Net中处理字符串时,有一个很重要的机制,叫做字符串驻留机制。由于string是编程中用到的频率较高的一种类型,CLR对相同的字符串,只分配一次内存。CLR内部维护着一块特殊的数据结构,我们叫它字符串池,可以把它理解成是一个HashTable,这个HashTable维护着程序中用到的一部分字符串,HashTable的Key是字符串的值,而Value则是字符串的内存地址。一般情况下,程序中如果创建一个string类型的变量,CLR会首先在HashTable遍历具有相同Hash Code的字符串,如果找到,则直接把该字符串的地址返回给相应的变量,如果没有才会在内存中新建一个字符串对象。 |
通过上一篇文章,大家会了解字符串驻留机制、恒定性、常量池等特性,这篇文章通过10个例子,来为大家讲解string,同时如果你自己对string的了解程度,有足够的信心,那么就来读一下这篇文章,试试做一下10个例子,检测一下自己有多“牛”。
重点回顾: 代码九:
string a = "a"; string b = new string('a', 1); Response.Write(a.Equals(string.Intern(b))); Response.Write(ReferenceEquals(a, string.Intern(b))); 输出:True (Equals比较值,无论是否Intern都会相同) True (ReferenceEquals比较字符串对象的引用,Intern已经将b驻留至字符串池内) 代码十: string a = "str"; string b = "str_2".Substring(0,3); Response.Write(a.Equals(b)); Response.Write(ReferenceEquals(a, b)); 输出:True (Equals比较值,a与c的值相同) False (ReferenceEquals比较字符串对象的引用,Substring操作产生了新的字符串对象) 此段代码产生了3个string对象,是哪3个呢?如果你不明白,还是从头再看一遍吧! |
这篇文章将全面的为大家介绍.Net 垃圾收集的运行方式、算法,以及与垃圾收集相关的关键方法。 说到垃圾收集机制,很少有人知道,垃圾收集并不是伴随Java出现的,早在1958年,图林奖得主John发明的Lisp语言就已经提供了GC的功能,这是GC的第一次出现,是思想的一次闪光!而后,1984年Dave Ungar发明的Small talk语言第一次正式采用了GC机制。
这篇文章将重点为大家介绍.Net垃圾收集器、代龄、策略引擎,并结合Windbg为大家展现一个有趣的机制平台。
重点回顾: .Net中采用了一种叫做“标记与清除(Mark Sweep)”算法来完成上述任务。 “标记与清除”算法,顾名思义,这种算法有两个本领: “标记”本领——垃圾的识别:从应用程序的root出发,利用相互引用关系,遍历其在Heap上动态分配的所有对象,没有被引用的对象不被标记,即成为垃圾;存活的对象被标记,即维护成了一张“根-对象可达图”。 其实,CLR会把对象关系看做“树图”,无疑,了解数据结构的同学都知道,有了“树图”的概念,会加快遍历对象的速度。 检测、标记对象引用,是一件很有意思的事情,有很多方法可以做到,但是只有一种是效率最优的,.Net中是利用栈来完成的,在不断的入栈与出栈中完成检测:先在树图中选择一个需要检测的对象,将该对象的所有引用压栈,如此反复直到栈变空为止。栈变空意味着已经遍历了这个局部根(或者说是树图中的节点)能够到达的所有对象。树图节点范围包括局部变量(实际上局部变量会很快被回收,因为它的作用域很明显、很好控制)、寄存器、静态变量,这些元素都要重复这个操作。一旦完成,便逐个对象地检查内存,没有标记的对象变成了垃圾。 “清除”本领——回收内存:启用Compact算法,对内存中存活的对象进行移动,修改它们的指针,使之在内存中连续,这样空闲的内存也就连续了,这就解决了内存碎片问题,当再次为新对象分配内存时,CLR不必在充满碎片的内存中寻找适合新对象的内存空间,所以分配速度会大大提高。但是大对象(large object heap)除外,GC不会移动一个内存中巨无霸,因为它知道现在的CPU不便宜。通常,大对象具有很长的生存期,当一个大对象在.NET托管堆中产生时,它被分配在堆的一个特殊部分中,移动大对象所带来的开销超过了整理这部分堆所能提高的性能。 |
这一篇主要讲了GC相关的重要方法。主要包括终止队列(Finalization Queue)与可达队列(Freachable Queue)、复生(Resurrection)、弱引用(WeakReference)、策略引擎、Dispose()、GC.Collect()、析构函数(Finalize()等知识点。
重点回顾: 首先要了解与Finalize相关的两个队列:终止队列(Finalization Queue)与可达队列(Freachable Queue),这两个队列存储了一组指向对象的指针。 当程序中在托管堆上分配空间时(new),如果该类含有析构函数,GC将在Finalization Queue中添加一个指向该对象的指针。 在GC首次运行时,会在已经被确认为垃圾的对象中遍历,如果某个垃圾对象的指针被Finalization Queue包含,GC将这个对象从垃圾中分离出来,将它的指针储存到Freachable Queue中,并在Finalization Queue删除这个对象的指针记录,这时该对象就不是垃圾了——这个过程被称为是对象的复生(Resurrection)。当Freachable Queue一旦被添加了指针之后,它就会去执行对象的Finalize()方法,清除对象占用的资源。 当GC再次运行时,便会再次发现这个含有Finalize()方法的垃圾对象,但此时它在Finalization Queue中已经没有记录了(GC首次运行时删掉了它的Finalization Queue记录),那么这个对象就会被回收了。 |
JIT--实时编译机制是.Net平台的又一亮点,这个文章将分为上下两节,从运行原理、机制等方面,结合WinDbg为大家详细的讲解JIT方面的知识。关键字:JIT MSIL 元数据 方法表 托管模块 本地映像。
重点回顾: JIT是运行时的一个重要职责模块,它将IL转换为本地CPU指令,从上图可以看出,也许你不敢相信,即时编译这个过程是在运行时发生的,这会不会对性能产生影响呢?事实上答案是虽然是肯定的,但这种开销物有所值: 1. JIT所造成的性能开销并不显著。 2. JIT遵循计算机体系理论中两个经典理论:局部性原理与8020原则。局部性原理指出,程序总是趋向于使用最近使用过的数据和指令,这包括空间的和时间的,将局部性原理引申可以得出,程序总是趋向于使用最近使用过的数据和指令,以及这些正在使用的数据和指令临近的数据和指令(凭印象写的,但不曲解原意);而8020原则指出,系统大多数时间总是花费80%的时间去执行那20%的代码。 根据这两个原则,JIT在运行时会实时的向前、后优化代码,这样的工作只有在运行时才可以做到。 3. JIT只编译需要的那一段代码,而不是全部,这样节约了不必要的内存开销。 4. JIT会根据运行时环境,即时的优化IL代码,即同样的IL代码运行在不同CPU上,JIT编译出的本地代码是不同的,这些不同代码面向自己的CPU做出了优化。 5. JIT会对代码的运行情况进行检测,并对那些特殊的代码经行重新编译,在运行过程中不断优化。 |
这一篇文章主要讲了JIT一些实例,结合Windbg,对代码进行运行时监控,并通过Windbg的反馈向大家展示运行时编译的过程。
重点回顾: 回车后注意高亮区域的信息:
图8 JIT前A类型的信息 高亮区域显示的是“ 同样,!name2ee *!JITTester.B和!name2ee *!JITTester.C命令会得到同样的结果。 好,现在做第4步操作,Detach Debuggee进程,并回到程序中点击“GO”按钮
图9 点击按钮 第五步 重新附加进程(参考第一步),这时程序已经调用了new A().a1()方法,并重新执行命令!name2ee *!JITTester.A ,注意高亮部分
图10 JIT后A类型的信息 和图8中的信息比较,图10中的方法表地址已经变为JIT后的内存地址,这时图4中的Stub槽将被一条强制跳转语句替换,跳转目标与该地址有关。这一点说明JIT在大多情况下,只编译一次代码。 |
新年伊始,该文章是博客园2010年的第一篇文章,感兴趣的同学可以注意一下该文章的发布日期,是2010年1月1日1秒。
本文分三节为大家深入介绍.Net GC的完整收集(Full GC)机制 、GC工作模式以及.Net 4.0中GC的特性方法。
重点回顾: Workstation GC without Concurrent: 用于单CPU的服务器,策略引擎会调节GC工作频率,使用挂起->查找与标记->压缩->恢复的流程进行GC工作。 Workstation GC with Concurrent: Concurrent GC与Non Concurrent GC模式相比,有着更敏捷的反应速度,Winform应用程序和Windows services 服务程序默认采用这种模式,单CPU机器上只能使用workstation GC方式,默认为 Workstation GC with Concurrent。 在这种模式下,第0、1代的收集仍然是要暂时挂起应用程序的,只有在收集第2代时,才会并行处理,这种并行收集是利用多CPU 对Full GC进行并行处理,具体原理是将Full GC过程切分成多个短暂子过程对线程进行冻结,在线程冻结时间之外,应用程序仍然可 以正常运行。这主要通过将0代空间设置的很大,使Full GC时,CLR仍然能够在0代中进行内存分配,如果Full GC时0代内存也已用尽,那么应用程序将被挂起,等待Full GC的完成。 Server GC: 用于多CPU的服务器,这种GC模式有着很高的性能和效率。这种模式下,CLR为每个CPU创建一个专用的GC线程,每个CPU可以独立的为相应的 heap执行GC操作,这些GC线程是以非并发的形式工作的,收集工作与线程正常工作不能同时进行,这就是说第0、1、2代的收集都会挂起应用线程。 在.Net 4.0中,有一种新的垃圾收集机制,叫做后台收集。这种机制以concurrent GC为基础的,如上文所讲,Workstation GC with Concurrent模式中,在Full GC过程时,CLR仍然能够在0代中进行内存分配,如果Full GC时0代内存也已用尽,那么应用程序将被挂起,等待Full GC的完成。 这个过程在后台收集机制中是这样工作的,在进行Full GC时可以同时进行第0、1代收集,并且后台收集是一个独立线程完成的,这个进程任务优先级低于第0、1代收集,如果在后台收集中需要对第0、1代收集,后台收集将会等待第0、1代收集完成后再进行工 作,当然第0、1代收集是需要短暂挂起应用的。 后台收集还会根据策略引擎的指示,动态调节第0、1代的容量,减少前台收集(第0、1代收集)次数。 |
本文是《.Net Discovery》系列文章(一)的勘误版。
重点回顾: 所以,第三行C#代码(a = "str_2";)的样子看起来是在修改变量a的旧值"str_1",但实际上是创建了一个新的字符串"str_2",然后将变量a的指针指向了"str_2"的内存地址,而"str_1"依然在内存中没有受到任何影响,所以变量b的值没有任何改变---这就是string的恒定性,同学们,一定要牢记这一点,在.Net中,string类型的对象一旦创建即不可修改!包括ToUpper、SubString、Trim等操作都会在内存中产生新的字符串。 |
本文是《.Net Discovery》系列文章(二)的勘误版。
重点回顾: 代码二: string a = "str_1str_2"; string b = "str_1"; string c = "str_2"; string d = b + c; Response.Write(a.Equals(d)); Response.Write(ReferenceEquals(a, d)); 输出:True(Equals比较字符串对象的值) False(ReferenceEquals比较字符串对象的引用,由于变量d的值为变量连接的结果,字符串驻留机制无效) 代码三: string b = "str_1" + "str_2"; Response.Write(a.Equals(b)); Response.Write(ReferenceEquals(a, b)); 输出:True(Equals比较字符串对象的值) True (ReferenceEquals比较字符串对象的引用,由于变量b的值为常量连接的结果,字符串驻留机制有效。如果变量b的值由“常量+变量”的方式得出,则字符串驻留无效) |
转眼间《.Net Discovery》系列文章已经推出1年了,本文为该系列的第10-13篇文章,在本文中将对以前所讲的.Net平台知识做一个小小的总结与机制分析,引出并重点介绍这些机制对程序性能的影响与改进建议。 本文将分为四部分,分别讲述了:垃圾回收机制、即时编译机制、异常处理机制、字符串驻驻留机制的原理与性能改进建议。
本文主要介绍垃圾回收机制对系统性能的影响分析。
重点回顾: 垃圾收集器一般将托管堆中的对象分为3代,这可以通过调用GC.MaxGeneration得知,对象按照存在时间长短进行分代,最短的分在第0代,最长的分在第2代,第2代中的对象往往是比较大的,第二代空间被称作Large Object Heap,对于2代对象的回收,与第0、1代回收方式相比最大的不同在于,没有了指针移动的压缩过程。 如下图,第一次GC时,左边第一列A-F表示内存中的对象,位于浅蓝色 区域,经过Mark后,ACDF标记为可用,Sweep过程清除了BE,Compact过程移动了ACDF,使之位于连续存储区域中;第二次使用绿色做标记;第三次GC使用蓝色表示标记;可以看出第三次GC过程没有了指针移动的压缩过程。
图1 对象的回收 |
转眼间《.Net Discovery》系列文章已经推出1年了,本文为该系列的第10-13篇文章,在本文中将对以前所讲的.Net平台知识做一个小小的总结与机制分析,引出并重点介绍这些机制对程序性能的影响与改进建议。 本文将分为四部分,分别讲述了:垃圾回收机制、即时编译机制、异常处理机制、字符串驻驻留机制的原理与性能改进建议
本文主要介绍即时编译机制对系统性能的影响分析。
重点回顾: 运行时,操作系统会根据托管模块中各种头信息,装载相应的运行时框架,Load()被加载,由于是第一次加载,这会触发对Load()的即时编译,JIT会检测Load()中引用的所有类型,并结合元数据遍历这些类型中定义的所有方法实现,并用一个特殊的HashTable(仅用于理解)储存这些类型方法与其对应的入口地址(在未被JIT前,这个入口地址为一个预编译代理(PreJitStub),这个代理负责触发JIT编译),根据这些地址,就可以找到对应的方法实现。 在初始化时,HashTable中各个方法指向的并不是对应的内存入口地址,而是一个JIT预编译代理,这个函数负责将方法编译为本地代码。注意,这里JIT还没有进行编译,只是建立了方法表!
图2方法表、方法描述、预编译代理关系 图2中所示的MS核心引擎指的是一个叫做MSCorEE的DLL,即Microsoft .NET Runtime Execution Engine,它是一个桥接DLL,连同mscorwks.dll主要完成以下工作: 1. 查找程序集中包含的对应类型清单,并调用元数据遍历出包含的方法。 2. 结合元数据获得这个方法的IL。 3. 分配内存。 4. 编译IL为本地代码,并保存在第3步所分配的内存中。 5. 将类型表(就是指上文中提到的HashTable)中方法地址修改为第3步所分配的内存地址。 6. 跳转至本地代码中执行。 所以随着程序的运行时间增加,越来越多的方法的IL被编译为本地代码,JIT的调用次数也会不断减少。 |
转眼间《.Net Discovery》系列文章已经推出1年了,本文为该系列的第10-13篇文章,在本文中将对以前所讲的.Net平台知识做一个小小的总结与机制分析,引出并重点介绍这些机制对程序性能的影响与改进建议。 本文将分为四部分,分别讲述了:垃圾回收机制、即时编译机制、异常处理机制、字符串驻驻留机制的原理与性能改进建议
本文主要介绍异常处理机制、字符串驻驻留机制对系统性能的影响分析。
重点回顾: .Net 中基本的异常捕获与处理机制是由try…catch…finally块来完成的,它们分别完成了异常的监测、捕获与处理工作。一个try块可以对应零个或多个catch块,可以对应零个或一个finally块。不过没有catch的try似乎没有什么意义,如果try对应了多个catch,那么监测到异常后,CLR会自上而下搜索catch块的代码,并通过异常过滤器筛选对应的异常,如果没有找到,那么CLR将沿着调用堆栈,向更高层搜索匹配的异常,如果已到堆栈顶部依然没有找到对应的异常,就会抛出未处理的异常了,这时catch块中的代码并不会被执行。所以距离try最近的catch块将最先被遍历到。 最后三行的每一个Item被称作Exception Handing Clause,EHC组成Exception Handing Table,EHT与正常代码之间由ret返回指令隔开。 可以看出,FormatException排列在EHT的第一位。 当代码成功执行或反之而返回后,CLR会遍历EHT: 1. 如果抛出异常, CLR会根据抛出异常的代码的“地址”找到对应的EHC(IL_0001 to IL_0010为检测代码的范围),这个例子中CLR将找到2条EHC, FormatException会最先被遍历到,且为适合的EHC。 2. 如果返回的代码地址在IL_0001 to IL_0029内,那么还会执行finally handler 即IL_0029 to IL_0033中的代码,不管是否因成功执行代码而返回。 事实上,catch与finally的遍历工作是分开进行的,如上文所言,CLR首先做的是遍历catch,当找到合适的catch块后,再遍历与之对应finally;而且这个过程会递归进行至少两次,因为编译器将C#的try…catch…finally翻译成IL中的两层嵌套。 当然如果没有找到对应的catch块,那么CLR会直接执行finally,然后立即中断所有线程。Finally块中的代码肯定会被执行,无论try是否检测到了异常。 |