经常看到有群友调侃“为什么搞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#、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

做法:仍然保留原始的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的内存结构更加紧凑,因此包含同样成员的struct比class对象内存占用更小。这就是为什么把class改为struct之后,峰值内存占用降低的原因。

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

当然不要因为struct比class占内存就滥用struct,和class相比,struct也有缺点,具体请自行搜索资料。

解决方案三: 手动GC

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

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

public class ValuesController : ControllerBase

{

            [HttpGet(Name = "RunGC")]

            public string RunGC()

            {

                        GC.Collect();

                        return "ok";

            }

}

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

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

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

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

做法:仍然保留原始的ToArray(),然后在ASP.NET Core项目文件(也就是csproj文件)中加入如下的配置:

<PropertyGroup>

            <ServerGarbageCollection>false</ServerGarbageCollection>

</PropertyGroup>

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

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

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

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

对应的,.NET的GC有Workstation和Server两种模式。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中也像Java的JVM中一样可以设置堆内存的大小、百分比等各种复杂的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技术内幕与项目实战》,这本书的宗旨就是“讲微软文档中没有的内容,讲原理、讲实践、讲架构”。具体见右边公告。

谁说.NET没有GC调优?只改一行代码就让程序不再占用内存的更多相关文章

  1. Java GC 专家系列3:GC调优实践

    本篇是”GC专家系列“的第三篇.在第一篇理解Java垃圾回收中我们学习了几种不同的GC算法的处理过程,GC的工作方式,新生代与老年代的区别.所以,你应该已经了解了JDK 7中的5种GC类型,以及每种G ...

  2. 八张图彻底了解JDK8 GC调优秘籍-附PDF下载

    目录 简介 分代垃圾回收器的内存结构 JDK8中可用的GC 打印GC信息 内存调整参数 Thread配置 通用GC参数 CMS GC G1参数 总结 简介 JVM的参数有很多很多,根据我的统计JDK8 ...

  3. GC参考手册 —— GC 调优(基础篇)

    GC调优(Tuning Garbage Collection)和其他性能调优是同样的原理.初学者可能会被 200 多个 GC参数弄得一头雾水, 然后随便调整几个来试试结果,又或者修改几行代码来测试.其 ...

  4. GC参考手册 —— GC 调优(工具篇)

    JVM 在程序执行的过程中, 提供了GC行为的原生数据.那么, 我们就可以利用这些原生数据来生成各种报告.原生数据(raw data) 包括: 各个内存池的当前使用情况, 各个内存池的总容量, 每次G ...

  5. 性能测试系列-java gc调优

    性能测试中除了需要做好性能测试外,我们还需要做性能测试后的,性能调优,需要发现性能问题,也需要做性能调优,在做性能调优中,jvm的性能调优是经常遇到的一个. 随着jdk版本的迅速变化,jdk里面的GC ...

  6. GC调优

    Gc调优的目标:1.降低停顿时间 2.提高吞吐量 3.避免full-gc 调优可以使用的手段:1.各个内存区的大小调整:堆,年轻代,老年代,方法区等等2.减少短暂对象的存活时间,提高长期对象的复用率( ...

  7. GC调优在Spark应用中的实践(转载)

    Spark是时下非常热门的大数据计算框架,以其卓越的性能优势.独特的架构.易用的用户接口和丰富的分析计算库,正在工业界获得越来越广泛的应用.与Hadoop.HBase生态圈的众多项目一样,Spark的 ...

  8. GC调优入门笔记

    想给项目代码做做调优但有许多疑惑,比如有哪些参数要调.怎么调.使用什么工具.调优的效果如何定量测量等.发现Oracle的这份资料不错,简洁直接,回答了我的许多问题,给了许多很实用的大方向上的指导.将其 ...

  9. GC调优在Spark应用中的实践[转]

    作者:仲浩   出处:<程序员>电子刊5月B   摘要:Spark立足内存计算,常常需要在内存中存放大量数据,因此也更依赖JVM的垃圾回收机制.与此同时,它也兼容批处理和流式处理,对于程序 ...

  10. gc调优我们到底在调整什么

    java开发一般都会涉及到jvm调优其中gc调优是个重点项.那gc调优调整的究竟是什么呢准确来说是业务.下面围绕这个话题展开 起因 为什么说是业务呢得从cc++开始说起如果说是用c/c++做开发运行的 ...

随机推荐

  1. 全志H616基于官方外设开发-蜂鸣器

    #include <stdio.h> #include <wiringPi.h> #include <unistd.h> #define BEEP 0 //设置针脚 ...

  2. Helm包管理

    Helm Kubernetes 包管理工具 Helm 可以帮助我们管理 Kubernetes 应用程序 - Helm Charts 可以定义.安装和升级复杂的 Kubernetes 应用程序,Char ...

  3. Elastic:Elastic部署架构介绍

    Elastic Stack是一套完整的从数据采集,解析,分析,丰富,到搜索,检索,数据程序等一套完整的软件栈.在具体的实践中,我们应该如何搭建我们的系统呢? 下图描述了常用的Elastic Stack ...

  4. Ceph分布式存储详述

    存储发展史 企业中使用存储按照其功能,使用场景,一直在持续发展和迭代,大体上可以分为四个阶段: DAS:Direct Attached Storage,即直连存储,第一代存储系统,通过SCSI总线扩展 ...

  5. Beats在Kibana中的集中管理

    前提条件: 1.es版本是白金版 2.es开启安全设置,kibana访问es需要密码 操作步骤汇总: 1-3步是基础环境配置 4-9步是注册beats到集中管理平台,然后启动beats,只是单纯启动b ...

  6. Prometheus监控Nginx

    转载自:https://www.cnblogs.com/you-men/p/13173245.html CentOS7.3 prometheus-2.2.1.linux-amd64.tar.gz ng ...

  7. Git Review + Gerrit 安装及使用完成 Code-Review

    转载自:https://cloud.tencent.com/developer/article/1010615 1.Code Review 介绍 Code Review 代码评审是指在软件开发过程中, ...

  8. 2022IDEA破解

    注意 本教程适用于 IntelliJ IDEA 2022.1.2 以下所有版本,请放心食用~ 本教程适用于 JetBrains 全系列产品,包括 IDEA.Pycharm.WebStorm.Phpst ...

  9. 微信小程序开发优化

    一.开发优化一 1.使用Vant Weapp 1.1 什么是Vant Weapp Vant Weapp官网链接 Vant Weapp是有赞前端团队开源的一套小程序UI组件库,助力开发者快速搭建小程序应 ...

  10. 几个实用 shell 脚本

    1. Dos攻击防范(自动屏蔽攻击 IP) #!/bin/bash DATE=$(date +%d/%b/%Y:%H:%M) LOG_FILE=/usr/local/nginx/logs/demo2. ...