谁说.NET没有GC调优,只改一行代码就让程序不再占用内存

经常看到有群友调侃“为什么搞Java的总在学习JVM调优?那是因为Java烂!我们.NET就不需要搞这些!”真的是这样吗?今天我就用一个案例来分析一下。

昨天,一位学生问了我一个问题:他建了一个默认的ASP.NET Core Web API的项目,也就是那个WeatherForecast的默认项目模板,然后他把默认的生成5条数据的代码,改成了生成150000条数据,其他代码没变,如下:

public IEnumerable<WeatherForecast> Get()
{return Enumerable.Range(1, 150000).Select(index => new WeatherForecast{Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),TemperatureC = Random.Shared.Next(-20, 55),Summary = Summaries[Random.Shared.Next(Summaries.Length)]}).ToArray();
}

然后他用压力测试工具对这个.NET编写的Web API模拟了1000个并发请求,发现内存一路飙升到7GB,并且在压力测试结束之后,内存占用也不见回落。而他用Python编写的同样功能的Web API项目,他用压力测试工具对这个Python编写的Web API模拟了同样多的请求,发现内存同样飙升,但是在压力测试结束之后,内存占用很快回落到了正常的水平。

他不由得发出了疑问“这样简单的程序就有内存泄漏了吗?.NET的性能这么差吗?”

我用了四种方式“解决”了他的这个问题,下面我将会依次分析这几种方式的做法和原理。在这之前,我先简单科普一下垃圾回收(GC)的基本原理:

一个被创建出来的对象是占据内存的,我们必须在对象不再需要被使用之后把对象占据的内存释放出来,从而避免程序的内存占用越来越高。在C语言中,需要程序员来使用malloc来进行内存的申请,然后使用free进行内存的释放。而在C#JavaPython等现代编程语言中,程序员很少需要去关心一个被创建出来的对象,程序员只需要根据需要尽情地new对象出来即可,垃圾回收器(Garbage Collector,简称GC)会帮我们把用不到的对象进行回收。

关于GC还有“0代、1代”等问题,这些问题大家可以看如下.NET官方的资料:https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/?WT.mc_id=DT-MVP-5004444

下面开始谈这几种“解决方案”。

解决方案一:去掉ToArray()

做法:Get方法的返回值就是IEnumerable<WeatherForecast>类型,而Select()方法的返回值也就是同样的类型,所以完全没必要再ToArray()转换为数组再返回,因此我们把ToArray()去掉。代码如下:

public IEnumerable<WeatherForecast> Get()
{return Enumerable.Range(1, 150000).Select(index => new WeatherForecast{Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),TemperatureC = Random.Shared.Next(-20, 55),Summary = Summaries[Random.Shared.Next(Summaries.Length)]});
}

再运行同样的压力测试,惊人的一幕发生了,峰值内存占用也不到100MB

原理分析:

这是为什么呢?IEnumerable以及LINQ默认是以一种“流水线”的方式在工作,也就是说使用IEnumerable的消费者(比如这里消费IEnumerable的应该是Json序列化器)每调用MoveNext()一次获取一条数据才执行一次Select()来创建一个新的WeatherForecast对象。而加上ToArray()之后,则是一次性生成150000WeatherForecast对象,并且把这150000个对象放到一个数组中才把这个大数组返回。

对于不采用ToArray()的“流水线式”工作方式,对象是一个个产生、一个个的消费,因此同时并发生成的对象是“缓缓流淌”的,因此不会有ToArray()那样逐渐累积150000个对象的操作,因此并发内存占用更小。同时,由于WeatherForecast对象是流水线式生产、消费的,因此当一个WeatherForecast对象被消费完成后,就“可以”被GC回收了。而用ToArray()之后,数组对象会持有那150000WeatherForecast对象的引用,因此只有数组对象被标记为“可回收”之后,那150000WeatherForecast对象才有可能被标记为“可回收”,因此WeatherForecast对象被回收的机会被大大推后。

不知道为什么微软官方要给WeatherForecast这个Web API例子项目代码里给出ToArray()这样没必要的写法,我要去找微软的人去反馈,谁也别拦着我!

这给我们的启示就是:尽量让Linq“流水线式”工作,尽量使用IEnumerable类型,而不是数组或者List类型,每次对IEnumerable类型使用ToArray()ToList()操作的时候要谨慎。

上面这个方案是最完美的方案,下面的几种方案只是为了帮助大家更深入的理解GC

