.NET内存性能分析指南InCerry

知道什么时候该担心,以及在需要担心的时候该怎么做

译者水平有限,如果错漏欢迎批评指正

本文旨在帮助.NET开发者,如何思考内存性能分析,并在需要时找到正确的方法来进行这种分析。在本文档中.NET的包括.NETFramework和.NETCore。为了在垃圾收集器和框架的其他部分获得最新的内存改进,我强烈建议你使用.NETCore,如果你还没有的话,因为那是应该尽快去升级的地方。

这是一份正在完善的文档。现在,这份文档主要是在Windows上。添加相应的Linux材料肯定会使它更有用。我正计划在未来这样做,但也非常欢迎其他朋友(尤其是对Linux部分)对该文件的贡献。

这是一份很长的文档,但你不需要读完它;你也不需要按顺序阅读各部分。根据你在做性能分析方面的经验,有些章节可以完全跳过。

如果你对性能分析工作完全陌生,我建议从头开始。

注意!

当我在写这篇文档时,我打算根据分析的需要来介绍一些概念,如并发的GC或钉住。所以在你阅读的过程中,你会逐渐接触到它们。如果你已经知道它们是什么,并且正在寻找关于特定概念的解释,这里有它们的链接-

那些在做性能分析方面有经验的人知道,这可能就像侦探工作一样--没有"如果你按照这10个步骤去做,你就会改善性能或从根本上解决性能问题"的方法。这是为什么呢?因为在运行的东西不仅仅是你的代码-还有你使用操作系统、运行时、库(至少是BCL,但通常是许多其他的库)。而运行你的代码的线程需要与同一进程中的其他线程和/或其他进程共享机器/VM/容器。

这篇文档谈到了你自己可以做什么,以及什么时候是把分析工作交给GC团队的好时机,因为这将是需要在运行时进行的改进。很明显,我们在GC中仍然在做改进的工作(否则我就不会还在这个团队中)。正如我们将看到的,GC的行为是由你的应用行为驱动的,所以你肯定可以改变你的应用行为来影响GC。在你作为性能工程师需要做多少工作和GC自动处理多少工作之间存在着一个平衡。.NETGC的理念是,我们尽量自动处理;当我们需要你的参与时(通过配置),我们会通过有意义的方式要求你的协助,这种方式是从应用程序的角度,并不要求你了解GC的全部细节。当然,GC团队一直在努力让.NETGC处理越来越多的性能场景,这样用户就不需要担心了。但如果你遇到了GC目前不能很好处理的情况,我将指出你可以做什么来解决它。

我对性能分析的目标是使客户需要做的大部分分析自动化。我们在这方面已经走了很长的路,但我们还没有达到所有分析都自动化的程度。在这份文件中,我将告诉你目前做分析的方法,在文件的最后,我将给你一个展望,说明我们正在为实现这个目标做了什么样的改进。

我们都有有限的资源,如何将这些资源花在能够产生最大回报的事情上是关键。这意味着你应该找到哪个部分是最值得优化的,以及如何以最有效的方式优化它们。当你断定你需要优化某些东西,或者你要如何优化某些东西时,应该有一个合理的理由来说明你为什么这样做。

当人们第一次来找我时,我总是问他们这样一个问题-你的性能目标是什么?不同的产品有非常不同的性能要求。在你想出一个数字之前(例如,将某些东西提高X%),你需要知道你要优化的是什么。在最高层次的角度来看,这些是需要优化的方面-

优化内存占用,例如,需要在同一台机器上尽可能多地运行实例。

针对尾部延迟进行优化,例如,需要满足一定的延迟SLA。

GC行为的改变可能是由于GC本身的变化或框架其他部分的变化,当你使用一个新版本时,框架中通常会有很多改动。当你在升级后看到内存行为的变化时,可能是由于GC的变化或框架中的其他东西开始分配更多的内存,并以不同的方式保留内存。此外,如果你升级你的操作系统版本或在不同的虚拟化环境中运行你的产品,你也可以得到不同的行为,因为它们可能导致你的应用程序出现不同的行为。

测量是你在开始一个产品时绝对应该计划做的事情,而不是在事情发生后才想到的,特别是当你知道你的产品需要在相当高负载的情况下运行时。如果你正在阅读这份文件,那么你很有可能正在从事一些对性能有要求的工作。

对于我所接触的大多数工程师来说,测量并不是一个陌生的概念。然而,如何测量和测量什么是我见过的许多人需要帮助的事情。

这意味着你需要有一些方法来真实地测量你的性能。在复杂的服务器应用程序上的一个常见问题是,你很难在你的测试环境中模拟你在生产中实际看到的情况。

测量并不仅仅意味着"我可以测量我的应用程序每秒可以处理多少个请求,因为这是我所关心的",它意味着你也应该有一些东西,当你的测量结果告诉你某些东西没有达到理想的水平时,你可以做有意义的性能分析。能够发现问题是一方面。如果你没有任何东西可以帮助你找出这些问题的原因,那就没有什么帮助了。当然,这需要你知道如何收集数据,我们将在下面谈及。

能够衡量来自修复/解决方法的效果。

在您知道哪些因素可能对您关心的事情(即您的性能指标)影响最大之后,你应该测量它们的影响,这样你就可以观察它们在你开发产品的过程中贡献是大还是小。一个完美的例子是,服务器应用程序如何改善其P95请求延迟(即第95百分位的请求延迟)。这是一个几乎每个网络服务器都会看的性能指标。当然,很多因素都可以影响这个延迟,但你知道那些可能影响最大的因素。

网络IO只是另一个可能导致你的请求延迟的因素的例子。这里的方框宽度仅仅是为了说明问题。

如果这是10%,你应该有其他因素没有计算在内。

通常人们会猜测GC停顿是影响他们P95延迟的原因。当然这是有可能的,但这绝不是唯一可能的因素,也不是对你的P95影响最大的因素。这就是为什么了解影响很重要,它告诉你应该把大部分精力花在什么地方。

而影响你的P95的因素可能与影响你的P99或P99.99的因素非常不同;同样的原则也适用于其他百分位数。

虽然这个文档是为每一个关心内存分析的人准备的,但根据你所工作的层次,应该有不同的考虑。

作为一个从事终端产品的人,你有很大的自由空间去优化,因为你可以预测你的产品在什么样的环境下运行,例如,一般来说,你知道你倾向于哪种资源的饱和,CPU,内存或其他东西。你可以控制你的产品在什么样的机器/虚拟机上运行,你使用什么样的库,你如何使用它们。你可以做一些估计,比如"我们的机器上有128GB的内存,计划在我们最大的进程中拿出20GB的内存缓存"。

从事平台技术或库工作的人无法预测他们的代码将在什么样的环境中运行。这意味着:1)如果希望用户能够在性能关键路径上使用代码,则需要节约内存使用;2)你可能要提供不同的API,在性能和可用性之间做出权衡,并指导你的用户如何做。

