4.21 زباله جمع کن (garbage collector)

4.21 زباله جمع کن (garbage collector)

یکی از جذابیت های زبان های برنامه نویسی جدید قابلیت زباله جمع کن (garbage collector) ایجاد شده در آنها است. بطور خلاصه مدیریت و آزاد سازی منابع اختصاص داده شده از حافظه را که بصورت خودکار در زبان های برنامه نویسی مدرن انجام می شود را garbage collector می شناسیم.

مدیریت منابع حافظه که بسیار حساس و در مواقعی زمان بر است توسط برخی زبان ها بصورت توکار پشتیبانی می شود و این کار به افزایش سرعت و همچنین کاهش خطا در زمان و پروسه تولید نرم افزار می شود. با تمام این تفاسیر مواقعی است که برنامه نویس می باید خودش مدیریت حافظه را در دست گرفته و پیش از بروز شرایط بحرانی اقدام به آزاد سازی حافظه و یا منابع اختصاص یافته نماید. خبر خوب اینکه در زبان Go مدیریت منابع حافظه نسبت به زبان های دیگر ساده تر و راحتر است ولی در این نوشته سعی می کنیم تا درک خوبی از مدیریت منابع و نحوه عملکرد آن داشته باشید بنابر این در ادامه با یک مثال که نیاز به بررسی کد در سطوح پایین دارد می پردازیم.

استفاده از uprobes #

برای بررسی وضعیت حافظه ما از uprobes استفاده می کنیم این بسته امکان بسیاری در اختیارمان قرار می دهد برای مثال نیازی به تغییر کد برنامه نمی باشد و برای برنامه های درحال اجرا نیز می توان استفاده نمود.

مراحل garbage collection #

مکانیزم garbage collection در زبان go بصورت هم رونده یا concurrent در کنار برنامه ما اجرا می شود که همین قابلیت دلیل عدم توقف یا مکث برنامه در زمان پاکسازی حافظه است و دو مرحله کلی را برای پاکسازی حافظه اجرا می کند.

1- مرحله Mark phase در این مرحله GC اشیاء و متغیرهای مرده دربرنامه که بخشی از حافظه را اشغال کرده‌اند را جستجو و شناسایی می کند.

2- مرحله Sweep phase در این مرحله اشیائی که در فاز قبل نشانه گذاری شده‌اند بصورت فیزیکی از حافظه دور ریخته می شود.

GC
تصویر بالا مراحل مختلف شناسایی و پاکسازی را نشان می دهد

در ادامه قطعه کد ذیل را داریم که در واقع یک عمل ساده درخواست و پاسخ به یک آدرس وب است

 1http.HandleFunc("/allocate-memory-and-run-gc", func(w http.ResponseWriter, r *http.Request) {
 2
 3   arrayLength, bytesPerElement := parseArrayArgs(r)
 4
 5   arr := generateRandomStringArray(arrayLength, bytesPerElement)
 6
 7   fmt.Fprintf(w, fmt.Sprintf("Generated string array with %d bytes of data\n", len(arr) * len(arr[0])))
 8
 9   runtime.GC()
10
11   fmt.Fprintf(w, "Ran garbage collector\n")
12
13 })

در این قطعه کد یک متغیر که محتوای آن آرایه‌ای از رشته ها است تولید می شود و در پایان با صدا زدن تابع runtime.GC از GC درخواست می کنیم که حافظه را پاکسازی کند دقت کنید که در این قسمت از کد شی arr دیگر مورد استفاده قرار نمی گیرد و از نظر طول عمر مرده به حساب می آید و GC اقدام به پاکسازی فظای اشغال شده توسط این متغیر را می کند.

اما داستان به این سادگی هم نیست برای مثال مهمترین ویژگی GC این است که بصورت خودکار در زمان های مناسب وارد عمل می شود و اقدام به رها سازی حافظه می نماید باید توجه داشته باشیم که خود عملیات GC بدون سربار به سیستم نیست و شامل موارد ذیل است