解决方案二:把class改成struct

做法:仍然保留原始的ToArray(),但是把WeatherForecast类型从class改为struct(结构体),代码如下:

public struct WeatherForecast
{public DateOnly Date { get; set; }public int TemperatureC { get; set; }public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);public string? Summary { get; set; }
}

再运行同样的压力测试,用struct的峰值内存占用只有用class的大约一半,同样的,在压力测试结束之后,内存占用没有回落。

原理分析:class对象包含的信息更多,而struct包含的信息更少,而且struct的内存结构更加紧凑,因此包含同样成员的structclass对象内存占用更小。这就是为什么把class改为struct之后,峰值内存占用降低的原因。

有的朋友可能会问“不是说struct对象是分配在栈上,会用完了之后自动回收,不需要GC回收吗?为什么在压力测试结束后内存占用没有回落呢?难道struct的内存没有被自动回收吗?”。需要注意的是“struct对象会自动回收,不需要GC”这种情况只发生在struct对象没有被引用类型对象所引用的情况,一旦一个struct对象被一个引用类型对象引用之后,struct对象也需要由GC来回收。我们的代码中由于进行了ToArray()操作,所以这150000struct对象会被一个数组引用,因此这些struct对象就必须依赖于GC的回收了。

解决方案三:手动GC

做法:既然由于GC没有及时执行导致在压力测试结束之后内存居高不下,那么我们可以在压力测试结束后手动调用GC,强制运行垃圾回收。

我们再创建一个新的Controller,然后在Action中调用一下GC.Collect()来强制执行内存回收。代码如下:

public class ValuesController : ControllerBase
{[HttpGet(Name = "RunGC")]public string RunGC(){GC.Collect();return "ok";}
}

我们再执行压力测试,在压力测试完成后,很显然内存占用没有回落。然后我们多请求几次RunGC(),我们就能发现内存占用回落到100MB了。

原理分析:GC.Collect();就是强制执行内存回收,所以那些还没有被回收的WeatherForecast对象就会被回收了。为什么要多次调用GC.Collect();才会让内存占用回落到初始状态呢?那是因为内存回收是比较消耗CPU的操作,为了避免对程序性能造成影响,所以不会一次执行垃圾回收的时候把所有用不到的对象一次性全部回收。

主要注意的是,手动调用GC.Collect()不是一个好的习惯,因为GC会根据策略选择合适的时机来执行内存回收,手动的执行垃圾回收可能会造成程序的性能问题。如果需要手动GC.Collect()来降低让程序内存占用的达到你的期望的目的,要么是你的程序需要优化,要么是你对程序的内存占用的期望是错误的。什么叫“对程序的内存占用的期望是错误的”呢?下面这个解决方案会提到。

解决方案四:调整GC的类型

做法:ASP.NET Core项目文件(也就是csproj文件)中加入如下的配置:

<PropertyGroup><ServerGarbageCollection>false</ServerGarbageCollection>
</PropertyGroup>

再运行同样的压力测试,压力测试结束后,内存占用很快就回落到初始的100MB了。

原理分析:我们知道,我们开发的程序常用的有两种类别:桌面程序(如WinFormsWPF)和服务器端程序(如ASP.NET Core)。

桌面程序一般不会独占整个操作系统的内存和CPU资源,因为操作系统上还有很多其他程序在运行,因此桌面程序在内存和CPU占用上比较保守。对于一个桌面程序,如果它内存占用过多,我们会认为它不好。

与之相反,服务器端程序通常是拥有整个服务器的内存和CPU资源的(因为正常的系统都会把数据库、Web ServerRedis等部署到不同的计算机中),所以充分利用内存和CPU能够提升网站程序的性能。这就是为什么Oracle数据库默认会占满服务器的大部分内存的原因,因为内存闲着也是闲着,不如用起来提高性能。对于一个网站程序,如果可以通过占尽可能多的内存提升性能,但是它却占很少的内存,我们会认为它对内存利用不足,当然这里指的不是滥用内存。