正如我在上面提到的,让一个人对整个技术栈有透彻的了解是完全不现实的。本节列出了任何需要从事内存性能分析工作的人都必须知道的基本知识。

我们通过VMM(虚拟内存管理器)使用内存,它为每个进程提供了自己的虚拟地址空间,尽管同一台机器上的所有进程都共享物理内存(如果你有页面文件的话)。如果你在一个虚拟机中运行,虚拟机就有一种在真实机器上运行的错觉。对于应用程序来说,实际上,你很少会直接使用虚拟内存工作。如果你写的是本地代码,通常你会通过一些本地分配器来使用虚拟地址空间,比如CRT堆,或者C++的new/delete关键字-这些分配器会代表你分配和释放虚拟内存;如果你写的是托管代码,GC是代表你分配/释放虚拟内存的人。

当机器上的进程使用的内存总量超过机器所拥有的内存时,一些页面将需要被写入页面文件(如果有的话,大多数情况下是这样的)。这是一个非常缓慢的操作,所以通常的做法是尽量避免进入分页。我在简化这个问题--实际的细节与这个讨论没有关系。当进程处于稳定状态时,通常你希望看到你正在使用的页面被保留在你的工作集中,这样我们就不需要支付任何成本来把它们带回来。在下一节中,我们将讨论GC是如何避免分页的。

我故意把这一节写得很短,因为GC才是需要代表你与虚拟内存互动的人,但了解一点基本情况有助于解释性能工具的结果。

在每个进程中,每个使用内存的组件都是相互共存的。在任何一个.NET进程中,总有一些非GC堆的内存使用,例如,在你的进程中总是有一些模块被加载,需要消耗内存。但可以说,对于大多数的.NET应用程序来说,这意味着GC堆占用大部分的内存。

GC是一个以进程为维度的组件(自从CLR诞生以来一直如此)。大多数GC的启发式方法都是基于每个进程的测量,但GC也知道机器上的全局物理内存负载。我们这样做是因为我们想避免陷入分页的情况。GC将一定的内存负载百分比识别为"高内存负载情况"。当内存负载百分比超过这个百分比时,GC就会进入一个更积极的模式,也就是说,如果它认为有成效的话,它会选择做更多的完全阻塞的GC,因为它想减少堆的大小。

对于在容器中运行的进程,GC会根据容器的限制来考虑物理内存。

到目前为止,我们用GC来指代组件。下面我将用GC来指代组件,或者指代一个或多个在堆上进行内存回收的集合行为,即GC或GCs。

由于大多数GC是由于分配而触发的,所以值得了解分配的成本。首先,当分配没有触发GC时,它是否有成本?答案是绝对的。有一些代码需要运行来提供分配--只要你必须运行代码来做一些事情,就会有成本。这只是一个多少的问题。

分配中开销最大的部分(没有触发GC)是内存清除。GC有一个契约,即它所有分配的内存会用零填充。我们这样做是为了安全、保障和可靠性的原因。

1)我们还可以测量GC的发生频率,这告诉我们发生了多少分配。毕竟,大多数GC是由于分配而被触发的。

2)对非常频繁发生的事情进行分析的方法之一是抽样。

3)当你有了CPU使用信息,你可以在GC方法名称查看内存清除的成本。实际上,通过GC方法名称来查找东西显然是非常内部且专业的,并受制于实现的变化。但由于本文档的目标是大众,包括有经验的性能工程师,我将提到几个具体的方法(其名称往往不会有太大的变化),作为进行性能测量的一种方式。

这听起来是一个简单的问题。通过测量,对吗?是的,但是当你测量GC堆的时候,就很重要了。

表格1

我们可以说,是的,有一个GC发生在第4秒,因为堆的大小比第3秒小。但我们再看看另一种可能性-

表格2

如果我们只有堆的大小数据,我们就不能说GC是否已经发生。

看完上一段,思考分配预算的一个简单方法是上一次GC退出时的堆大小和这次GC进入时的堆大小之间的差异。因此,分配预算是指在触发下一次GC之前,GC允许多少分配。在表1和表2中,分配预算是一样的-3GB。

当试图提高内存性能时,我看到人们经常做的一件事(或只做一件事)是减少分配。如果你真的可以在性能关键路径开始之前预先分配所有的东西,我说你更有更多的权利!但是,这有时是非常不实际的。例如,如果你使用的是库,你并不能完全控制它们的分配(当然,你可以尝试找到一种无分配的方式来调用API,但并不保证有这样的方式,而且它们的实现可能会改变)。

那么,减少分配是一件好事吗?是的,只要它确实会对你的应用程序的性能产生影响,并且不会使你的代码中的逻辑变得非常笨拙或复杂,从而使它成为一个值得的优化。减少分配实际上会降低性能吗?这完全取决于你是如何减少分配的。你是在消除分配还是用其他东西来代替它们?因为用其他东西代替分配可能不会减少GC所要做的工作。

根据设计,分代GC不会在每次触发GC时收集整个堆。他们尝试做年轻一代的GC,比老一代的GC更频繁。老一代的GC通常成本更高,因为它们收集的堆更多。

这也使得看堆变得更加复杂,因为如果你刚从一个老一代的GC中出来,特别是一个正在整理的GC,你的堆的大小显然比你在该GC被触发之前要小得多;但如果你看一下年轻一代的GC,它们可能正在被整理,但堆的大小差异没有那么大,这就是设计。

上面提到的分配预算概念实际上是每一代的,所以gen0、gen1和gen2都有自己的分配预算。用户的分配将发生在gen0,并消耗gen0的分配预算。当分配消耗了gen0的所有预算时,GC将被触发,gen0的幸存者将消耗gen1的分配预算。同样地,gen1的幸存者将消耗gen2的预算。

图1-经过不同代GC的对象

一个对象"死了"和它被清理掉之间的区别可能会让人困惑。我收到的一个常见问题是:"我不再保留我的对象了,而且我看到GC正在发生,为什么我的对象还在那里?"。请注意,一个对象不再被用户代码持有的事实(在本文中,用户代码包括框架/库代码,即不是GC代码)需要被GC扫描到。要记住的一个重要规则是:"如果一个对象在genX中,这意味着它只可能在genXGC发生时被回收",因为这时GC会真正去检查genX中的对象是否还活着。如果一个对象在gen2中,不管发生了多少次短暂的GC(即0代和1代GC),这个对象仍然会在那里,因为GC根本没有收集gen2。另一种思考方式是,一个对象所处的代数越高,GC需要收集的工作就越多。

现在是谈论大对象的好时机,也就是LOH(大对象堆)。到目前为止,我们已经提到了gen0、gen1和gen2,以及用户代码总是在gen0中分配对象。实际上,如果对象太大,这并不正确-它们会被分配到堆的另一个部分,即LOH。而gen0、gen1和gen2组成了SOH(小对象堆)。