GC Pause Time: عملیات پاکسازی بصورت همزمان با اجرای برنامه انجام می شود ولیکن در برنامه های سنگین بصورت لحظه ای می شود متوجه سربار زمان توقف برنامه برای عملیات پاکسازی شد. هرچند که برای کاربران عادی مشهود نباشد.

Memory Allocation: جهت نشانه گذاری اشیاء مرده نیز نیاز به تخصیص حافظه است.

CPU usage: تمامی فعالیت های انجام شده نیاز به پردازش دارد که قاعدتا به پردازنده سربار اضافی تحمیل خواهد کرد.

GC trigger threshold این قابلیت در زبان go قابل تنظیم است و اجازه می دهد تا یک آستانه برای عملیات پاکسازی در نظر بگیریم که به بصورت درصد مشخص می شود. چنانچه درصد استفاده از حافظه از مقدار تعیین شده در متغیر آستانه بیشتر شود عملیات پاکسازی اجرا خواهد شد که این به نوبه خود چالش برانگیز است برای مثال اگر مقدار آستانه را زیاد تعریف کنیم ممکن است برنامه با کمبود منابع روبرو شود و یا درصورت تعیین مقدار پایین منابعی مانند پردازنده بیش از حد درگیر خواهند شد. Memory Pressure: مواقعی که برنامه به لحاظ منابع حافظه تحت فشار و محدودیت است در این زمان GC بطور متوالی اجرا خواهد شد که می تواند دلیل توقف برنامه اصلی شود

مانیتور نحوه کار GC #

در ادامه میخواهیم توابع ذیل که در runtime موجود است را به کمک uprobes زیر نظر بگیریم تا علاوه بر مقادیر تولید شده در آنها، فرآیند آنها نیز برایمان قابل درک باشد.

  1. GC تابع اجرای عملیات پاکسازی
  2. gcWaitOnMark تابع تشخیص اشیا جهت رها سازی
  3. gcSweep تابع رها سازی منابع

دقت داشته باشید که تنظیم و اجرا uprobes نیاز به اطلاع بیشتر دارد که در حوصله این نوشتار نیست و متوانید با جستجو در اینترنت به منابع مورد نیاز دسترسی داشته باشید

1$ curl '127.0.0.1/allocate-memory-and-run-gc?arrayLength=10&bytesPerElement=20'
2
3Generated string array with 200 bytes of data
4
5Ran garbage collector

پس از اجرای کوئری بالا بر روی آدرس برنامه خود نتایج ذیل حاصل می شود

table

نکته قابل توجه اینجاست که تابع gcWaitOnMark دو بار در طول عملیات پاکسازی اجار می شود که بار اول جهت اعتبار سنجی منابع نشانه گذاری شده قسمت قبل است.

با این توضیحات حال می خواهیم به بررسی چند مثال ساده بپردازیم تا ببینیم در چه مواقعی بهتر است برنامه نویس در بخش های مناسب خود اقدام به پاکسازی حافظه نماید

 1
 2package main
 3
 4import (
 5	"fmt"
 6	"runtime"
 7)
 8
 9func main() {
10	// Allocate some memory for the program to use
11	s := make([]string, 0, 100000)
12	for i := 0; i < 100000; i++ {
13		s = append(s, "hello, world")
14	}
15
16	// Print the initial memory usage
17	var m runtime.MemStats
18	runtime.ReadMemStats(&m)
19	fmt.Println("Initial HeapAlloc: ", m.HeapAlloc)
20
21	// Trigger the garbage collector
22	runtime.GC()
23
24	// Print the memory usage after the garbage collector has run
25	runtime.ReadMemStats(&m)
26	fmt.Println("After GC HeapAlloc: ", m.HeapAlloc)
27
28	// Release the memory
29	s = nil
30	// Trigger the garbage collector
31	runtime.GC()
32	// Print the memory usage after the garbage collector has run
33	runtime.ReadMemStats(&m)
34	fmt.Println("After release HeapAlloc: ", m.HeapAlloc)
35}