对应的,.NETGCWorkstationServer两种模式。Workstation模式是为桌面程序准备的,内存占用偏保守,而Server模式是为服务器端程序准备的,内存占用上更激进。我们知道垃圾回收比较消耗资源,对于服务器端程序来讲,频繁的GC会降低性能,因此Server模式下,只要还有足够的可用内存,.NET会尽量降低GC的频率和范围。而桌面程序对GC造成的性能影响容忍度高,而对内存占用过多则容忍度低。因此Workstation模式下,GC会更高频的运行,从而保证程序内存占用小;而Server模式下,只要还有足够多的可用内存,GC就尽量少运行,运行的时候也不会长时间的进行大量对象的回收。当然,这两种模式还有很多其他的区别,详细请查看微软的文档:https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/workstation-server-gc?WT.mc_id=DT-MVP-5004444

ASP.NET Core程序默认就是启用的Server模式的GC,所以压力测试结束后,内存也没有回落。而通过<ServerGarbageCollection>false</ServerGarbageCollection>禁用Server模式的GC之后,GC就变成了Workstation模式后,程序就会更激进地回收内存了。当然把服务器端程序改为Workstation模式之后,程序的性能就会受影响,因此除非有充足的理由,否则不建议这样做,毕竟对于服务器来讲,内存闲着就是一种浪费。

除了GC的模式之外,.NET中也像JavaJVM中一样可以设置堆内存的大小、百分比等各种复杂的GC调优参数,详细请阅读微软的文档 https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector?WT.mc_id=DT-MVP-5004444

总结:尽量使用LINQ的“流水线”操作,尽量避免对大数据量的数据源进行ToArray()或者ToList();避免手动GC;建立对程序内存占用的正确期望,对于服务器端程序来讲并不是内存占用越低越好;用好GC的模式,从而满足不同程序的性能和内存占用的不同追求;可以通过GC的参数来对于程序的性能进行更加个性化的设置。

欢迎阅读我编写的《ASP.NET Core技术内幕与项目实战》,这本书的宗旨就是“讲微软文档中没有的内容,讲原理、讲实践、讲架构”。人民邮电出版社出版,在京东、淘宝都可以购买到。也可以点击下方链接购买。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/280748.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

wmi服务或wmi提供程序_什么是WMI提供程序主机(WmiPrvSE.exe),为什么使用那么多的CPU?...

wmi服务或wmi提供程序The WMI Provider Host process is an important part of Windows, and often runs in the background. It allows other applications on your computer to request information about your system. This process shouldn’t normally use many system re…

C# 快捷键/hotkey简单例子

1.导入dll [System.Runtime.InteropServices.DllImport("user32.dll")] //申明API函数public static extern bool RegisterHotKey(IntPtr hWnd, // handle to windowint id, // hot key identifieruint fsModifiers, // key-modifier optionsKeys vk // virtual-key …

POJ 3233

矩阵分治 注意不要用 (*this) 会改变原值 #include <iostream> #include <cstdio> #include <cstring> #include <cmath> #include <algorithm> #include <cstdlib> using namespace std; int n, p, k; struct Matrix{int num[35][35];voi…

zookeeper和etcd有状态服务部署

zookeeper和etcd有状态服务部署实践 docker etcd zookeeper kubernetes 4k 次阅读 读完需要 78 分钟 0 一. 概述 kubernetes通过statefulset为zookeeper、etcd等这类有状态的应用程序提供完善支持&#xff0c;statefulset具备以下特性&#xff1a; 为pod提供稳定的唯一…

正在创建系统还原点_如何使Windows在启动时自动创建系统还原点

正在创建系统还原点By default, System Restore automatically creates a restore point once per week and also before major events like an app or driver installation. If you want even more protection, you can force Windows to create a restore point automaticall…

WinForm(十六)绑定

在WinForm中&#xff0c;有很多添加和修改数据的场景&#xff0c;一般的做法是当点击“添加”按钮时&#xff0c;收集各控件的值&#xff0c;然后赋值给实体类的各个属性&#xff0c;然后再完成保存工作。在修改时&#xff0c;首先把实体的原值&#xff0c;一个个赋值给控件&am…

在ubuntu 16.04里使用python—scrapy将爬取到的数据存到mysql数据库中的一些随笔

一、将爬取的数据保存到mysql数据库的代码&#xff08;已经能将爬取的数据保存到json文件&#xff09; &#xff08;1&#xff09;编辑Pipeline.py文件 &#xff08;2&#xff09;编辑settings.py文件 二、将数据保存至mysql数据库出现的问题 &#xff08;1&#xff09;在将数据…

powershell XML操作