在某种程度上,你可以认为LOH是一种阻止用户不小心分配大对象的方式,因为大对象比小对象更容易引入性能挑战。例如,当运行时默认发放一个对象时,它保证内存被清空。内存清空是一个昂贵的操作,如果我们需要清空更多的内存,它的成本会更高。也更难找到空间来容纳一个更大的对象。

LOH在内部是作为gen3被跟踪的,但在逻辑上它是gen2的一部分,这意味着LOH只在gen2的GC中被收集。这意味着,如果你代码经常会使用LOH,你就会经常触发gen2的GC,如果你的gen2也很大,这意味着GC将不得不做大量的工作来执行gen2的GC。

和其他gen一样,LOH也有它的分配预算,当它用完时,与gen0不同,gen2GC将被触发,因为LOH只在gen2GC期间被清理。

另一个常见问题是"我看到gen2有很多自由空间,为什么GC没有使用这些空间?"。

答案是,GC正在使用这个空间。我们必须再次回到何时测量堆的大小,但现在我们需要增加另一个维度-整理GCvs清扫GC。

.NETGC可以执行整理或清扫GC。整理是开销更大的操作,因为GC会移动对象(会发生内存复制),这意味着它必须更新堆上这些对象的所有引用,但整理可以大大减少堆的大小。清扫GC不进行压缩,而是将相邻的死对象凝聚成一个空闲对象,并将这些空闲对象穿到该代的空闲列表中。空闲列表占据的大小,我们称之为碎片,也是gen的一部分,因此在我们报告gen和堆的大小时也包括在内。虽然在这种情况下,堆的大小并没有什么变化,但重要的是要明白这个空闲列表是用来容纳年轻一代的幸存者的,所以我们要使用空闲空间。

这里我们将介绍GC的另一个概念-并发的GC与阻塞的GC。

现在我们再来思考一下"何时测量"的问题。当我们做一个BGC时,在该GC结束时,一个新的自由列表被建立起来。随着第一代GC的运行,他们将使用这个自由列表的一部分来容纳他们的幸存者,所以列表的大小将变得越来越小。因此,当你说"我看到gen2有很多空闲空间"时,如果那是在BGC刚刚发生的时候,或者刚刚发生不久的时候,那是正常的。如果到了我们做下一次BGC的时候,gen2中总是有很多空闲空间,这意味着我们做了那么多工作来建立一个空闲列表,但它并没有被使用多少,这就是一个真正的性能问题。我已经在一些场景中看到了这种情况,我们正在建立一个解决方案,使我们能够进行最佳的BGC触发。

我们一直在讨论如何正确地测量GC堆的大小,但是GC堆在内存中到底是什么样子的,也就是说,GC堆是如何物理组织的?

GC像其他Win32程序一样通过VirtualAlloc和VirtualFreeAPI来获取和释放虚拟内存(在Linux上通过mmap/munmap完成)。GC对虚拟内存进行的操作有以下几点

当GC堆被初始化时,它为SOH保留了一个初始段,为LOH保留了另一个初始段,并且只在每个段的开头提交几个页面来存储一些初始信息。

图.2-GC堆的段

随着GC的发生和内存回收,当段上没有发现活对象时,段就会被释放;段空间的末端(即段上最后一个活对象的末端,直到段的末端)被取消提交,除了短暂的段。

请注意,在.NET5中,取消提交的行为发生了变化,我们可以留下更多的内存,因为我们想把gen1也纳入GC的考虑。另外,服务器GC的取消提交现在是在GC暂停之外完成的,所以GC结束时报告的部分内容可能会被取消提交。这是一个实现细节--使用gen0的预算通常仍然是一个非常好的近似值,可以确定投入的部分是多少。

按照上面的例子,在gen2GC之后,堆可能看起来是这样的(注意这只是一个例子说明)。

图3-gen2GC后的GC堆段

在gen0的GC之后,由于它只能收集gen0的空间,我们可能会看到这个:

图4-gen0GC后的GC堆段

大多数时候,你不必关心GC堆被组织成段的事实,除了在32位上,因为虚拟地址空间很小(总共2-4GB),而且可能是碎片化的,甚至当你要求分配一个小对象时,你可能得到一个OOM,因为我们需要保留一个新的段。在64位平台上,也就是我们大多数客户现在使用的平台上,有大量的虚拟地址空间,所以预留空间不是一个问题。而且在64位平台上,段的大小要大得多。

几乎所有人都听说过或遇到过OOM异常。GC究竟什么时候会抛出一个OOM异常呢?在抛出OOM之前,GC确实非常努力。因为GC大多做短暂的GC,这意味着堆的大小往往不是最小的,这是设计上的。然而,GC通常会尝试一个完全阻塞的GC,并在抛出OOM之前验证它是否仍然不能满足分配请求。但也有一个例外,那就是GC有一个调整启发式,说它不会继续尝试完全阻塞的GC,如果它们不能有效地缩小堆的大小。它将尝试一些gen1GCs和完全阻塞的GCs混合在一起。所以你可能会看到一个OOM抛出,但抛出它的GC并不是一个完全阻塞的GC。

这就是我们引入的另一个概念,即GC的不同主要类型--工作站GCvs服务器GC(简称WKSGCvsSVRGC)。

由于2种工作负载的性质不同,SVRGC有2个明显不同的属性,而WKSGC则没有。

SVRGC线程的优先级被设置为"THREAD_PRIORITY_HIGHEST",这意味着如果其他线程的优先级较低,它就会抢占这些线程,而大多数线程都是如此。相比之下,WKSGC在触发GC的用户线程上运行GC工作,所以它的优先级是该线程运行的任何优先级,通常是正常的优先级。

SVRGC线程与逻辑核心硬性绑定。

如前所述,当gen0的分配预算用完时,就会触发GC。当一个GC被触发时,发生的第一步是我们决定这个GC将是哪一代。在工具那一章节,我们将看到哪些原因会导致GC从gen0升级到可能的gen1或gen2,但其中的一个主要因素是gen1和gen2的分配预算。如果我们检测到gen2的分配预算已经用完,我们就会把这个GC升级到完全的GC。

这如何转化为触发GC的频率是,如果一个代被频繁地使用(即,它的存活率很低),它将被更频繁地收集。这就解释了为什么我们最频繁地收集gen0,因为gen0是用于非常临时的对象,其存活率非常低。根据代际假说,对象要么活得很久,要么很临时,gen2持有长寿的对象,所以它们被收集的次数最少。

存活的对象数量通常决定了GC需要做多少工作;不存活的对象数量通常决定了GC被触发的频率

下面是一些极端的例子,当我们应用这一规则时-

你不能处于分配率和生存率都很高的情况下-你会很快耗尽内存。

从GC的角度来看,它被各种运行时组件告知哪些对象应该存活。它并不关心这些对象是什么类型;它只关心有多少内存可以存活,以及这些对象是否有引用,因为它需要通过这些引用来追踪那些也应该存活的子对象。我们一直在对GC本身进行改进,以改善GC暂停,但作为一个写托管代码的人,知道是什么让对象存活下来是一个重要的方法,你可以通过它来改善你这边的个别GC暂停。

