var _gaq = _gaq || []; _gaq.push(['_setAccount', 'UA-333696-1']); _gaq.push(['_trackPageview']); _gaq.push(['_trackPageLoadTime']); (function() { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })();
  • 2009年03月03日

    网页应用程序反应时间

    分类:

    Grow Fast We Apps Organical中讲道,Keynote公司度量反应时间,最近一期Keynote商业40报告中平均反应时间为1.82秒。

     

    动态事务访问多架构层,它典型地可能包括一个Web服务器,应用程序服务器,数据库和后端/主机服务器。一个动态事务的执行是重要的。当更多层和集成点允许一个更灵活的系统实施,每个集成点增加反应和执行时间。该总开销可能包括数据的列集/散列,压缩/解压缩,和队列/出列。独立的这些行为可能仅需耗费数百万秒,但合聚起来能增至数秒。

     

    通常复杂动态事务包括账务详细和搜索。下图显示最佳反应时间从最近信用卡账户细览报告由Gomez生成。反应时间范围介于8~17秒,并且平均反应时间是14秒。用户似乎习惯这个执行时间长并且期望被有效管理通过进度条,动画消息.gif文件或其它方法。

     

    对媒体店,它典型使用内容管理引擎并带有多个数据库,Gomez跟踪搜索反应时间(见下图)。该范围从4秒到多于15秒,平均11秒左右。

     

    诸如这些提供的真实性能数据你能用来对比你的界面。在我们顾问实施中,我们理想争取12秒的反应时间-对静态网页有效。然而,对当今复杂的动态事务来说,一个更实际的跨越静态和动态内容的反应时间应该是3~8秒。达成用户体验通过包括内容缓存,异步加载技术和进度条技术的使用,所有这些有效地辅助达成用户的期望和大部分用户满意。

  • 原作者:John Thomas, Markus Clermont

    原文第一部分第二部分

     

    分析导航板过滤

    l  客户端

    ¡  RPC获得所有的办公室

    ¡  选选择,通过RPC获取选项。选完成,更新表。

    ¡  选取消选择,从表上清空选项。

     

    l  RRC实现

    ¡  RPC客户端获得所有办公室

    ¡  RPC后端获取一个办公室所有货物

     

    l  RPC后端

    ¡  扫描所有办公室的bigtable

    ¡  bigtable查询指定办公室货物

     

    我们下一步就是要找出“最小的测试”,它可以给我们信心即每一个组件如预期般运作。

     

    测试客户端行为

    确保取消选择一项会删除它。为此,我们需要确定那些项目将在列表中。假的RPC实现可以做到-独立于其它测试可能使用相同的数据源。

     

    该任务是使Servlet同作为“模拟存储服务”即RPC实现交互。我们有不同的可能性实现:

    l  引入一个标记以切换

    l  使用代理模式

    l  在运行时切换它

    l  给该servlet增加一个不同的构造

    l  引入一个不同的构造目标使得链接到假实现

    l  使用依赖注入换出真实而以假实现替代

     

    这些选项中的任何一个都可以实现这有赖于应用程序。像给该servlet增加一个新构造需要依赖测试代码的生产代码,这显然是一个坏主意。运行时切换(使用类加载器伎俩)同样是一个选项但会暴露安全漏洞。依赖注入提供一个灵活又有效的途径来实现却不污染生产代码。

     

    有不同的框架允许这种形式的依赖注入。我们想简要介绍它们中的一个GuiceBerry

     

    GuiceBerry是一个允许对你测试需要的服务使用组合模型。换句话说,如果你的测试依赖某些服务你能使这些服务“注入”到你的测试使用一个流行的叫Guice的依赖注入工具。

     

    在我们的示例中我们需要用”@Inject”标记RPC实现对象在servlet测试类并创建一个替代实现叫模拟存储服务以换入在运行时。这里是一个代码片段展示如何实现:

    @GuiceBerryEnv(StoreGuiceBerryEnvs.NORMAL)

     

    public class StorePortalTest extends TestCase {

    @Inject

    StoreServiceImpl storeService;

    public void testStorePortal() {

    ...

    storeService.doSomething();

    ...

    }

    }

     

    上面代码片段中,注意加粗行。那是Guiceberry的魔力使得我们把StoreServiceImpl对象注入到StorePortalTest类。StoreServiceImpl的构造在GuiceberryNormalStoreGuiceBerryEnvs

    的环境类(并通过StoreGuiceBerryEnvs类链接到StorePortal)。为了注入模拟RPC实现进StorePortalTest我们可能需要创建MockStoreGuiceBerryEnvs(它能初始化模拟存储服务)并在运行时交换为NormalStoreGuiceBerryEnvs。所有我们要做是为测试指定以下的 JVM 参数...

    JVM_ARGS="-DNormalStoreGuiceBerryEnvs=MockStoreGuiceBerryEnvs"

    这就是快速一瞥Guiceberry如何运行。访问Guiceberry官方网站学习更多。

     

    这将足够从系统其余部分分离客户端。GwtTestCase在客户端作了剩下的工作。你能找到更多详情这里。不要忘记注入所有可能失败的场景通过模拟存储服务。

     

    让我们看看目前我们发现了:

    l  我们知道

    ¡  用户界面回调工作正常

    ¡  交互界面=前端工作正常

    ¡  用户界面充分处理预期错误

     

    l  我们不知道是否

    ¡  事物是否正确渲染

    ¡  在页面上我们期待的事物是否真在那

     

    尽管我们已经找出大量的用户界面,实在是言之过早确信客户端如预期般工作。我们需要了解更多的用户界面以回答剩下的两个问题。

     

    这是一些更传统的技术,即自动化用户界面测试,进入这个阶段。

     

    l  增加JavaScript钩进页面,返回元素(JSNI就是这个方式)

    l  使用Selenium来作用户界面测试(使用钩子和模拟存储服务)。所有我们要做的是检查是否

    ¡  元素存在

    ¡  所有按钮(需要被点击的)是可点击的

    ¡  滚动栏当被需要时能被加上

     

    这里我们不需要再多的工作-GwtTestcase帮助我们决定“模型”和“控制”正常工作。所有我们需要作的是“视图”。

     

    过去用Selenium测试通常有的一个问题是人们过分依赖XPath查询来获取网页元素。当然,当DOM改变它导致许多测试坏掉。一个解决办法是引入JavaScript钩子。它们仅当该应用程序运行带有特定“测试”标识才被加上,然后它们直接返回需要的元素。

     

    你可能在想那种方法更好?就这一点,我们能较早捕捉问题,并修补它们甚至不需要查看使用它们的测试。小而快的JsUnit测试能被用作决定是否钩子坏掉。如是,只要一行代码就能修补这问题。

     

    现在,我们又知道了

    ¡  事物是否正确渲染

    ¡  DOM是正确的

     

    测试存储服务(RPC实现)

    存储服务(RPC实现)中的方法需要许多好的单元测试。如果我们编写一些好的单元测试,我们可能已经有了模拟办公室管理(RPC后端),这样我们能用作我们以后的测试工作。

     

    我们能加在这里的主要价值是验证(1)存储服务中的每个接口方法表现正常,即使在同RPC后端交流出错(2)每个方法表现如预期。通过使用模拟办公室管理作为RPC后端,我们不不需要担心建立数据(并且注入错误简单了!)

     

    此外测试基本功能,例如

    l  我们期待获得的是所有的纪录

    l  没有传递给调用者的纪录不被获得

    l  应用程序正常工作吗,即使没有纪录被找到

     

    我们现在也能看

    l  格式不正确或不希望的数据

    l  太多数据

    l  空依赖

    l  异常

    l  超时

    l  并发问题

     

    如何用模拟替代我们实际的RPC后端?这不难,因为使用RPC机制已经强迫我们给服务端定义接口。所有我们需要做的是实现模拟RPC后端并替代运行。你可能想要考虑运行模拟RPC后端在同测试相同的机器上,使你的测试运行的更快。

     

    在这级的一些示例测试用例是:

    l  取得所有办公室列表让模拟RPC后端

    ¡  不返回办公室

    ¡  返回100个办公室,1个编码不正确

    ¡  返回100个办公室,1个空

    ¡ 

    ¡  抛出一个异常

    ¡  超时

    l  取得一个办公室的产品/货物让模拟RPC后端健壮返回

    ¡ 

    l  返回一个办公室的一个产品让模拟RPC后端,并

    ¡  为同样的产品在同时生成第二个查询(并使得更有趣,处理模拟能返回的结果!)

    l 

     

    现在我们知道:

    l  隔离用户界面如预期运行

    l  存储服务(RPC实现)适当地调用了RPC后端服务

    l  存储服务(RPC实现)适当地处理了任何错误条件

    l  一点并发下的应用程序行为

     

    我们不知道是否

    l  RPC后端服务实际期望行为如存储服务实现所想。

    容易看到我们对办公室管理(RPC后端)执行同样的操作并可能使用模拟Bigtable实现。毕竟我们应该知道:

    l  后端正确从Bigtable读取

    l  后端商务逻辑工作正常

    l  后端知道如何处理错误条件

    l  后端知道如何处理丢失数据

     

    我们不知道是否

    l  后端被正确使用,如它被设计被使用的方式

     

    测试办公室管理(RPC后端)和存储服务(RPC实现)

    现在让我们验证办公室管理(RPC后端)和存储服务(RPC实现)间的交互。这是一项重要任务,并不真那么难。以下几点将使测试快而简单:

    l  易测(通过Java API)

    l  易懂

    l  合理包括所有商务逻辑

    l  早期可用

    l  快速执行(这里模拟Bigtable是一个选项)

    l  维护负担低(因有稳定接口)

    l  可能的单独为存储服务(RPC实现)的测试子集

     

    我们现在知道:

    l  隔离用户界面如预期工作

    l  办公室管理(RPC后端)和存储服务(RPC实现)一起如预期工作

     

    我们不知道是否

    l  结果正如用户所作的

     

    最后系统测试!

    现在我们需要把所有组件插在一起并做“大”系统测试。在我们的示例中,典型的建立将是:

    l  操控“实际”Bigtable并产生“好”数据来测试

    ¡  5个办公室,每个有5个产品而且每个有5个货物

    l  使用Selenium(带有钩子)

    ¡  通过导航板导航

    ¡  执行一项

    ¡  增加一项

    ¡ 

     

    我们现在知道所有被插在一起的组件能处理一个典型用例。我们将为每个功能重复这个测试以使我们能通过用户界面调用。

     

    最大的便利,然而,是我们仅需要查找交互问题在所有这3块间。我们不需要验证边界例子,注入网络错误,或其它事情(因为我们已经在早期验证过了!)

     

    结论

    我们的方法需要我们

    l  理解系统

    l  理解平台

    l  理解什么会出错(还有哪里)

    l  我们的测试开展早

    l  投资基础设置以运行我们的测试(模拟,欺骗,…)

     

    我们得到的回报是

    l  更快的测试执行

    l  更少的测试维护

    ¡  分享的所有权

    ¡  早期执行>早期破裂>易于修复

    l  更短的反馈循环

    l  更早的调试/更好的bug本地化缘于更少假负面。

  • 原作者:Markus Clermont, John Thomas

    原文第一部分第二部分

     

    背景

    通常我们解决测试AJAX应用程序通过过多的大的端到端测试,并且(很有希望地)高单元测试覆盖。这里,我们概要了用这种方法的主要问题并演示一个有效的测试策略为一个基于GWT应用程序的示例,它超过“测试仅通过GUI。”

     

    GUI测试的问题

    一般通过GUI测试:

    l  是昂贵的(需要长时间写测试而且执行是资源密集型)

    l  有限地深入系统

    l  通常仅考虑“恰当路径”

    l  把多个方面化合成单个测试

    l  速度慢且脆弱

    l  需要大量的维护

    l  很难调试

     

    而在单元测试中不面临许多这些问题,它们本身不够主要是因为它们:

    l  较少深入组件间如何相互交互

    l  不提供业务逻辑和系统功能满足需求的信心

     

    解决办法

    虽然没有“一个通适”解决方案,有一些基本原则我们可以使用,以解决web应用程序的测试问题:

    l  投资在集成测试(找出最小的子系统)

    l  关注分离(不做安排通过你正在测试的接口)

    l  单独地测试每个接口(模拟出所有你不在测试的内容)

    l  考虑在生产中的依赖 (找出如何依赖能失败,并测试它)

    l  混合使用策略和工具。没有银弹。

    l  还有不要。。。你不能扔弃你所有的端到端测试

     

    好的测试食谱

    使用上述原则我们可以建立用于测试 Web 应用程序一个食谱。

    1.     探索系统的功能

    2.     确定系统的体系结构

    3.     确定组件间的接口

    4.     确定的依存关系和故障状态

    5.     对每个功能:

    l  确定参加组件

    l  找出潜在的问题

    l  隔离测试问题

    l  创造一个“恰当路径”测试

    后注:测试的价值

    常见由开发人员编写测试时问道的问题,“这真正值得我的时间?”简短的回答是“总是!”。由于修复一个bug比首先预防它更为昂贵,编写好的测试始终值得耗费时间。

     

    虽然有许多不同分类的测试,最常见分类它们的方式是基于它们的大小和它们测试产品的面。每个测试回答有关产品的特定的问题:

    l  单元测试:该方法履行了其制约?

    l  小集成测试:两个类能互相交互?

    l  中等集成测试:类能正确交互它的依赖?是否正确预见和处理错误?所需的函数是否公开APIGUI 上?

    l  子系统测试:两个子系统能互相交互?是否其中一个预期其它所有的错误并相应地处理它们?

    l  系统测试:是否整个系统如预期表现?

     

    记住这个问题,各种级别的测试使我们可以编写更集中和有意义的测试。请记住有效的测试是指那些提供快速和有用的反馈,即快速识别问题和定位该问题的确切位置。

     

    测试应用程序

    我们要测试的示例应用程序是一个简单的库存管理系统,它允许用户增加或减少在不同的存储地点的部分数。该应用程序使用GWT构建但这里描述的测试方法能使用到任何AJAX应用程序。

     

    让我们看看详细的每个步骤:

    1.    探索系统的功能

    听起来简单,它是测试应用程序一个关键的第一步。在你能开始写测试用例前,你需要从用户角度看系统如何运行。开启应用程序,浏览,点击按钮和链接并获取该应用程序的“感觉”。这里我们的示例应用程序如下所示:

    应用程序有一个导航板过滤清单的地点,列出每个位置中的项目数,增加/减少平衡的项目并依据办公室和产品排序清单。

     

    2.    确定系统的体系结构

    有关系统体系结构的学习是下一个关键的步骤。这一点上认为,该系统作为一组组件和找出他们如何互相交谈。设计文档和体系结构图有助此步骤。 在我们的示例中我们有下组成部分:

    l  GWT客户端:Java代码编译成驻存在用户浏览器的JavaScript。与服务端通过HTTP-RPC进行通信

    l  Servlet:标准Apache Tomcat servlet服务"frontend.html"(主页面)用注入的JavaScript,并服务RPC以同客户端JavaScript进行通信

    l  服务器端实现的RPC桩:Servlet调度RPC通过HTTP调用来实现它。RPC实现通过协议缓冲区同RPC后端进行通信

    l  RPC后端:处理业务逻辑和数据存储。

    l  Bigtable:用于存储数据

     

    这有助于绘制一个简单的关系图,表示这些组件之间的数据流,如果不存在:

    在我们的示例应用程序中,RPC实现称为“存储服务”,而其它RPC后端称为"办公室后端"

     

    3.    确定组件间的接口

    一些明显的是:

    l  gwt_module指向Ant构建文件

    l  “服务”Apache Tomcatservlet

    l  RPC接口的定义

    l  协议缓冲区

    l  Bigtable

    l  用户界面(毕竟它是一个接口!)

     

    4.    确定的依存关系和故障状态

    随着接口被正确识别,我们需要确定依赖项并支出被需要模拟系统中错误条件的输入值。

     

    我们的案例中,用户界面跟servlet交互反过来servlet又跟存储服务(RPC实现)交互。我们应验证在存储服务时会发生什么:

    l  返回空

    l  返回空列表

    l  返回大列表

    l  返回列表格式不正确的内容 (错误编码,空或长字符串)

    l  超时

    l  获取两个并发调用

     

    除了RPC实现(存储服务)RPC后端(办公室管理)交互,再次我们要确保正确的调用被引发和在后端时会发生什么:

    l  返回格式不正确的内容

    l  超时

    l  发送两个并发请求

    l  抛出异常

     

    为了实现这些目标,我们将要用模拟(原文:mock)替代RPC实现(存储服务),这样我们能控制,并使servlet同模拟交互。对办公室管理是同样的-我们将要用一个更可控的欺骗替代实际的RPC后端,并使存储服务同该模拟交互。

     

    为了更好地概述,我们会首先看个别用例并看组件间如何互动。示例将是用户界面上的过滤功能(只有那些在导航板中在勾选位置被勾选的项将被显示在表中)

  • 原文链接

     

    考虑下列函数,它修改一个客户端对象:

    bool SomeCollection::GetObjects(vector* objects) const {
      objects->clear();
      typedef vector::const_iterator Iterator;
      for (Iterator i = collection_.begin();
            i != collection_.end();
            ++i) {
        if ((*i)->IsFubarred()) return false;
        objects->push_back(*i);
      }
      return true;
    }

    考虑当GetObjects()被调用。如果调用者不检查该返回值,并设想该数据是一个有效状态但实际不是的?如果调用者检查该返回值,它该怎样设想它的对象状态在用例失败情况下?当GetObjects()失败,这将更好如果所有或者没有一个对象被收集。这能帮助避免引入很难找到的错误。

     

    通过使用良好的设计合约和扎实的实施,很合理地容易使函数GetObjects()行为像事物。通过遵从Sutter的规则即修改外部的可视性状态仅当在完成所有可能失败的操作后[1],并混合Meyers的“swap把戏”[2],我们从未定义行为移向Abrahams定义的强保证[3]

    bool SomeCollection::GetObjects(vector* objects) const {
      vector known_good_objects;
      typedef vector::const_iterator Iterator;
      for (Iterator i = collection_.begin();
            i != collection_.end();
            ++i) {
        if ((*i)->IsFubarred()) return false;
        known_good_objects->push_back(*i);
      }
      objects->swap(known_good_objects);
      return true;
    }

     

    以一个临时和指针交换的代价,我们强化了像那样的我们接口的连系,最好,调用者接受一个完整的有效对象的新集合;最坏,调用者的对象状态保持不变。调用者可能无法验证此返回值,但不会面临为定义的结果。这允许我们更加清晰地判断程序状态,使它更加容易核实自动化测试的输出意图并在回归测试再创建,精确定位并驱逐bugs

     

    1.     http://www.gotw.ca/publications

    2.     Scott Meyers, Effective C++条款25swap成员函数不抛出异常

    3.     http://www.boost.org/more/generic_exception_safety.html

  • 原作者:Karl Seguin

    原文链接

     

    尝试因为它们可能,现代编程语言并不能完全抽象计算机系统的基本方面。这被证明由高级语言抛出的不同异常。例如,放心地假设你可能已经面临以下.NET异常:NullReferneceException, OutOfMemoryException, StackOverflowExceptionhreadAbortException。对开发人员来说它同利用各种高级模式和技术同等重要,它同等重要以理解你的程序运行的生态系统。回顾C#(或VB.NET)编译器所提供的层,CLR和操作系统,我们发现内存。所有程序广泛的使用系统内存并以了不起的方式与之进行交互,很难成为一个好的程序员如果不了解此基础的交互。

     

    有关内存的混淆大部分来自于事实即C#VB.NET是托管的语言并且CLR提供自动垃圾回收。这导致许多开发人员可以错误地认为他们需要不操心内存。

     

    内存分配

    .NET,像大多数语言,你定义每个变量也存储在栈上或堆中。这是系统内存中的两个单独的空间分配,它提供一个清楚还互补的目的。变量存到哪被预定:值类型到栈上,而所有引用类型到堆上。换句话说,所有系统类型,例如char,int,long,byte,enum和任何结构体(或者在.NET中定义或由你定义)到栈上。此规则唯一的例外是属于引用类型的值类型 - 例如一个用户类的Id属性到堆上同该用户类自身实例一起。

     

    尽管我们已经习惯神奇的垃圾回收,栈上的值自动进行管理甚至在没有垃圾回收的环境中(如 C) 。这是因为当你进入一个新的作用域(如一种方法或一个if语句),值被推到栈上而且当你退出该栈时此值被弹出。这就是为什么栈是LIFO - 后进先出同义词的原因。你可以想象它这种方式:你可以想象它这种方式:当你创建一个新的作用域,如一种方法,一个标记被放在栈上并且值根据需要被添加到它。当你离开该作用域,所有的值被弹出并包括该方法标记。这适用于任何级别的嵌套。

     

    直到我们看堆栈之间交互为止,唯一可行惹上栈的麻烦的方法是StackOverflowException。这意味着你已经使用掉了所有在栈上的可用空间。99.9%的时间,这表明一个无限递归调用(一个函数调用它自身并永远)。在理论上,它可能由一个非常非常不合理设计的系统导致,虽然我从来没有见过一个非递归调用使用掉栈上所有空间。

     

    堆上的内存非配是没有栈上的那么简单。大多数基于堆的内存分配发生在当我们创建一个新对象时。编译器指出多少内存我们将需要(这并不难,即使对于嵌套引用的对象),瓜分一块合适的大块内存并返回一个指向所分配内存的指针(转瞬间更多的在它上)。最简单的例子是一个字符串,如果每个字符在一个字符串中占用2个字节,而且我们创建一串新字符,它的值是“Hello World”,然后CLR将需要分配22字节(11x2),加上无论什么总开销被需要。

     

    说及字符串,你无疑听到字符串是不可变的 - 即一旦你已经声明一串字符并赋予它值,如果你修改该字符串(通过改变其值,或连接另一个字符串放到它上),则一串新字符被创建。这实际上可能有负面性能影响,因此通常建议是使用一个StringBuilder对任何大字符串操作。事实尽管是存储在堆上的任何对象是不可变的,而且任何对基本尺寸的改变将需要新的分配。StringBuilder同若干集合,部分地解决此问题靠使用内部缓存。一旦该缓冲区填满,同样的重新分配发生,并且若干类型增长算法被用来去顶新的大小(最简单是旧大小 * 2)。只要有可能这是一个好主意,指定该对象初始容量以避免这种类型的重新分配(StringBuilderArrayList(在许多其它集合间)的构造函数允许你指定一个初始容量)。

     

    垃圾回收在堆是一项重要任务。不像栈,它上一个作用域能简单地弹出,对于某给定作用域堆中的对象不在本地。相反,大部分是深嵌套的其它引用对象的引用。在语言如C,每当程序员引起堆上的内存分配,他或她还必须确保将它移出当他结束它时。在托管语言,运行时负责清理资源(.NET使用世代垃圾收集,这在维基百科被简单描述)。

     

    有很多的危险的问题,当用堆时能使开发人员苦恼。内存泄漏不仅可能还很常见,内存碎片可能导致所有类型的破坏,以及各种性能问题都可以出现,缘于奇怪的分配行为或与非托管代码的交互(这.NET在下面做了许多)。

     

    指针

    对许多开发人员来说,在校时学习指针是一项痛苦的经历。它们代表代码和硬件间存在的非常真实的间接。更多开发从未学过它们 - 直接从一种没有直接暴露它们的语言跳进编程。事实虽然是任何人声称像JavaC#是无指针语言是完全错误的。由于指针是种机制,靠它所有语言管理堆上的值,看上去很愚蠢不了解它们如何被使用。

     

    指针代表系统的内存模型的联系 - 即指针是项机制,靠它栈和堆协同工作以提供你的程序所需的内存子系统。如前所叙,每当实例化一个新对象,.NET分配堆上的内存块并返回此内存块开始的一个指针。这就是指针是:包含一个对象的内存块的起始地址。此地址仅是一个唯一的数,通常以十六进制格式表示。因此指针只是一个唯一的数,它告诉.NET实际对象在内存的位置。当你指派一个引用类型给一个变量时,你的变量实际是一个指向该对象的指针。在Java.NET中这种间接是透明的,但不在CC++,这里你能通过指针算法直接操控内存地址。在CC++中,你能用一个指针并把它加1,任意更改它指向的位置(并可能使你的程序崩溃,因为这)。

     

    其中感兴趣的是指针实际存储的位置。它们实际上遵循相同的规则上文所述:作为整数它们被存储在栈上 - 除非当然它们是一个引用对象的一部分于是它们与其它它们的对象在堆上。这可能还不清楚,但这意味着最终所有的堆对象被根植在栈上(可能通过大量级的引用)。让我们首先看一个简单的例子。

    static void Main(string[] args)

    {

      int x = 5;

      string y = "codebetter.com";

    }

     

    从上面的代码,我们将结束栈上的2个值,整数5和指向我们字符串的指针,和在堆上的实际字符串。下面是图形表示:

     

    当我们退出我们的main函数(忘记该程序将停止这一事实),我们的栈弹出所有的本地值,这意味着xy的值都将丢失。这是显然的因为堆上分配的内存仍包含我们的字符串,但我们已经丢失了所有它的引用(没有指针回指向它)。在CC++中,这将导致内存泄漏 - 没有一个我们堆地址的引用,我们将无法释放内存。在C#Java中,我们可靠的垃圾回收将检测未引用对象并释放它。

     

    我们来看一个更复杂的示例,但除了有更多的箭头,它基本上是相同的。

    public class Employee

    {

      private int _employeeId;

      private Employee _manager;

      public int EmployeeId

      {

        get { return _employeeId; }

        set { _employeeId = value; }

      }

      public Employee Manager

      {

        get { return _manager; }

        set { _manager = value; }

      }

      public Employee(int employeeId)

      {

        _employeeId = employeeId;

      }

    }

    public class Test

    {

      private Employee _subordinate;

      void DoSomething(

      {

        Employee boss = new Employee(1);

        _subordinate = new Employee(2);

        _subordinate.Manager = _boss;

      }

    }

     

    有趣的是,当我们离开我们的方法,boss变量将弹出堆栈,但subordinate它被定义在父作用域,不会。这意味着垃圾回收将不会清除因为这两个堆值仍将被引用(一个直接从栈,另一个间接从栈通过一个引用的对象)。

     

    正如你所看到,指针相当确定扮演一个重要角色在C#VB.NET中。由于指针算法不可用在任何语言中,指针被极大地简化并希望很容易理解。

     

    实践中的内存模型

    现在,我们将查看这对我们应用程序的实际影响。请记住虽然理解脚本中的内存模型不仅会帮助你避免陷阱,而且它还将帮助你编写更好的应用程序。

     

    装箱

    装箱发生在一个值类型(存储在栈上)被强制到堆上。拆箱发生在这些值类型被放回到栈上。强制一个值类型最简单的方法,例如一个整数,到堆上是通过强制转换它:

    int x = 5;

    object y = x;

     

    一个更常见的装箱情景发生在当你提供一个值类型给一个方法,它接受一个对象。这是常见集合在.NET 1.x里在范型介入前。非范型集合类大多使用对象类型,所以下面的代码导致装箱和拆箱:

    ArrayList userIds = new ArrayList(2);

    userIds.Add(1);

    userIds.Add(2);

    int firstId = (int)userIds[0];

     

    泛型的真正优点是增加的类型安全,但它们还解决装箱有关的性能下降。在大多数情况下你不会注意到此损失,但在某些情况下,例如大集合,你可以很清楚。无路是否是你该实际关注的是你自己,装箱是一个首要的例子,关于背后的内存系统如何能影响你的应用程序。

     

    ByRef

    对指针没有一个好的理解,几乎不可能理解靠引用和靠值传值。开发人员一般理解传递一个值类型的实现,例如一个整型,靠引用,但较少明白为何你要通过引用传递引用。ByRefByVal同样影响引用和值类型 - 假如你明白它们通常运行靠的内在值(即在次引用类型示例意味着它们运行靠指针而不是值)。使用ByRef是唯一普遍情况,即.NET不会自动解决指针间接(靠引用传递或作为一个输出参数在Java中不被允许)。

     

    首先我们将看ByVal/ByRef如何影响值类型。给出下列代码:

    public static void Main()

    {

      int counter1 = 0;

      SeedCounter(counter1);

      Console.WriteLine(counter1);

     

      int counter2 = 0;

      SeedCounter(ref counter2);

      Console.WriteLine(counter2);

    }

    private static void SeedCounter(int counter)

    {

      counter = 1;

    }

    private static void SeedCounter(ref int counter)

    {

      counter = 1;

    }

    我们可以期望输出0接着1 。第一次调用没有引用传递counter1,这意味着counter1的一份拷贝被传进SeedCounter并且更改限制在本地函数。换言之,我们采用栈上的值并复制它到另一个栈位置。

     

    在第二种情况下,我们实际通过引用传值,这意味着没有拷贝被创建和修改没限制在SeedCounter函数本地。

     

    行为和引用类型完全相同,尽管在第一个它可能不这么显示。我们来看两个示例。第一个使用一个 PayManagement类来更改一个Employee属性。在以下代码中我们看到我们有两个员工,在这两种情况下我们正在给他们一个 $2000增加。唯一的区别是一个传递该雇员通过引用而其它由值传递。你能猜到输出吗?

    public class Employee

    {

      private int _salary;

      public int Salary

      {

        get {return _salary;}

        set {_salary = value;}

      }

      public Employee(int startingSalary)

      {

        _salary = startingSalary;

      }

    }

    public class PayManagement

    {

      public static void GiveRaise(Employee employee, int raise)

      {

        employee.Salary += raise;

      }

      public static void GiveRaise(ref Employee employee, int raise)

      {

        employee.Salary += raise;

      }

    }

    public static void Main()

    {

      Employee employee1 = new Employee(10000);

      PayManagement.GiveRaise(employee1, 2000);

      Console.WriteLine(employee1.Salary);

     

      Employee employee2 = new Employee(10000);

      PayManagement.GiveRaise(ref employee2, 2000);

      Console.WriteLine(employee2.Salary);

    }

    在这两种情况下,输出是12000。乍看来,这似乎不同于我们刚看到带值类型。正在发生的是传递一个引用类型靠值确实传递一份该值拷贝,但不是堆值。相反,我们正在传递一份我们指针的拷贝。而且由于一个指针和该指针的一份拷贝指向堆上同样的内存,其中一个的改变会被反映给其它。

     

    通过引用传递引用类型时, 你在传递实际指针而不是该指针的一份拷贝。这回避了问题,何时我们通过引用传递引用类型?通过引用传递的唯一原因是当你要修改指针自身 - 在它指向指出。这实际上能导致危险的负面影响 - 这正是的一个好函数为此,必须专门指定它们希望通过引用传递的参数。让我们来看一下我们的第二个示例。

    public class Employee

    {

      private int _salary;

      public int Salary

      {

        get {return _salary;}

        set {_salary = value;}

      }

      public Employee(int startingSalary)

      {

        _salary = startingSalary;

      }

    }

    public class PayManagement

    {

      public static void Terminate(Employee employee)

      {

        employee = null;

      }

      public static void Terminate(ref Employee employee)

      {

        employee = null;

      }

    }

    public static void Main()

    {

      Employee employee1 = new Employee(10000);

      PayManagement.Terminate(employee1);

      Console.WriteLine(employee1.Salary);

     

      Employee employee2 = new Employee(10000);

      PayManagement.Terminate(ref employee2);

      Console.WriteLine(employee2.Salary);

    }

    尝试找出什么将会发生并且为什么。我将给你一个提示:一个异常将会被抛出。如果你猜测对employee1.Salary的调用输出10000当第二个抛出一个NullReferenceException,然后你就是正确的。在第一种情况下我们只将一份原始指针的拷贝设置为null - employee1正指向的它没有任何影响。在第二种情况下,我们不传递一个拷贝而是employee2使用的同样的栈值。因此设置employeenull等同于写employee2 = null;

     

    这相当少见,想要变更地址指向由一个从一个单独的方法内的变量 - 这正是唯一一次你可能会看到按值传递一个引用类型当你想要从一个函数调用返回多重值(在这种情况下最好使用一个输出参数,或使用纯面向对象方法)。上面的示例真正高亮在一种规则没被完全理解的环境玩的危险。 

     

    托管内存泄漏

    我们已经看到了一个例子,什么样的内存泄漏看起来像C中的。基本上,如果C#没有一个垃圾回收,下面的代码会泄漏:

    private void DoSomething()

    {

      string name = "dune";

    }

    我们的栈值(一个指针)将被弹出,并且使用它我们唯一的方法将不得不引用创建存放我们字符串的内存。留给我们毫无释放它的方法。这不是在.NET中的问题,因为它有一个垃圾回收以跟踪未引用内存并释放它。然而,一种内存泄漏仍是可能如果你无限期地持有引用。这在大型具有深度嵌套引用的应用程序中常见。它们很难识别因为泄漏可能非常少并且你的应用程序运行不够长 - 即使ASP.NET习惯频繁地被回收。

     

    最终当你的程序终止该操作时,系统将回收所有内存,泄漏或其他。但是,如果你开始看到OutOfMemoryException并不处理异常大的数据,还有一个好的机会就是你有一个内存泄漏.NET附带工具来帮你找出,但是你可能需要利用一个商业内存探测器如dotTraceANTS Profiler。当找寻内存泄漏你将查看你的泄漏对象(这很容易查找通过获取2个你的内存快照并比较它们),追踪通过仍保持对它引用的所有对象并更正该问题。

     

    碎片

    outofmemoryexception另一个常见原因为内存碎片。在堆上分配内存时通常是一块连续块。这意味着可用内存必须扫描一个大足够的块。作为你的程序运行过程中,此堆变得越来越零碎(就像你的硬盘驱动)而且你可能结束有足够的空间,但分散在各处并不能用。在正常情况下,垃圾回收将压缩堆当它在释放内存时。因为它压缩了内存,对象地址改变,而且.NET确保相应更新所有你的引用。尽管有时.NET不能移动一个物体:即当该对象被钉扣在某一特定内存地址。

     

    钉扣

    钉扣发生在一个对象被锁定到堆上某一特定地址。钉扣的内存不能被垃圾回收压缩导致碎片。为何值被钉扣?最常见的原因是你的代码正在跟非托管代码交互。当.NET垃圾回收压缩此堆时,它更新托管代码中的所有引用,但它没办法跳转到非托管代码同样进行操作。因此,在内部钉扣它之前必须首先钉扣内存中的对象。由于.NET框架中的许多方法依赖托管代码,钉扣能发生而你无须了解它(我最熟悉场景是.NET套接字类依赖非托管实现并固定缓冲)。

     

    围绕这个钉扣类型一个常见的方式是声明大对象,它不引发尽多的许多小对象碎片(考虑被置于某个特殊堆的大对象(叫大对象堆(LOH)它根本不被压缩))。比如关于钉扣更多信息,我建议你阅读Greg Young高级贴关于钉扣和异步套接字。

     

    有第二个原因为何一个对象可能被钉扣 - 当你显示进行它就发生。在C#(不在VB.NET)如果你编译你的程序集使用不安全选项你能钉扣一个对象通过固定语句。而广泛的钉扣能引发系统上的内存压力,固定语句的公正使用能极大提升性能。为什么?因为一个钉扣对象能用指针算法直接操控 - 如果此对象不被钉扣这不可能因为垃圾回收可能重分配你的对象在内存的其它处。

     

    举例这个有效的ASCII字符串到整形转换,它运行比使用int.Parse6倍。

    public unsafe static int Parse(string stringToConvert)

    {

      int value = 0;

      int length = stringToConvert.Length;

      fixed(char* characters = stringToConvert)

      {

        for (int i = 0; i &lgt; length; ++i)

        {

          value = 10 * value + (characters[ i ] - 48);

       

        }

      }

      return value;

    }

    除非您在做一些异常,永远不会应有标记为你的程序集不安全并充分利用固定语句的需要。上面的代码将轻松地崩溃(传递null作为字符串并看发生什么),不如int.Parse特性丰富,并在规模内容是非常危险的同时没提供好处。

     

    设置内容为null

    因此,你要设置你的引用类型为null当你完成时?当然不。一旦一个变量超出了作用域,它被弹出栈并且该引用被移除。如果你不能等到退出作用域,你可能需要重构你的代码。

     

    结论

    首先栈,堆和指针似乎可以势不可挡的。尽管在托管语言上下文,没有真正多少跟它有关。了解这些概念的优点是具体的在日常的编程中,并且是无价的当未预料行为发生。你可以是引发怪异的NullReferenceExceptionsOutOfMemoryExceptions的程序员,或者修复它们的那位。