经常看到有群友调侃“为什么搞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/ target=_blank class=infotextkey>Python编写的同样功能的Web API项目,他用压力测试工具对这个Python编写的Web API模拟了同样多的请求,发现内存同样飙升,但是在压力测试结束之后,内存占用很快回落到了正常的水平 。
【谁说.NET没有GC调优,只改一行代码就让程序不再占用内存】他不由得发出了疑问“这样简单的程序就有内存泄漏了吗?.NET的性能这么差吗?”
我用了四种方式“解决”了他的这个问题,下面我将会依次分析这几种方式的做法和原理 。在这之前,我先简单科普一下垃圾回收(GC)的基本原理:
一个被创建出来的对象是占据内存的,我们必须在对象不再需要被使用之后把对象占据的内存释放出来,从而避免程序的内存占用越来越高 。在C语言中,需要程序员来使用malloc来进行内存的申请,然后使用free进行内存的释放 。而在C#、Java、Python等现代编程语言中,程序员很少需要去关心一个被创建出来的对象,程序员只需要根据需要尽情地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()之后,则是一次性生成150000个WeatherForecast对象,并且把这150000个对象放到一个数组中才把这个大数组返回 。
对于不采用ToArray()的“流水线式”工作方式,对象是一个个产生、一个个的消费,因此同时并发生成的对象是“缓缓流淌”地,因此不会有ToArray()那样逐渐累积150000个对象的操作,因此并发内存占用更小 。同时,由于WeatherForecast对象是流水线式生产、消费的,因此当一个WeatherForecast对象被消费完成后,就“可以”被GC回收了 。而用ToArray()之后,数组对象会持有那150000个WeatherForecast对象的引用,因此只有数组对象被标记为“可回收”之后,那150000个WeatherForecast对象才有可能被标记为“可回收”,因此WeatherForecast对象被回收的机会被大大推后 。
不知道为什么微软官方要给WeatherForecast这个Web API例子项目代码里给出ToArray()这样没必要的写法,我要去找微软的人去反馈,谁也别拦着我!
这给我们的启示就是:尽量让Linq“流水线式”工作,尽量使用IEnumerable类型,而不是数组或者List类型,每次对IEnumerable类型使用ToArray()、ToList()操作的时候要谨慎 。
上面这个方案是最完美的方案,下面的几种方案只是为了帮助大家更深入的理解GC 。
解决方案二:把class改成struct
推荐阅读
- 美丽|李少莉事件尘埃落定?没有结果就是最好的结果
- 赵文瑄|新冠有没有后遗症?“老戏骨”赵文瑄:病了5天,1月后痊愈
- 2019年日本销量最高的十款车型,没有一款德系车
- 谁说冬天不能穿匡威!几招潮流穿搭,让你帅爆整个冬天
- 刘銮雄|刘銮雄或将再当爷爷,跟儿子已数年没有同框,父子两人感情成谜
- 芒果台|李维嘉,问了杜海涛一句话,没有人理他,快本四人已经离开芒果台
- 女性后腰上两个凹点到底是啥?为什么有的女性有,有的没有?
- B站和西瓜别争了,中国没有Youtube
- 抗战时期八路军一个团有多少人,到底有没有386旅独立团?
- 天下第一|《天下第一》古三通也会吸星大法,为什么没有传授给成是非呢?