我们已经谈到了分代GC的效果,所以第一条规则是当一个代没有被回收,这意味着该代的所有对象都是活的。

你最有可能听到的常见类型的根是指向对象的堆栈变量、GC句柄和终结器队列。我把这些称为用户根,因为它们来自用户代码。由于这些是用户代码可以直接影响的东西,所以我将详细地讨论它们。

堆栈变量

堆栈变量,特别是对于C#程序来说,实际上并没有被谈及很多。原因是JIT也能很好地意识到堆栈变量何时不再被使用。当一个方法完成后,堆栈根保证会消失。但即使在这之前,JIT也能知道什么时候不再需要一个堆栈变量,所以不会向GC报告,即使GC发生在一个方法的中间。请注意,在DEBUG构建中不是这种情况。

GC句柄

我在上面提到过几次钉住。大多数人都知道钉住是什么-它向GC表示一个对象不能被移动。但从GC的角度来看,钉住的意义是什么呢?由于GC不能移动这些被钉住的对象,它使被钉住的对象之前的死角变成了一个自由对象,这个自由对象可以用来容纳年轻一代的生存者。但这里有一个问题-正如我们从上面的代际讨论中看到的,如果我们简单地将这些被钉住的对象提升到老一代,就意味着这些自由空间也是老一代的一部分,要用它们来容纳年轻一代的幸存者,唯一的办法就是我们真的对年轻一代做一次GC(否则我们甚至没有"年轻一代的幸存者")。然而,如果我们能在gen0中保留这些自由空间,它们就可以被用户分配使用。这就是为什么GC有一个叫做降代的功能,我们将把这些被钉住的对象降代到gen0,这意味着它们之间的空闲空间将是gen0的一部分,当用户代码分配时,我们可以立即使用它们。

图5-降代(我从一张旧的幻灯片上取下来的,所以这看起来与之前的片段图片有些不同。)

由于gen0分配可以发生在这些自由空间中,这意味着它们将消耗gen0预算而不增加gen0的大小(除非自由空间不能满足所有的gen0预算,在这种情况下它将需要增长gen0)。

然而,GC不会无条件地降代,因为我们不想在gen0中留下许多固定对象,这意味着我们必须在每次GC中再次查看它们,可能会有很多次GC(因为它们是gen0的一部分,每当我们执行gen0GC我们需要查看它们)。这意味着如果您遇到严重的固定情况,它仍然会导致gen2中的碎片问题。同样,GC确实有机制来应对这些情况。但是如果你想对GC施加更少的压力,你可以从用户的POV中遵循这个规则—

早点钉住对象,分批钉住对象

分配

·如果你分配了一个可终结的对象(意味着它的类型有一个终结器),就在GC返回到VM端的分配助手之前,它将把这个对象的地址记录在终结队列中。

·有一个终结者意味着你不能再使用快速分配器进行分配,因为每个可终结的对象的分配都要到GC去注册。

回收

然而,如果你用GC.SuppressFinalize来抑制终结器,你告诉GC的是你不需要运行这个对象的终结器。所以GC就没有理由去提升(升代)它。当GC发现它死亡时,它将被回收。

运行终结器

这是由终结器线程处理的。在GC发现死的、可终结的对象(然后被升代)后,它将其移至终结队列的一部分,告诉终结者线程何时向GC请求运行终结者,并向终结者线程发出信号,表示有工作要做。在GC完成后,终结器线程将运行这些终结器。被移到终结队列这一部分的对象被说成是"准备好终结了"。你可能已经看到各种工具提到了这个术语,例如,sos的!finalizequeue命令告诉你finalize队列的哪一部分储存了准备好的对象,像这样:

Readyforfinalization0objects(000002E092FD9920->000002E092FD9920)

您经常会看到这是0,因为终结器线程以高优先级运行,因此终结器将快速运行(除非它们被某些东西阻塞)。

下图说明了2个对象以及可最终确定的对象F是如何演变的。正如你所看到的,在它被提升到gen1之后,如果有一个gen0的GC,F仍然是活的,因为gen1没有被收集;只有当我们做一个gen1的GC时,F才能真正成为死的,我们看一下F所处的代。

图6-O是不可终结的,F是可终结的

现在我们了解了不同类别的根,我们可以谈谈管理性内存泄漏的定义了

托管内存泄漏意味着你至少有一个用户根,随着进程的运行,直接或间接地引用了越来越多的对象。这是一个泄漏,因为根据定义,GC不能回收这些对象的内存,所以即使GC尽了最大努力(即做一个全堆阻塞的GC),堆的大小最终还是会增长。

所以最简单的方法,如果可行的话,识别你是否有托管内存泄漏,就是在你知道你应该有相同的内存使用量的时候,简单地诱导全阻塞GC(例如,在每个请求结束时),并验证堆的大小没有增长。显然,这只是一种帮助调查内存泄漏的方法--当你在生产中运行你的应用程序时,你通常不希望诱发全阻塞的GCs。

我们没有提到的GC暂停的最后一个部分是根本不做GC工作的部分--我指的是运行时中的线程暂停机制。GC调用暂停机制,让进程中的线程在GC工作开始前停止。我们调用这个机制是为了让线程到达它们的安全点。因为GC可能会移动对象,所以线程不能在随机的点上停止;它们需要在运行时知道如何向GC报告对GC堆对象的引用的点上停止,这样GC才能在必要时更新它们。这是一个常见的误解,认为GC在做暂停工作--GC只是调用暂停机制来让你的线程停止。然而暂停被报告为GC暂停的一部分,因为GC是使用它的主要组件。

注意,在GC的暂停部分,只有运行托管代码的线程被暂停。运行本地代码的线程可以自由运行。然而,如果它们需要在这样的暂停部分返回到托管代码,它们将需要等待,直到暂停部分结束。

与任何性能调查一样,首要的是弄清楚你是否应该担心这个问题。

既然你在阅读本文档,显然你关心的组件之一就是GC。那么,你应该跟踪哪些顶层的GC指标,以及如何决定何时应该担心?

表格3

让我们详细看看每个目标,以了解为什么你应该看他们相应的GC指标-

吞吐量

为了提高你的吞吐量,你希望GC尽可能少地干扰你的线程。GC会在以下两个方面进行干扰

线程被GC暂停时的耗时/进程的总耗时

(100%*10%)/(100%*10%+50%*90%)=18%

尾部延时

内存占用率

PerfView中的另一个功能,我不太经常使用,但作为GC的用户,你可能更经常使用,那就是堆快照,即显示堆上有哪些对象,它们之间是如何连接的。我不经常使用它的原因是,GC并不关心对象的类型。

当你开始进行内存性能分析时,这些步骤是否听起来很熟悉?

捕获一个CPU采样文件,看看你是否可以减少任何高开销的方法的CPU

