بهینهسازی حافظه و عملکرد Garbage Collector در برنامههای ASP.NET Core
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۳/۱۴ ۰۹:۵۲
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
.csproj) خود را به صورت زیر ویرایش کنید:<PropertyGroup> <ServerGarbageCollection>true</ServerGarbageCollection> </PropertyGroup>
runtimeconfig.json:{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
}
}<PropertyGroup> <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection> </PropertyGroup>
runtimeconfig.json:{
"runtimeOptions": {
"configProperties": {
"System.GC.Concurrent": true // Mostly relevant for Workstation GC
}
}
}System.GC.Server معمولاً به طور ضمنی بهترین حالت همزمانی را برای سناریوهای سروری فراهم میکند.cgroups در لینوکس (که توسط Kubernetes برای مدیریت منابع استفاده میشود) آگاه باشد.GCHardLimitPercent) یا به عنوان یک مقدار مطلق (GCHardLimit) تنظیم کنید. در تجربه گزارش شده، پس از پروفایلینگ، مقدار اولیه ۶۰٪ برای GCHardLimitPercent در نظر گرفته شد، اما تاکید شد که این مقدار نیازمند بررسی و تنظیم دقیقتر بر اساس پروفایلینگ بیشتر است.runtimeconfig.json اعمال کرد. برای مثال، برای تنظیم سقف سخت به ۶۰٪ از حافظه در دسترس کانتینر:runtimeconfig.json:{
"runtimeOptions": {
"configProperties": {
"System.GC.HeapHardLimitPercent": 60
}
}
}string برای سریالسازی JSON به جای Stream، منجر به تولید حجم زیادی از اشیاء بزرگ (Large Object Heap - LOH) و فشار بر GC میشود. استفاده از APIهای مبتنی بر Stream در کتابخانههایی مانند System.Text.Json (مثلاً JsonSerializer.SerializeAsync(stream, ...) ) میتواند به طور چشمگیری تخصیص حافظه و تولید زباله را کاهش دهد. بررسی و استفاده از الگوهایی مانند Pooling اشیاء و استفاده از Span و Memory نیز در کاهش فشار حافظه مؤثر است.dotnet-counters (برای مشاهده معیارهای عملکردی زنده شامل اطلاعات GC)، dotnet-trace (برای جمعآوری ردپاهای اجرایی) و dotnet-gcdump (برای جمعآوری dumpهای GC) برای تحلیلهای عمیقتر بسیار کاربردی هستند.stream دریافت میشد را به یک نوع (type) تبدیل کردم (DTO)new میکردم تا برای دسته (batch) بعدی آماده بشهnew کردن لیست، ابتدا لیست موجود را clear و پس از اون GC رو هم صدا زدم. ولی این بار بعد از پایان عملیات فقط 500MB حافظه اشغال شده بود که باز هم افتضاح بودnullable تعریف کردم و بعد از اینکه 1000 آیتم به لیست اضافه میشد، ابتدا لیست را برابر null قرار دادم تا در حلقههای بعدی مجددا فرآیند افزودن آیتم به لیست انجام شود و...null اینقدر در کاهش مصرف حافظه کمک کرده؟ آیا میشه در فرآیندهای پردازشی سنگین که تعداد متغیرهای زیادی تعریف و مقدار دهی میشن، همه آنها را بصورت nullable تعریف کرد و آنچه که در انتهای پردازش در حافظه باقی میماند را به GC بسپاریم؟new کردن دوباره بازنویسی میکردید، لیست قبلی همچنان در حافظه باقی میماند، چون ممکن بود همچنان مرجعی به آن وجود داشته باشد (مثلاً در جایی از برنامه یا در stack/heap). این باعث میشد که حافظه بهمرور انباشته شود، بهخصوص در پردازشهای سنگین با تعداد زیادی شیء. وقتی لیست را با متد Clear خالی میکردید، فقط اشیاء داخل لیست (عناصر لیست) از لیست حذف میشدند، اما خود شیء لیست (ساختار دادهای آن) و برخی متادیتاها (مانند ظرفیت داخلی آرایه) همچنان در حافظه باقی میماندند. همچنین، اگر عناصر لیست خودشان اشیاء پیچیدهای (مانند DTOها) باشند، ممکن است مرجعهایی به آنها در جای دیگری از برنامه وجود داشته باشد، که مانع از جمعآوری کامل توسط GC میشود. به همین دلیل، حتی با فراخوانی دستی GC، حافظه آزادسازی کامل نشد و همچنان 500 مگابایت اشغال شده بود. وقتی لیست را null میکنید، مرجع به شیء لیست کاملاً قطع میشود. این کار به GC اجازه میدهد که نهتنها خود شیء لیست، بلکه تمام اشیاء داخل آن (مانند DTOها) را که دیگر مرجعی به آنها وجود ندارد، جمعآوری کند. این باعث شد که حافظه بهصورت مؤثرتری آزاد شود و در نهایت فقط 8 تا 10 مگابایت اشغال شود. دلیل این موفقیت این است که null کردن مرجع، به GC سیگنال واضحی میدهد که این شیء دیگر مورد نیاز نیست و میتواند بهطور کامل از حافظه حذف شود. GC در زبانهای مدرن مانند #C و Java بهگونهای طراحی شده است که بهصورت خودکار و بهینه حافظه را مدیریت کند. اگر شما بهصورت دستی مرجعها را null کنید، ممکن است در برخی موارد به GC فشار غیرضروری وارد کنید یا رفتار پیشبینیناپذیری ایجاد کنید. بهعنوان مثال، اگر متغیری هنوز در scope باشد، null کردن آن ممکن است تأثیر محدودی داشته باشد، چون GC همچنان ممکن است آن را نگه دارد تا scope به پایان برسد.private static IEnumerable<DataRowDto> GetDataStreamFromDatabase()
{
using (var connection = new SqlConnection("connection_string"))
{
connection.Open();
using (var command = new SqlCommand("SELECT Id, Data FROM YourTable", connection))
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
yield return new DataRowDto
{
Id = reader.GetInt32(0),
Data = reader.GetString(1)
};
}
}
}
}IEnumerable و yield return برای استریم کردن استفاده میکنیم. متد GetDataStreamFromDatabase از yield return استفاده میکند تا دادهها را بهصورت تنبل (lazy) تولید کند. این یعنی دادهها فقط زمانی که نیاز است (در زمان iteration) تولید میشوند، نه اینکه همهی ردیفها یکجا در حافظه بارگذاری شوند.