خروجی برنامه :

1# go run main.go 
2Initial HeapAlloc:  1654512
3After GC HeapAlloc:  37872
4After release HeapAlloc:  37872

در کد بالا به کمک یک حلقه در هربار اجرای آن مقداری را به رشته قبلی خود اضافه نموده ایم و بعد از آن مقدار فضای اشغال شده توسط رشته ما در حافظه را نمایش می دهیم در ادامه به کمک runtime.GC حافظه را تخلیه می کنیم و در انتها بررسی می کنیم که آیا مقدار متغیر ما بصورت واقعی تخلیه شده است که نتایج خروجی موارد فوق را تائید می نماید. به یاد داشته باشید که منابع سیستم همواره محدود می باشد و در شرایط این چنینی می بایست خود برنامه نویس با تشخیص درست اقدام به تخلیه حافظه نماید.

استفاده از GODEBUG #

در مواقعی نیاز است تا GC را بدونه کتابخانه و ابزار اضافی و فقط با قابلیت‌های داخلی خود زبان go بررسی نمائیم که در چنین شرایطی بهتر است از GODEBUG استفاده کنیم.

 1package main
 2
 3import (
 4	"fmt"
 5	"runtime"
 6	"time"
 7)
 8
 9func printStats(mem runtime.MemStats) {
10	runtime.ReadMemStats(&mem)
11	fmt.Println("mem.Alloc:", mem.Alloc)
12	fmt.Println("mem.TotalAlloc:", mem.TotalAlloc)
13	fmt.Println("mem.HeapAlloc:", mem.HeapAlloc)
14	fmt.Println("mem.NumGC:", mem.NumGC)
15	fmt.Println("-----")
16}
17
18func main() {
19	var mem runtime.MemStats
20	printStats(mem)
21
22	for i := 0; i < 10; i++ {
23		s := make([]byte, 100000000)
24		if s == nil {
25			fmt.Println("Operation failed!")
26		}
27	}
28	printStats(mem)
29
30	for i := 0; i < 10; i++ {
31		s := make([]byte, 100000000)
32		if s == nil {
33			fmt.Println("Operation failed!")
34		}
35		time.Sleep(5 * time.Second)
36	}
37	printStats(mem)
38
39}

خروجی کد:

 1# go run main.go 
 2mem.Alloc: 48256
 3mem.TotalAlloc: 48256
 4mem.HeapAlloc: 48256
 5mem.NumGC: 0
 6-----
 7mem.Alloc: 100045200
 8mem.TotalAlloc: 1000128496
 9mem.HeapAlloc: 100045200
10mem.NumGC: 9
11-----
12^Csignal: interrupt

در کد فوق برنامه بدون دیباگ خروجی فوق را تولید نموده است که مقدار منابع مصرف شده در هر چرخه را نمایش می دهد اما اگر بخواهیم برنامه را با دستور GODEBUG اجرا کنیم خروجی متفاوت خواهد بود به یاد داشته باشید ما در اینجا میخواهیم مقادیر تولید شده که بصورت key-value است و با علامت کاما از هم جدا شده اند و فقط برای GC را بررسی کنیم درحالی که می توان بخش های دیگر برنامه را نیز با سوئیچ های مختلف دیباگ کرد بنابراین اینبار برنامه را با دستور ذیل اجرا می کنیم

 1# GODEBUG=gctrace=1 go run main.go a
 2gc 1 @0.019s 1%: 0.014+2.4+0.001 ms clock, 0.014+0.33/0/0+0.001 ms cpu, 4->4->0 MB, 5 MB goal, 1 P
 3gc 2 @0.050s 3%: 0.027+5.1+0.002 ms clock, 0.027+0.37/1.0/0+0.002 ms cpu, 4->4->1 MB, 5 MB goal, 1 P
 4gc 3 @0.089s 2%: 0.067+3.3+0.002 ms clock, 0.067+0.66/0/0+0.002 ms cpu, 4->4->1 MB, 5 MB goal, 1 P
 5gc 4 @0.128s 2%: 0.032+2.6+0.003 ms clock, 0.032+0.82/0/0+0.003 ms cpu, 4->4->1 MB, 5 MB goal, 1 P
 6gc 5 @0.153s 2%: 0.046+5.1+0.002 ms clock, 0.046+0.81/0/0+0.002 ms cpu, 4->5->1 MB, 5 MB goal, 1 P
 7gc 6 @0.175s 3%: 0.030+11+0.002 ms clock, 0.030+1.4/0.16/0+0.002 ms cpu, 4->5->1 MB, 5 MB goal, 1 P
 8gc 7 @0.224s 2%: 0.027+2.4+0.003 ms clock, 0.027+0.63/0/0+0.003 ms cpu, 4->5->2 MB, 5 MB goal, 1 P
 9# command-line-arguments