在一个工具中打开一个堆快照,看看你能摆脱什么?

捕获内存分配,看看你能摆脱什么?

Memory\AvailableMBytes(内存\可用内存单位MB)Process\PrivateBytes(进程\私有内存占用单位MB)

对于一般的CPU使用率,你可以监控

GC发出轻量级的信息级事件,你可以收集(如果你愿意,可以一直开着),涵盖所有顶级的GC指标。使用PerfView的命令行来收集这些事件-

perfview/GCCollectOnly/AcceptEULA/noguicollect

完成后,在perfviewcmd窗口中按下s来停止它。

perfview/GCCollectOnly/AcceptEULA/nogui/MaxCollectSec:1800collect

并将1800(半小时)替换为你需要的任何秒数。当然,你也可以将这个参数应用于其他命令行。这将产生一个名为PerfViewGCCollectOnly.etl.zip的文件。用PerfView的话来说,我们称之为GCCollectOnly跟踪。

在Linux上,有一种类似的方法,就是这个dotnettrace命令行:

dotnettracecollect-p-o--profilegc-collect--duration

这算是一种等价的方法,因为它收集了同样的GC事件,但只针对已经启动的一个进程,而perfview命令行收集的是整个机器的ETW,即该机器上每个进程的GC事件,在收集开始后启动的进程也会收集到。

在PerfView的"GCStats"视图中,这些数据与你刚刚收集的轨迹一起被方便地显示。

在PerfView中打开PerfViewGCCollectOnly.etl.zip文件,即通过运行PerfView并浏览到该目录,双击该文件;或者运行"PerfViewPerfViewGCCollectOnly.etl.zip"命令行。你会看到该文件名下有多个节点。我们感兴趣的是"MemoryGroup"节点下的"GCStats"视图。双击它来打开它。在顶部,我们有这样的内容

我运行了VisualStudio,它是一个托管应用程序--这就是顶部的devenv进程。

对于每一个进程,你都会得到以下细节--我对那些命令行的进程添加了注释。

GCstatsrollupbygeneration–对于gen0/1/2,它有一些东西,如该gen的多少个GCs被完成,它们的平均/平均停顿,等等。

LOHAllocationPause(duetobackgroundGC)>200Msecforthisprocess-Gen2GCstats该进程的LOH分配暂停(由于后台GC)>200Msec对于大型对象的分配,有一个注意事项,即在BGC进行时,我们不允许过多的LOH分配。如果你在BGC期间分配了太多的对象,你会看到一个表格出现在这里,告诉你哪些线程在BGC期间被阻塞了(以及多久),因为有太多的LOH分配。这通常是一个信号,告诉你减少LOH分配将有助于不使你的线程被阻塞。

AllGCstats所有GC统计资料

CondemnedreasonsforGCsGC被触发的原因

FinalizedObjectCounts终结器对象数量

Summaryexplanation"摘要"解释

·Commandlineself-explanatory.命令行

·Runtimeversion运行时版本是相当无用的,因为我们只是显示一些通用版本。然而,你可以使用事件视图中的FileVersion事件来获得你所使用的运行时的确切版本。

·TotalAllocs你在这次追踪中为这个进程所做的总分配。

·MaxGCHeapSize在本次跟踪过程中,该进程的最大托管堆尺寸。

·其余的都是链接,我们将在本文件中介绍其中一些。

所有AllGCstats表中显示了在跟踪收集过程中发生的每一个GC(如果有太多的话,它会有一个链接到没有显示的GC)。在这个表中有很多列。由于这是一个非常广泛的表格,我将只显示这个表格中与每个主题有关的列。

其他顶级的GC指标,个别暂停和堆大小,作为这个表格的一部分被显示出来,就像这样(PeakMB指的是该GC进入时的GC堆大小,After是退出时的大小)。

现在,这是一个html表格,你不能进行排序,所以如果你确实想进行排序(例如,找出最长的单个GC停顿),你可以点击每个过程开始时的"在Excel中查看"链接--

·IndividualGCEvents

oViewinExcel

除了GCStats视图之外,介绍PerfView中的其他几个视图也很有用,因为我们会用到它们。

CPUStacks是你所期望的--如果你在追踪中收集了CPU的样本事件,这个视图就会亮起来。有一点值得一提的是,我总是先清除3个高亮的文本框--在你这样做之后,你需要点击更新来刷新。

我很少发现这3个文本框有用。偶尔我会想按模块分组,你可以阅读PerfView的帮助文档,看看如何做到这一点。

Events就是我们上面提到的-原始事件视图。由于是原始的,它可能听起来很不友好,但它有一些功能,使它相当方便。你可以通过"过滤器"文本框过滤事件。如果你需要用多个字符串进行过滤,你可以使用"|"。如果我想获得所有名称中带有file的事件和GC/Start事件,我使用file|GC/Start(没有空格)。

双击一个事件的名称会显示该事件的所有发生情况,你可以搜索具体细节。例如,如果我想找出coreclr.dll的确切版本,我可以在查找文本框中输入coreclr。

然后你可以看到你正在使用的coreclr.dll的确切版本信息。

内存组下的GCHeapAllocIgnoreFree是我们要用来查看分配的东西。

AnyStacks显示所有的事件和它们的堆栈(如果堆栈被收集)。如果我想看一个特定的事件,但还没有一个既定的视图,或者既定的视图没有提供我所要的东西,我觉得这很方便。

像CPU堆栈视图一样的视图(即堆快照视图或GC堆分配视图)提供了一个Diff-ing功能。如果你在同一个PerfView实例中打开2个跟踪,当你为每个跟踪打开一个堆栈视图时,并且Diff菜单提供了一个"withBaseline"选项(在Help/UsersGuide中搜索"DiffingTwoTraces")。

获得分配的字节数

我们已经知道,你可以在摘要中得到分配字节的总数。在GCStats视图中,你还可以得到每个GC的分配字节数gen0。

在Gen一栏中,'N'表示NonconcurrentGC,'B'表示Background。所以完全阻塞的GC显示为2N。因为只有gen2的GC可以是后台,所以你只能看到2B,而不是0B或1B。你也可能看到'F',这意味着前景GC--当BGC正在进行时发生的短暂GC。

注意,对于2B来说是0,因为如果gen0或gen1的分配预算被超过,我们会在BGC的开始做一个gen0或gen1的GC,所以gen0分配的字节数会显示在gen0或gen1的GC上。

我们知道,当gen0的分配预算被超过时,就会触发GC。这个数据在GCStats中默认是不显示的(只是因为表格中已经有很多列了)。但你可以通过点击GCStats中表格前的RawDataXMLfile(用于调试)链接来获得。一个xml文件将被生成并包含更详细的数据。对于每个GC,你会看到这个(我把它修剪了一下,所以不会太宽)-

