誰說.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.pswp.cn/news/280748.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/280748.shtml
英文地址,請注明出處:http://en.pswp.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…