10gc 1 @0.004s 17%: 0.009+2.4+0.002 ms clock, 0.009+1.3/0/0+0.002 ms cpu, 4->6->5 MB, 5 MB goal, 1 P
11gc 2 @0.036s 16%: 0.014+8.7+0.004 ms clock, 0.014+4.0/2.2/0+0.004 ms cpu, 9->9->8 MB, 11 MB goal, 1 P
12mem.Alloc: 48128
13mem.TotalAlloc: 48128
14mem.HeapAlloc: 48128
15mem.NumGC: 0
16-----
17gc 1 @0.007s 1%: 0.011+0.11+0.002 ms clock, 0.011+0.10/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
18gc 2 @0.054s 0%: 0.030+0.13+0.002 ms clock, 0.030+0.12/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
19gc 3 @0.106s 0%: 0.023+0.12+0.002 ms clock, 0.023+0.12/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
20gc 4 @0.141s 0%: 0.023+0.15+0.004 ms clock, 0.023+0.15/0/0+0.004 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
21gc 5 @0.185s 0%: 0.021+0.12+0.001 ms clock, 0.021+0.11/0/0+0.001 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
22gc 6 @0.221s 0%: 0.023+0.22+0.002 ms clock, 0.023+0.22/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
23gc 7 @0.269s 0%: 0.025+0.12+0.001 ms clock, 0.025+0.12/0/0+0.001 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
24gc 8 @0.311s 0%: 0.032+0.33+0.002 ms clock, 0.032+0.32/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
25gc 9 @0.350s 0%: 0.022+0.10+0.006 ms clock, 0.022+0.097/0/0+0.006 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
26gc 10 @0.390s 0%: 0.021+0.11+0.005 ms clock, 0.021+0.10/0/0+0.005 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
27mem.Alloc: 100045256
28mem.TotalAlloc: 1000128368
29mem.HeapAlloc: 100045256
30mem.NumGC: 9
31-----

در نگاه اول خروجی کد بالا کمی گند به نظر می رسد که سعی می کنیم در ذیل آنها را توضیح دهیم

خروجیتوضیح
gc 1شماره پاکسازی که در هربار اجرای عملیات پاکسازی بصورت خودکار به آن اضافه می شود
@0.007sزمان اجرای پاکسازی بعد از شروع به کار برنامه
0%درصد منابع پردازشی استفاده شده بعد از اجرای برنامه
0.011+0.11+0.002 ms clockمقدار این متغیر متشکل از چند مقدار است که بصورت ذیل محاسبه می شود Tgc = Tseq + Tmark + Tsweep
95->95->0 MBاین متغیر نیز چند مقداری است و مقادیر اول نشانگر عملکرد حافظه قبل از اجرای پاکسازی ، دوم بعد از اجرای پاکسازی و سوم مقدار پشته است
96 MB goalاندازه پشته برنامه مورد نظر
1 Pتعداد پردازنده مورد استفاده شده

Tseq: زمان توقف گوروتین های کاربر
Tmark: زمان مورد استفاده جهت فاز mark
Tsweep: زمان مورد استفاده جهت فاز sweep