FinalYoungestDesired是为这个GC计算的最终gen0预算。由于我们对所有堆的预算进行了均衡,这意味着每个堆都有这个相同的预算。由于有12个堆,任何一个堆用完它的gen0预算都会导致下一次GC被触发。所以在这种情况下,这意味着最多只有12*9,830,400=117MB的分配,直到下一次GC被触发。我们可以看到下一个GC是一个BGC,它的Gen0AllocMB是0,因为我们把这个BGC开始时做的短暂GC归结为GC#11,它在GC#9结束后在Gen0中分配了111.28MB。

查看带有堆栈信息的采样分配

当然,你会想知道这些分配的情况。GC提供了一个叫做AllocationTick的事件,大约每100KB的分配就会被触发。对于小对象堆来说,100KB意味着很多对象(这意味着对于SOH来说是采样),但对于LOH来说,这实际上是准确的,因为每个对象至少有85000字节大。这个事件有一个叫做AllocationKind的字段--small意味着它是由分配为SOH而触发的,而这个分配恰好使该SOH上的累积分配量超过了100KB(那么这个量会被重置)。所以你实际上不知道最后的分配量是多大。但是根据这个事件的频率,看看哪些类型被分配的最多,以及分配它们的调用栈,仍然是一个非常好的近似值。

很明显,与只收集GCCollectOnly跟踪相比,收集这个会增加明显的开销,但这仍然是可以容忍的。

PerfView.exe/nogui/accepteula/KernelEvents=Process+Thread+ImageLoad/ClrEvents:GC+Stack/BufferSize:3000/CircularMB:3000collect

这将收集AllocationTick事件及其分配被采样对象的调用栈。然后你可以在内存组下的"GCHeapAllocIgnoreFree(CoarseSampling)"视图中打开它。

点击一个类型,你就可以看到分配该类型实例的堆栈。

而且你可以双击每一种类型来查看分配给它们的调用栈。

查看AllocationTick事件的另一种方法是使用AnyStacks视图,因为它按大小分组。例如,这是我从一个客户的跟踪中看到的情况(类型名称已匿名或缩短)。

这说明大部分分配来自于字典的重新调整,你也可以从GCHeapAlloc视图中看到,但样本计数信息给了你更多的线索(Resize有4013次,而Initialize有159次)。所以,如果你能更好地预测字典会有多大,你可以把初始容量设置得更大,以大大减少这些分配。

**使用CPU样本查看内存清理成本**

如果你没有这些AllocationTick事件的跟踪,但有CPU样本(这很常见),你也可以看一下内存清除的成本-

如果你看一下memset_repmovs的调用者,突出显示的2个调用者来自GC在把新对象递出之前的内存清除:

(这是在.NET5下,如果你有旧版本,你会看到WKS::gc_heap::bgc_loh_alloc_clr而不是WKS::gc_heap::bgc_uoh_alloc_clr)。

在我的例子中,因为分配几乎是测试的全部内容,所以分配成本非常高--占CPU总使用量的25.6%

而我们知道,触发频繁的完全GC通常是性能问题的秘诀。其他由于分配引起的触发原因是OutOfSpaceSOH和OutOfSpaceLOH-你看到这些比AllocSmall和AllocLarge要少得多--这些是为你接近物理空间极限时准备的(例如,如果我们内存分配正在接近短暂段的终点)。

那些几乎总是引起危险信号的事件是"Induced",因为它们意味着一些代码实际上是自己触发了GC。我们有一个GCTriggered事件,专门用于发现什么代码用其调用栈触发了GC。你可以用堆栈和最小的内核事件收集一个非常轻量级的GC信息级别的跟踪:

PerfView.exe/nogui/accepteula/KernelEvents=Process+Thread+ImageLoad/ClrEvents:GC+Stack/ClrEventLevel=Informational/BufferSize:3000/CircularMB:3000collect

然后在任意堆栈视图中查看GCTriggered事件的堆栈:

因此,"触发原因"是指GC是如何开始或产生的。如果一个GC开始的最常见原因是由于在SOH上分配,那么这个GC将作为一个gen0的GC开始(因为gen0的预算被超过了)。现在在GC开始之后,我们再决定我们实际上会收集哪一代。它可能保持为0代GC,或者升级为1代甚至2代GC-这是我们在GC中最先决定的事情之一。导致我们升级到更高世代的GC的因素就是我们所说的"派遣的原因"(所以对于一个GC来说,只有一个触发的原因,但可以有多个派遣的原因)。

以下是出现在表格本身之前的"GC的派遣理由"部分的解释文本

本表更详细地说明了GC决定收集那一代的确切原因。将鼠标悬停在各列标题上,以获得更多信息。

我不会在这里重复这些信息。最有趣的是那些升级到gen2的GC-通常这些是由gen2的高内存负载或高碎片引起的。

我们知道,所有短暂的GCs都是阻塞的,而gen2GC可以是阻塞的,也可以是背景的(BGC)。短暂的GC和BGC应该会产生短暂的停顿。但是事情可能出错,我们将展示如何分析这些情况。

如果堆的大小很大,我们知道一个阻塞的gen2GC会产生一个很长的停顿。但是当我们需要做gen2GC的时候,我们一般倾向于BGC。所以长的GC暂停是由于阻塞的gen2GC造成的,我们会想弄清楚为什么我们要做这些阻塞的gen2GC。

·在暂停期间有很多GC工作要做。

让我们看看如何分析每个场景。

在生产中触发完全阻塞的GC可能是非常不可取的,所以在开发阶段做尽职调查很重要。例如,如果你的产品处理请求,你可以在每个请求或每N个请求结束时触发一个完全阻塞的GC。如果你认为内存使用量应该是相同的,你应该能够用工具来验证。在这种情况下可以使用很多工具,因为这是一个简单的场景。所以PerfView当然也有这个功能。你可以通过Memory/TakeHeapSnapshot来获取堆快照。它确实有一些不完全直截了当的选项-

"MaxDumpKObjs"是一个"聪明"的尝试,所以它不会转储每一个对象。我总是把它增加到至少是默认值(250)的10倍。冻结选项是为生产诊断而设的,当你不想招致完全阻塞的GC暂停时。但是,如果你在开发过程中这样做,并试图了解你的应用程序的基本行为,你没有理由不检查它,这样你就能得到一个准确的图片,而不是用非Freeze选项试图"尽最大努力来跟踪对象图"。

然后在PerfView中打开生成的.gcDump文件,它有一个类似堆栈的视图,显示根信息(例如,GC句柄持有这个对象)和转储中的类型实例的聚合信息。由于这是一个类似于堆栈的视图,它提供了差分功能,所以你可以在PerfView中取两个gcDump文件并进行差分。

当你在生产中这样做时,你可以先尝试不使用冻结。

这说明当GC#45发生时,它观察到的内存负载为47%。

我不知道有什么其他工具可以方便地告诉你这些信息(如果你知道有什么工具可以告诉你老一代的对象在年轻一代的对象上保持着什么,使它们在GC期间存活,请好心地告诉我!)。

偶尔长停的一个例子-

