前言
这是续第三节。
概况垃圾回收与我们写代码的关系:
- 强引用和弱引用
- 针对共享 Web 承载优化
- 垃圾回收和性能
- 应用程序域资源监视
正文
强引用和弱引用
垃圾回收器不能回收仍在引用的对象的内存——这是一个强引用。它可以回收不在根表中直接或间接的托管内存。然而,有时可能会忘记释放内存。
注意:如果对象相互引用,但是没有在根表中引用,例如:对象A 引用对象B,B引用C,C引用A,这时候如果ABC没有在根表中引用那么直接会被销毁。
补充一下根表在垃圾回收中的作用:
垃圾回收在引用的根表中找到所有引用对象,接着在引用的对象树中查找。
恰恰正好弱类型就没有在根表中,然后垃圾处理器首先开刀的就是弱类型引用。可以这么理解,理论上弱引用关联的对象只能生存到下一次垃圾收集发生为止,但是往往不会那么短时间,因为垃圾收集器并不是那么容易发现这些弱引用。
强引用很好理解:
如果应用程序的代码可以访问一个正由该程序使用的对象,垃圾回收器就不能回收该对象, 那么,就认为应用程序对该对象具有强引用。
var student=new Student();
一但student离开了所在作用区域那么引用对象就开始要被销毁了。
所以我们有缓存这个概念:
var myCache=new MyCache();
myCache.add(student);
缓存的本质目的不就是为了延长垃圾回收吗?或者说不让其垃圾回收,持续在内存中。当student 超出作用区后,还是不能释放student 的引用内存,因为此时对象在缓存对象中引用。
即使是student=null后,那么这个时候new Student()还是在内存中,因为被缓存对象引用了,student在栈中的指向(无论是清空回收还是置空)控制不了释放垃圾回收了。
那么能不能这样,即使被myCache引用了还是可以自动被消耗?这个时候就是弱类型登场的时候。
官方文档这样介绍道:
弱引用允许应用程序访问对象,同时也允许垃圾回收器收集相应的对象。
如果不存在强引用,则弱引用的有限期只限于收集对象前的一个不确定的时间段。
使用弱引用时,应用程序仍可对该对象进行强引用,这样做可防止该对象被收集。
但始终存在这样的风险:垃圾回收器在重新建立强引用之前先处理该对象。
占用大量内存,但通过垃圾回收功能回收以后很容易重新创建的对象特别适合使用弱引用。
假设 Windows 窗体应用中的树状视图向用户显示层次结构复杂的选项。 如果基础数据量很大,则用户使用应用程序中的其他部分时,在内存中保留该树会导致效率低下。
这里有些关键的地方,一个体现就是:数据量很大,也就是弱类型适合占有内存比较大的对象。为什么这样说呢?
是这样子的,我们创造一个弱类型就是要内存开销的,本身目的就是为了及时回收降低内存,这个时候整弱类型这不是添堵吗?
第二个在于容易创建,如果不容易创建,那么这个时候是空间换时间的代价有点大啊。
如何延长弱类型的生命周期呢?这时候应该使用七星灯[强类型]进行续命。
举个栗子:
var myWeaKReference=new WeakReference(new DataObject());
if(myWeaKReference.isAlive)
{
DataObject strongReference=myWeaKReference.Target as DataObject;
}
这时候吧弱类型给了一个强类型引用。
起码可以续命到if结束,也就不用担心用到一半的时候突然挂了,那么就非常尴尬。
垃圾回收和性能
垃圾回收机制和影响到性能,最简单的例子就是垃圾回收不好,导致了内存过大。
那么我们就需要去排除是不是垃圾回收的问题。
首先第一步要确定是否是垃圾回收问题,可能出现下面的问题:
1. 引发内存不足异常
2. 进程占用过多内存
3. 垃圾回收器回收对象的速度不够快
4. 托管堆太零碎
5. 垃圾回收暂停时间太长
6. 第 0 代太大
7. 垃圾回收期间的 CPU 使用率太高
那么如何去排除呢?这时候就要使用工具了。
举个容易出现的例子:托管堆太零碎,这与我们代码息息相关。
我们代码可能会出现,下面的情况:
频繁加载和卸载许多小的程序集。
与非托管代码互操作时,保留了太多对 COM 对象的引用。
大型暂时性对象的创建会导致大型对象堆频繁分配和释放堆段。
这些会导致托管堆太零碎。
如何去排查?
这时候可以使用windbg,这个工具还是很好用的。最主要是windows10现在自带了,没有版本不够升级一下,对了不会windows 10还用盗版吧?
我们都是正经人,能白嫖肯定白嫖啊,不给钱就不算嫖啊。
下面是我调试的内容:
可能有些人没用过windbg,简单过下流程。
打开windbg后:
选择对应的进程,进程很多,那么这个时候你应该打印出来。如果调试打包好的,直接看程序名。
Console.WriteLine(Process.GetCurrentProcess().Id);
然后开始调试。
你需要加载sos,来查看托管程序。
.net core 加载是这样子的.load C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.2.8\sos
然后查看一下是否加载完毕: !help.
然后你就可以查询一些托管的东西。
在这里!dumpheap -type Free -stat 显示堆里面的一些使用情况,上图windbg就是了。
若要确定第 0 代中的可用空间,请键入以下命令以获取代的内存使用信息:
!eeheap -gc
当然这是一个漫长查看过程,但是想要高性能,这又是必须的。
针对共享 Web 承载优化
我直接把文档里面的贴过来吧,因为这很详细了。
由于垃圾回收器保留内存以供将来分配,因此它提交的空间可能会超过真正所需。 可以减少此空间来适应系统内存负载过重的情况。 减少提交的此空间可提升性能,并将容量扩展为托管更多网站。
如果启用 gcTrimCommitOnLowMemory 设置,垃圾回收器会计算系统内存负载,并在负载达到 90% 时进入修整模式。 除非负载下降到不到 85%,否则会一直处于修整模式。
如果条件允许,垃圾回收器可以决定 gcTrimCommitOnLowMemory 设置对当前应用没有帮助并忽略它。
然后给了一个示例:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<runtime>
. . .
<gcTrimCommitOnLowMemory enabled="true"/>
</runtime>
. . .
</configuration>
应用程序域资源监视
这个是什么呢?就是说用来监控应用域监视cpu和内存的使用情况。里面是这样解释的,说多个应用在服务器上运行,可以监听到哪个程序占用过多,同时告诉我们这个arm消耗小。
这个呢,其实个人觉得现在容器化了,监控容器专门的工具了,很容易监听到。
有四种启动资源监控的东西。
1.可以在 CLR 启动时启用 ARM,具体操作是向配置文件添加 <appDomainResourceMonitoring> 元素,并将 enabled 属性设置为 true。 值 false(默认值)只表示不在启动时启用 ARM;稍后可以使用其他激活机制之一来激活它。
2.主机可以请求获取 ICLRAppDomainResourceMonitor 托管接口来启用 ARM。 成功获取此接口后,就会启用 ARM。
3.托管代码可以将静态 AppDomain.MonitoringIsEnabled 属性(Visual Basic 中的 Shared)设置为 true,从而启用 ARM。 设置此属性后,就会启用 ARM。
4.启动后,可以通过侦听 ETW 事件来启用 ARM。 使用 AppDomainResourceManagementKeyword 关键字启用公共提供程序 Microsoft-Windows-DotNETRuntime 后,ARM 便会启用,并开始抛出所有应用域的事件。 若要将数据与应用域及线程相关联,还必须使用 ThreadingKeyword 关键字启用 Microsoft-Windows-DotNETRuntimeRundown 提供程序。
首先改配置文件的放弃。然后 Windows 事件跟踪 (ETW)是windows的。去调用api感觉麻烦。应用程序域资源监视非常重要,但是还是找个第三方监听吧。
结
前面一直介绍托管资源,后面介绍非托管资源,整理了一点点。
注:上述纯属个人的整理,如有误,望指出。