1.直接加入xml结构 加入<title>是为了后续能直接添加其他node&#xff0c;否则&#xff0c;后续操作可能无法AppendChild $xml "<?xml version1.0 encodingUTF-8?><case><title>please check each point</title></case>"$xm…

十大经典排序算法(动图演示)

转自&#xff1a;https://www.cnblogs.com/onepixel/articles/7674659.html 0、算法概述 0.1 算法分类 十种常见排序算法可以分为两大类&#xff1a; 非线性时间比较类排序&#xff1a;通过比较来决定元素间的相对次序&#xff0c;由于其时间复杂度不能突破O(nlogn)&#xff0c…

【Python】安装配置Anaconda

优点&#xff1a;解决Python 库依赖问题清华安装镜像https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/ 转载于:https://www.cnblogs.com/Neo007/p/7419253.html

如何实现 WPF 视频封面查看器

如何实现 WPF 视频封面查看器控件名&#xff1a;NineGridView作 者&#xff1a;WPFDevelopersOrg - 驚鏵原文链接[1]&#xff1a;https://github.com/WPFDevelopersOrg/WPFDevelopers框架使用.NET40&#xff1b;Visual Studio 2019;实现视频封面查看器NineGridView基于Grid实…

如何从Internet Explorer或Edge迁移到Chrome(以及为什么要迁移)

Google’s Chrome web browser is now more widely used than Microsoft’s Internet Explorer and Edge combined. If you haven’t switched to Chrome yet, here’s why you might want to–and how to quickly switch over. Google的Chrome网络浏览器现在的使用范围比Micro…

SQL中触发器的使用

创建触发器 是特殊的存储过程&#xff0c;自动执行&#xff0c;一般不要有返回值 类型&#xff1a; 1.后触发器 &#xff08;AFTER,FOR&#xff09;先执行对应语句&#xff0c;后执行触发器中的语句 2.前触发器 并没有真正的执行触发语句&#xff08;insert&#xff0c;update…

powershell XML数据保存为HTML

1.设置html头和尾 beginning内包含表格表头 $beginning {<html><head><meta charset"utf-8" /><title>Report</title><STYLE type"text/css">h1 {font-family:SegoeUI, sans-serif; font-size:30}th {font-family:…

浏览器自动化操作标准--WebDriver

WebDriver是一个浏览器远程控制协议&#xff0c;是一个既定标准&#xff0c;它本身的内容非常丰富&#xff0c;本文不可能全部介绍&#xff0c;本文仅粗略带大家了解一下WebDriver的部分内容以及一个小的实际应用。想深入了解的请参考W3C文档WebDriver. 问题背景 开发的同学都知…

versa max_如何从Mac(和Vice Versa)打开或关闭iPhone的Safari选项卡

versa maxMany of us are familiar with this scenario: you’re looking something up on our iPhone, find exactly what we’re looking for, but then have to put our phone away to attend to something else. Later, while working on your Mac, you want to continue w…

【nuxtjs 指南】解决nuxtjs本地开发跨域和防止路由与api冲突问题

目前vue很火&#xff0c;大部分开发者把vue当做框架首选&#xff0c;然而spa是对搜素引擎很不友好&#xff0c;就会想到ssr&#xff0c;在vue社区nuxtjs完美的解决了这个问题&#xff0c;目前nuxt还不算太成熟&#xff0c;当然对于新手坑比较多&#xff0c;当我们确定使用了这个…

WPF效果第二百零五篇之自定义导航控件

前面摸索了一下会简单玩耍自定义控件了;今天再次分享一下N年前想要在GIS实现的一个导航控件;来看看最终实现的效果:1、先来看看前台xaml布局:2、后台路由事件就参照上一篇快捷方式3、关键依赖属性的回调触发路由事件:4、内部Arc的MouseDown事件触发路由事件:private void Arc_M…

python3用list实现栈

工作中遇到的需求&#xff0c;****代表标签数据别的信息&#xff1a; D01  ********  1  ******** D01  ********  2  ******** D01  ********  3  ******** D01  ********  4  ******** D02  ********  1  ******** D02  ********  2  **…

powershell 脚本运行策略,参数....

1.运行策略 Powershell一般初始化情况下都会禁止脚本执行。脚本能否执行取决于Powershell的执行策略。 PS E:> Get-ExecutionPolicy Restricted PS E:> Set-ExecutionPolicy UnRestricted 2.直接运行 PS E:> "Hello,Powershell Script" > MyScript.ps…