Microsoft-Windows-DotNETRuntime/GC/SuspendEEStart //开始暂停托管线程运行Microsoft-Windows-DotNETRuntime/GC/SuspendEEStop //暂停托管线程完成Microsoft-Windows-DotNETRuntime/GC/Start //GC开始回收Microsoft-Windows-DotNETRuntime/GC/Stop //GC回收结束Microsoft-Windows-DotNETRuntime/GC/RestartEEStart //恢复之前暂停的托管线程Microsoft-Windows-DotNETRuntime/GC/RestartEEStop //恢复托管线程运行完成(你可以在事件视图中看到这些内容)

在一个典型的阻塞式GC中(这意味着所有短暂的GC和完全阻塞的GC),事件发生顺序非常简单:

BGC要复杂得多,一个完整的BGC事件序列看起来是这样的

有3个GC特定的停止触发器-

我通常与/StopOnGCOverMSec和/StopOnBGCFinalPauseOverMSec一起使用的命令行是--

PerfView.exe/nogui/accepteula/StopOnGCOverMSec:15/Process:A/DelayAfterTriggerSec:0/CollectMultiple:3/KernelEvents=default/ClrEvents:GC+Stack/BufferSize:3000/CircularMB:3000/Merge:TRUE/ZIP:Truecollect

你可以看到MsMpEng.exe进程的样本的优先级非常高--15。服务器GC线程运行的优先级是11左右。

PerfView.exe/nogui/accepteula/StopOnGCSuspendOverMSec:200/Process:A/DelayAfterTriggerSec:0/CollectMultiple:3/KernelEvents=ThreadTime/ClrEvents:GC+Stack/BufferSize:3000/CircularMB:3000/Merge:TRUE/ZIP:Truecollect

然而,ThreadTime追踪可能太多,可能会导致你的应用程序运行得不够"正常",无法表现出你所调试的行为。在这种情况下,我会从默认的内核事件开始追踪,这通常会揭示问题或给你足够的线索。你可以简单地把ThreadTime替换成Default-

PerfView.exe/nogui/accepteula/StopOnGCSuspendOverMSec:200/Process:A/DelayAfterTriggerSec:0/CollectMultiple:3/KernelEvents=Default/ClrEvents:GC+Stack/BufferSize:3000/CircularMB:3000/Merge:TRUE/ZIP:Truecollect

剖析一下这些尺寸是有帮助的。AfterMB这一栏是以下的总和

Gen0MB+Gen1MB+Gen2MB+LOHMB

perfview/nogui/KernelEvents=Process+Thread+ImageLoad/ClrEvents:GC+Stack+GCHandle/clrEventLevel=Informationalcollect

SizeBefore=ObjSpaceBefore+FreeListSpaceBefore+FreeObjSpaceBefore

SizeBefore`这一代的总规模

ObjSpaceBefore这一代的有效对象所占的大小

FreeListSpaceBefore这一代的自由列表所占的大小

FreeObjSpaceBefore在这一代中,太小的自由对象所占用的大小,不能进入自由列表。

(FreeListSpaceBefore+FreeObjSpaceBefore)就是我们所说的碎片化

在这种情况下,我们看到((FreeListSpaceBefore+FreeObjSpaceBefore)/SizeBefore)是5%,这是相当小的,这意味着我们已经用掉了大部分BGC建立好的自由空间。当然,我们希望看到这个比例越小越好,但如果自由空间太小,就意味着GC可能无法使用它们。一般来说,如果这个比例是15%或更小,我不会担心,除非我们看到自由空间足够大但没有被使用。

在你经历了上述情况后,你可能会发现,从GC的角度来看,这一切都可以解释。但如果你仍然希望堆的大小更小呢?

暂停通常在每次发生时都会少于1ms。如果你看到的是10秒或100秒的东西,你不需要怀疑你是否有宁问题--这是一个明确的信号,说明你有。

这可以通过GCStats视图中的"SuspendMsec"和"PauseMsec"列来表示。我模拟了一个例子--

每个版本都会有新的GC变化,所以我们很自然地想知道你使用的是哪个版本的运行时,以便我们知道该版本的运行时有哪些GC变化。所以提供这些信息是非常必要的。版本如何映射到"公共名称",如.NET4.7,是不容易追踪的,所以提供dll的"FileVersion"属性会对我们有很大帮助,它可以告诉我们版本号与分支名称(对于.NETFramework)或实际提交(对于.NETCore)。你可以通过像这样的powerhell命令来获得这些信息:

PSC:\Windows\Microsoft.NET\Framework64\v4.0.30319>(Get-ItemC:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll).VersionInfo.FileVersion4.8.4250.0builtby:NET48REL1LAST_CPSC:\>(Get-ItemC:\temp\coreclr.dll).VersionInfo.FileVersion42,42,42,42424@Commit:a545d13cef55534995115eb5f761fd0cecf66fc1获得这些信息的另一个方法是通过调试器通过lmvm命令(部分省略)-

0:000>lmvmcoreclrBrowsefullmoduleliststartendmodulename00007ff8`f1ec000000007ff8`f4397000CoreCLR(deferred)Imagepath:C:\runtime-reg\artifacts\tests\coreclr\windows.x64.Debug\Tests\Core_Root\CoreCLR.dllImagename:CoreCLR.dllInformationfromresourcetables:FileVersion:42,42,42,42424@Commit:a545d13cef55534995115eb5f761fd0cecf66fc1如果你捕捉到ETW跟踪,你也可以找到KernelTraceControl/ImageID/FileVersion事件。它看起来像这样(部分省略)。

就像任何性能问题一样,在没有任何性能跟踪数据的情况下,我们真的只能给出一些一般性的指导和建议。要真正找出问题所在,我们需要性能跟踪数据。

[苏州-同程旅行]-.NET后端研发工程师

招聘中级及以上工程师,优秀应届生也可以,我会全程跟进,从职位匹配,到面试建议与准备,再到面试流程和每轮面试的结果等。大家可以直接发简历给我。

工作职责负责全球前三中文在线旅游平台机票业务系统的研发工作,根据需求进行技术文档编写和编码工作

THE END
1....是科学探究的常用方法之一B.调查首先要明确调查目的和调查对象...13.下列关于调查的叙述中.错误的是( )A.调查是科学探究的常用方法之一B.调查首先要明确调查目的和调查对象C.无论调查范围有多大.都必须逐个调查D.在调查过程中要如实记录http://www.1010jiajiao.com/czsw/shiti_id_87751f93353ef35694e04b9d7a6a3c60
2.统计调查过程中采用的大量观察法,是指必须对研究对象的所有单位...统计调查过程中采用的大量观察法,是指必须对研究对象的所有单位进行调查。 参考答案:错 点击查看答案进入题库练习 查答案就用赞题库小程序 还有拍照搜题 语音搜题 快来试试吧 无需下载 立即使用 你可能喜欢 判断题 社会经济统计工作的研究对象是社会经济现象总体的数量方面。 参考答案:对 点击查看答案进入题库...https://m.ppkao.com/mip/tiku/shiti/8942658.html
1.统计学研究对象9篇(全文)会计学与财政学研究对象辨析对同属于价值运动管理的.会计学与财政学在研究对象上存在的共同之处,以及二者的明显差异进行了比较分析.分析表明,了解、把握和运用这些差异,有助于开拓会计学的研究视野,认识会计学的学科地位和社会属性.作者:张建军 作者单位:北京工业大学经https://www.99xueshu.com/w/file0v84pv5b.html
2.毛概社会实践调查报告3、社会实践(调查)要眼睛向下,充分占有第一手材料,利用所掌握的思想政治理论知识以及专业理论知识实事求是地进行研究分析,创造性地提出思考和建议。 4、调查对象必须真实、具体。为了反映本次社会调查的真实性,要求将本次调查的对象(被调查的单位、个人及地点)用图片及相关证明材料(须签字盖章)表现出来,调查报告须插入...https://www.jy135.com/diaochabaogao/516502.html
3.对某一调查对象在一个较长的时间内的特征变化进行调查,目的是了解...刷刷题APP(shuashuati.com)是专业的大学生刷题搜题拍题答疑工具,刷刷题提供对某一调查对象在一个较长的时间内的特征变化进行调查,目的是了解研究对象前后的变化和差异情况,这是教育调查研究中的()。A.现状调查B.相关调查C.发展调查D.预测调查的答案解析,刷刷题为用户https://www.shuashuati.com/ti/80b7a1ba0545468e9bd812ede9fddd56.html?fm=bdbds
4.小学生心理状况调查报告(通用10篇)20XX年10月5日——11月15日,我对我校店子二小四——六年级学生进行了小学生心理健康现状问卷调查,调查结果对了解我校学生心理健康现状,把握其心理动态,分析心理问题成因,寻找合理的教育对策提供了有益帮助。 二、对象和方法 (一)对象:xx小学四——六年级学生进行调查,基本情况见表:...https://mip.ruiwen.com/gongwen/baogao/458702.html
5.问卷调查分析报告(共13篇)“扇子不够撕”通过精心收集,向本站投稿了13篇问卷调查分析报告,下面是小编精心整理后的问卷调查分析报告,希望能够帮助到大家。篇1:问卷调查分析报告 一、调查对象:本次问卷调查https://www.gerenjianli.cn/fwdq/huibaobaogao/10253775.html
6.小学生心理降的调查报告(通用13篇)20xx年10月5日——11月15日,我对我校店子二小四——六年级学生进行了小学生心理健康现状问卷调查,调查结果对了解我校学生心理健康现状,把握其心理动态,分析心理问题成因,寻找合理的教育对策提供了有益帮助。 二、对象和方法 (一)对象:xx小学四——六年级学生进行调查,基本情况见表:...https://www.fwsir.com/jiaoan/html/jiaoan_20230114164600_2246441.html
7.Java基础建立Java面向对象编程OOP模型OOA是指在一个系统的开发过程中进行了系统业务调查以后,按照面向对象的思想来分析问题。OOA与结构化分析有较大的区别,OOA强调在系统调查资料的基础上,针对OO方法所需要的素材进行的归类分析和整理,而不是对管理业务现状和方法的分析。 OOA(面向对象的分析)模型由五个层次:主题层、对象类层、结构层、属性层和服务层...https://blog.csdn.net/qq_42322103/article/details/88254566
8.生态环境统计调查制度环境统计与总量控制《中华人民共和国统计法》第七条规定:国家机关、企业事业单位和其他组织以及个体工商户和个人等统计调查对象,必须依照本法和国家有关规定,真实、准确、完整、及时地提供统计调查所需的资料,不得提供不真实或者不完整的统计资料,不得迟报、拒报统计资料。 https://sthj.sh.gov.cn/hbzhywpt1133/hbzhywpt1135/20200410/b8b8c470a592474bba2c113d8ccadccf.html
9.关于英语调查报告二、调查结果数据统计表 三、调查的对象及内容 对象:我校八年级学生。 内容:主要从是否留守儿童,是否单亲家庭,有不良英语学习习惯的学生,喜欢英语学科的学生和喜欢英语老师的学生等方面设置问题。 四、 调查的结果分析 1、近年来,随着农业产业结构的调整、城市化进程的加快,人口迁移流动日益频繁,大批农村劳动者离开家...https://www.wenshubang.com/diaochabaogao/273753.html
10.幼儿降调查报告范文(通用9篇)想要知道一些情况或事件时,我们常常要开展全面的分析研究,调查的结果通常在调查报告上面呈现出来。你想知道调查报告怎么写吗?下面是小编收集整理的幼儿健康调查报告范文,仅供参考,欢迎大家阅读。 幼儿健康调查报告 篇1 一、幼儿心理健康现状 在十几年前,“心理健康教育”一词,对绝大多数人来说还是非常陌生的字眼,直到...https://m.yjbys.com/diaochabaogao/2496566.html
11.市场调查与预测2011年7月真题试题(00178)自考10.选择调查对象,主要是确定调查对象( ) A.应具备的条件 B.应抽取的样本容量大小 C.抽样方法 D.类型 11.在进行生产者市场调查时,除了注意处理好供应与需要之间的数量关系上的矛盾外,还必须注意分析生产资料供应同生产需要之间在下列哪一选项上的衔接问题?( ) ...https://www.educity.cn/zikao/26563.html
12.调研方案9篇第四阶段:组织实地调查,将调查人员分为几个小组,每个小组选择一个方向,有的小组找确定的被调查对象,依照问卷要求进行询问,有的小组在周边的餐厅进行观察记录,有的小组寻找实验对象进行实验调查。 第五阶段:统计调查资料,分析调查结果,撰写调查报告,将每个小组获得的信息和资料进一步统计分析,提出相应的建议和对策,并且...https://www.liuxue86.com/a/4915249.html
13.经济学专业本科人才培养方案专项调查(三)调查对象 湖北经济学院经济学系2006~2010届毕业生。 (四)调查时间 2010年6月至8月、2011年6月至8月 (五)调查方法 1、问卷调查 根据《方案》设计问卷,通过实地发放和发送电子邮件的方式进行数据的收集,整合出有效信息,进行深入分析。 2、实地考察 ...https://jmxy.hbue.edu.cn/ec/d8/c2289a126168/pagem.htm
14.检察院调查核实能不能接触被调查对象依据《人民检察院刑事诉讼规则》的规定,人民检察院对刑事案件进行调查核实时,调查核实一般不得接触被调查对象。必须接触被调查对象的,应当经检察长批准。关于检察院调查核实能不能接触被调查对象的问题,下面由华律网小编为你详细解答。一、检察院调查核实能不能接触被调查对象 1、一般不能。人民检察院对刑事案件进行...https://www.66law.cn/laws/1726421.aspx