Goning GC'si unutiladigan qilib loyihalashtirilgan — aksariyat Go dasturlari hech qachon runtime.GC() ni chaqirmaydi, hech qachon GOGCni sozlamaydi va yig'ish haqida umuman o'ylamaydi, va bu ataylab shunday. Lekin "unutiladigan" degani "ahamiyatsiz" degani emas: GC'ning xatti-harakati ajratishga og'ir kod aslida qanchalik qimmatligini shakllantiradi, va ajratishga o'ylab yondashadigan dastur bilan yondashmaydigan dastur orasidagi farq real ishga emas, yig'ishga sarflangan CPU sifatida to'g'ridan-to'g'ri ko'rinadi. Ushbu bob GC aslida qanday ishlashi, qachon haqida o'ylashga arzishi va — kundalik hayotda foydaliroq — uni keraksiz ishga majburlamaydigan kodni qanday yozish haqida.
Goning GC'si bir vaqtda ishlaydigan (concurrent) tri-color mark-and-sweep GC: u dasturingizning ildizlaridan (global o'zgaruvchilar va faol goroutine stack'laridan havola qilingan qiymatlar) qaysi heap obyektlariga hamon erishish mumkinligini aniqlaydi va erishib bo'lmaydigan hamma narsani qaytarib oladi. Uch rang marking paytida obyektlar grafi bo'ylab progressni kuzatish uchun hisob vositasi: har bir obyekt oq (potensial keraksiz) boshlanadi, ildizlardan erishish mumkin bo'lgan obyektlar kulrang bo'ladi (tirikligi ma'lum, lekin o'z reference'lari hali tekshirilmagan), va kulrang obyektning barcha reference'lari kuzatilib, navbatda ular ham kulrang qilib belgilangach, u qora bo'ladi (tirikligi ma'lum, to'liq qayta ishlangan). Kulrang obyektlar qolmaganda, hamon oq bo'lgan hamma narsa erishib bo'lmaydigan va sweep qilinadi.
Bu GC'ni e'tiborga loyiq qiladigan jihat shuki, marking jarayonining deyarli barchasi dastur goroutine'lari bilan parallel (bir vaqtda) bajariladi, bitta uzoq davom etuvchi to'xtash paytida emas. GC har bir siklda ikki marta juda qisqa muddatga dastur ishini to'xtatadi (STW — stop-the-world): birinchi marta eng boshida — goroutine stack'larini skanerlab boshlang'ich ildizlar (roots) to'plamini aniqlash uchun, ikkinchi marta esa oxirida — mark bosqichini yakunlash uchun. Har ikki pauza odatda heap hajmiga bog'liq bo'lmagan holda millisekunddan ancha kam vaqt oladi. Bu esa Go dasturlarini kechikishga sezgir (low-latency) serverlar va xizmatlar uchun juda qulay qiladi.
Marking bosqichini ishlayotgan dastur bilan parallel bajarishning eng nozik jihati — ushbu jarayon davomida o'zgaruvchilarning pointer'lari qayta yozilishidir. Go buni write barrier yordamida hal qiladi: kompilyator pointer'larga qiymat yoziladigan joylarga kichik qo'shimcha kod qo'yadi va bu orqali yangi yaratilgan yoki ko'chirilgan havolalar GC e'tiboridan chetda qolib ketmasligini ta'minlaydi.
Sweeping — ya'ni oq rangda qolgan (keraksiz) obyektlardan xotirani bo'shatish ham birdaniga emas, dasturning keyingi xotira ajratish amallari bilan parallel ravishda bosqichma-bosqich amalga oshiriladi.
Yuqorida aytilganlarning hammasi GC algoritmini yillar davomida barqaror qolgan darajada tasvirlaydi — erishuvchanlik, tri-color marking, bir vaqtdagi ijro, write barrier'lar — va bularning hech biri to'g'ri bo'lishdan to'xtamadi. O'zgargan narsa uning ostidagi implementatsiya. Go 1.25 GC ichki qismlarining tajribaviy qayta ishlanishini, "Green Tea" laqabli, chiqardi — u aynan GC vaqtining katta ulushi aslida ketadigan kichik obyektlarni qanday belgilash va skanerlashni maqsad qilgan; u Go 1.26 da standart GC bo'ldi. Google'ning o'z o'lchovlari ish yukiga qarab overhead kamayishini 10-40% qilib ko'rsatadi, uni to'liqroq ishlata oladigan yangiroq amd64 chiplarda (Intel Ice Lake va undan keyingi, AMD Zen 4 va undan keyingi) yana taxminan 10% yaxshilanish bilan.
Bularning hech biri siz yozadigan narsani o'zgartirmaydi — bu yangi API emas, faqat yuqorida tasvirlangan ishni bajaradigan tezroq GC — aynan shu gap: ushbu bobdagi tri-color modeli hamon to'g'ri aqliy model, va GOGC/GOMEMLIMIT hamon avvalgi ma'nosini anglatadi. Agar GC bilan bog'liq regressiyani qidirayotganda yangi implementatsiyani chiqarib tashlash kerak bo'lsa (yoki Go 1.26+ toolchain'ni ishlatib, 1.24 yoki undan oldingi versiyada olingan benchmark'lar bilan solishtirayotgan bo'lsangiz), uni hamon GOEXPERIMENT=nogreenteagc bilan o'chirish mumkin — garchi bu qochish yo'li eski implementatsiya butunlay olib tashlanganda kelajakdagi relizda yo'qolishi kutilsa ham.
Sukut bo'yicha GC yangi siklni oldingi yig'ish tugagandan beri heap qanchalik o'sganiga qarab qo'zg'atadi, bu GOGC muhit o'zgaruvchisi bilan boshqariladi (standart 100, ya'ni "tirik heap oxirgi siklga nisbatan 100% o'sganda keyingi GC'ni qo'zg'at" — taxminan, heap ikki baravar bo'lganda). runtime.GC() chaqiruvi shu jadvaldan tashqari zudlik bilan, to'liq yig'ishni majburlaydi, benchmark'lar va diagnostikada foydali, lekin oddiy ilova kodida deyarli hech qachon xohlanmaydigan narsa, chunki u GC'ning o'z sur'at qarorlarini bekor qiladi.
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("heap in use: %d KB, GC cycles: %d\n", m.HeapInuse/1024, m.NumGC)
Go 1.19 ikkinchi, to'ldiruvchi boshqaruvni qo'shdi: GOMEMLIMIT, yumshoq xotira chegarasi (baytlarda yoki GOMEMLIMIT=512MiB kabi qo'shimcha bilan), u runtime'ga GOGC o'sish nisbatidan mustaqil ravishda, "shuncha jami xotiradan oshmaslik uchun kerak bo'lsa, agressivroq yig'" deydi. Bu aynan qattiq xotira chegarasiga ega konteynerlashtirilgan muhitlarda muhim, bunda GOGCning standart o'sishga asoslangan trigger'i haqiqatda qancha xotira mavjudligini bilmaydi va nisbatga asoslangan trigger ishga tushishidan oldin heap'ni konteynerning OOM chegarasiga qadar — yoki undan o'tib — o'stirishi mumkin. GOGC (oddiy, throughput'ga yo'naltirilgan sur'at tugmasi sifatida) va GOMEMLIMIT (konteyneringizning haqiqiy xotira chegarasiga moslangan xavfsizlik klapani sifatida, odatda zaxira qoldirish uchun qattiq chegaradan biroz pastroq belgilanadi) ikkalasi bilan ishlash cgroup'lar yoki Kubernetes resurs chegaralari ostida ishlaydigan xizmatlar uchun hozirgi tavsiya etilgan amaliyot.
GOGC=100 GOMEMLIMIT=900MiB ./myservice # 1GiB konteyner chegarasidan past shift
"GC bosimi" (GC pressure) — dasturingiz bajargan foydali ish birligiga nisbatan qancha vaqtinchalik keraksiz obyektlarni (garbage) heap'da yaratishini anglatadi. Uni kamaytirishning eng samarali va to'g'ri yo'li — xotirada kamroq obyekt ajratish (allocation) dir. GOGC yoki GOMEMLIMIT kabi parametrlarni sozlash yig'ishning qachon boshlanishini va uning CPU hamda xotira o'rtasidagi muvozanatini boshqaradi, lekin xotirada yaratiladigan axlatning umumiy hajmini kamaytirmaydi — bu faqat dastur kodini to'g'ri yozish orqali hal qilinadi.
GC bosimini kamaytirishning eng samarali usuli — kamroq xotira ajratish (kamroq allocate qilish).
Yakuniy o'lcham ma'lum bo'lganda slice va map'larni oldindan o'lchamlang. Slice'ning joriy sig'imidan oshgan har bir append qayta ajratish va shu paytgacha bo'lgan hamma narsani nusxalashni qo'zg'atadi; buni butunlay o'tkazib yuborish uchun makega sig'im maslahatini oldindan bering:
results := make([]Result, 0, len(items)) // bitta ajratish, boshidanoq to'g'ri o'lchamda
for _, item := range items {
results = append(results, process(item)) // hech qachon qayta ajratmaydi
}
Xuddi shu map'larga make(map[K]V, n) orqali tegishli.
Har chaqiruvda yangi buferlarni ajratish o'rniga, ayniqsa qizg'in yo'llarda, ularni qayta ishlating. sync.Pool buning uchun standart vosita — runtime GC sikllari orasida bo'shatishi mumkin bo'lgan vaqtinchalik, qayta ishlatiladigan obyektlar puli, aynan shu sababli u qisqa muddatli scratch buferlar uchun mos va davom etishini xohlaydigan narsa uchun noto'g'ri:
var bufferPool = sync.Pool{
New: func() any { return make([]byte, 0, 1024) },
}
func process(data []byte) {
buf := bufferPool.Get().([]byte)
defer bufferPool.Put(buf[:0]) // pool'ga qaytarishdan oldin uzunlikni tiklash
// buf dan foydalanish...
}
sync.Poolni uzoq yashaydigan obyektlar uchun — masalan, har foydalanuvchi sessiya struct'i — ishlatish yordam bermaydi, chunki pool aks holda qayta-qayta aylantiriladigan obyektlar uchun ajratish narxini taqsimlash uchun mavjud, uzoq yashaydigan obyekt esa hech qachon qayta ajratilmagan bo'lardi; siz shunchaki hech narsa uchun pool hisob-kitobi overhead'ini qo'shgan bo'lasiz.
Qayta-qayta += bilan birlashtirish o'rniga strings.Builderni afzal ko'ring — 16-bobda chuqur yoritilgan — chunki satrdagi har bir += butunlay yangi satr ajratadi va eskisini darhol tashlab yuboradi.
Pointer'ga boy ma'lumotlar tuzilmalari haqida ataylab bo'ling. GC'ning mark bosqichi kuzatishi kerak bo'lgan har bir pointer sikl boshiga qo'shimcha ish, va pointer'lar bilan bog'langan kichik, alohida heap'da ajratilgan obyektlarning katta grafi (masalan, mayda tugunlardan iborat klassik bog'langan ro'yxat) xuddi shu ma'lumotni uzluksiz slice'ga joylashtirishdan sezilarli ko'proq yuk beradi, u yerda GC minglab alohida ajratish o'rniga slice'ning asosiy massivini yagona ajratish sifatida hisobga olishi kerak:
type Node struct {
Value int
Next *Node // har bir tugun uchun bitta heap obyekti va kuzatiladigan bitta pointer
}
qarshisida xuddi shu ro'yxatni ulashilgan slice'ga indekslar sifatida tasvirlash, bunda "pointer'lar" GC kuzatishi shart bo'lmagan oddiy butun sonlar:
type Node struct {
Value int
Next int // pointer emas, indeks — GC uchun ko'rinmas
}
Bu muayyan kelishuvga profillash GC bosimi haqiqiy to'siq ekanini ko'rsatgandan keyingina murojaat qilish foydali — ma'lumotlar modelini uning muhimligiga dalil bo'lmasdan GC-do'st qilib qayta qurish klassik erta optimallashtirish.
22-bob escape analysis'ni chuqur yoritadi, lekin aloqani bu yerda ham takrorlash foydali: har bir ajratish ham dastlab GC ko'radigan heap trafigiga aylanmaydi. Kompilyator har bir qiymat uchun uning yashash muddati butunlay funksiya chaqiruvi ichida cheklanganini isbotlay olishini hal qiladi — agar isbotlay olsa, qiymat stack'ga qo'yiladi va funksiya qaytganda avtomatik qaytarib olinadi, GC umuman ishtirok etmaydi. Faqat "escape qiladigan" qiymatlar (pointer bilan qaytarilgan, funksiyadan uzoqroq yashaydigan closure ushlagan, uzoqroq yashash muddatli joyga saqlangan yoki kompilyator uchun mulohaza qilish uchun juda katta/dinamik o'lchamli) haqiqatan GC'ning muammosiga aylanadi. go build -gcflags="-m" kompilyator qaysi ajratishlar escape qilishi kerak deb hal qilganini aniq ko'rsatadi, va u ko'pincha to'g'ridan-to'g'riroq tutqich: nima escape qilishini kamaytirish GC bosimini manbada kamaytiradi, yuqoridagi pooling yoki oldindan o'lchamlash usullari o'yinga kirishidan ham oldin.
Go 1.24 dan ikkita qo'shimchani bilish foydali, garchi aksariyat dasturlarga ular hech qachon kerak bo'lmasa ham: ular GC'ga shunchaki bo'ysunish o'rniga uni kuzatmoqchi yoki unga munosabat bildirmoqchi bo'lgan kodning tor, lekin haqiqiy holati uchun mavjud.
weak paketi weak pointer'larni beradi: weak.Pointer[T] qiymatga uni tirik ushlab turmasdan ishora qiladi. Agar qiymatga boshqa hech narsa strong reference ushlab turmasa, GC uni qaytarib olishi mumkin, va shundan keyin weak pointer'ning Value() metodi nol qiymat qaytara boshlaydi. Bu o'zi hech qachon yig'ilmaslikning sababi bo'lmasligi kerak bo'lgan keshlar va kanonikalashtirish map'lari uchun qurilish bloki — strong reference'larni ushlab turadigan kesh, keshlagan hamma narsaga oxirgi qolgan reference bo'lib qolgan zahoti o'z maqsadini yo'qotadi:
cache := map[string]weak.Pointer[BigObject]{}
func get(key string) *BigObject {
if wp, ok := cache[key]; ok {
if v := wp.Value(); v != nil {
return v // hamon tirik; unga boshqa kimdir ham reference ushlab turibdi
}
}
v := loadBigObject(key)
cache[key] = weak.Make(v)
return v
}
runtime.AddCleanup(obj, cleanup) qiymatga erishib bo'lmay qolganda biror narsani ishga tushirish kerak bo'lgan kod uchun eski, o'tkirroq qirrali runtime.SetFinalizerni almashtiradi — masalan, Go qiymati o'ragan fayl deskriptorini yoki native handle'ni bo'shatish. U SetFinalizerning bir necha taniqli tuzoqlarini tuzatadi: obyekt uchun bir nechta cleanup ro'yxatdan o'tkazish mumkin (finalizer'lar faqat bittasiga ruxsat berardi, oldingi ro'yxatdan o'tkazishni jimgina bosib), cleanup faqat obyekt boshiga emas, ichki pointer'ga ham biriktirilishi mumkin, cleanup'lar finalizer'lar kabi reference sikliga qatnashgan obyektlarning yig'ilishiga to'sqinlik qilmaydi, va cleanup ro'yxatdan o'tkazish obyektning o'zi yig'ishga yaroqli bo'lish vaqtini kechiktirmaydi. Agar hamon SetFinalizerni chaqiradigan kodni qo'llab-quvvatlayotgan bo'lsangiz, AddCleanup xuddi shu ishni bajaradigan yangi kod uchun oddiy, xavfsizroq almashtirish.
GODEBUG=gctrace=1 runtime'ga har bir yig'ish sikli uchun stderr'ga bitta qator chop etishni buyuradi:
gc 1 @0.001s 2%: 0.010+0.20+0.003 ms clock, 0.040+0.20/0.10/0.20+0.012 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
O'rtadagi raqamlarni chapdan o'ngga o'qing: 4->4->0 MB — sikldan oldingi heap o'lchami, marking tugagach darhol heap o'lchami va sweeping'dan keyin hamon tirik qolgan heap o'lchami; 5 MB goal — GOGC keyingi sikl uchun hisoblagan trigger chegarasi. Bu chiqishni kuzatish — yoki, amaliyroq, xuddi shu raqamlarni production'da runtime.ReadMemStats yoki runtime/metrics orqali metrik sifatida eksport qilish — GC sikllari vaqt o'tishi bilan tez-tez yoki qimmatroq bo'layotgan dasturni sezish usuli, bu odatda GC'ning o'zi noto'g'ri ishlashidan emas, balki o'sib borayotgan tirik heap'ning (aslida sizib chiqish, hech qanday xavfli xotira ishtirok etmasa ham — shunchaki tashlab yuborilishi kerak bo'lgan, lekin tashlab yuborilmagan reference'lar) belgisi.
Tez trace qatoridan ortiq narsa uchun net/http/pprofning heap profili haqiqiy vosita — u tirik heap'ingiz va ajratish tezligingizni tashkil etuvchi ajratishlar uchun qaysi chaqiruv joylari javobgar ekanini aniq ko'rsatadi, bu "GC sekin tuyulyapti"ni optimallashtirish uchun aniq, amaliy funksiyalar ro'yxatiga aylantiradi:
import _ "net/http/pprof"
func main() {
go http.ListenAndServe("localhost:6060", nil)
// ...
}
va u yerdan http://localhost:6060/debug/pprof/heap ga kirish (yoki go tool pprof bilan olib kelish). 57-bob pprofga asoslangan unumdorlik ishini to'liq yoritadi.
Ushbu bobdagi har bir usul faqat sizda GC sizga haqiqatan biror narsaga tushayotganiga dalil bo'lgandagina qo'llash foydali — pprofdan, gctracedan, production'dagi kechikish regressiyasidan. Standart sozlamalar (GOGC=100, GOMEMLIMITsiz) dasturlarning aksariyat ko'pchiligi uchun yaxshi sozlangan, va haqiqiy muammoni o'lchashdan oldin ajratish andozalarini oldindan optimallashtirish yoki sync.Poolga murojaat qilish murakkablik — pool boshqaruvi kodi, pointer'li versiyadan o'qish qiyinroq indeksga asoslangan ma'lumotlar tuzilmalari — qo'shadi, buning evaziga isbotlangan foyda esa yo'q. Optimallashtirish emas, sukut bo'yicha deb qarash foydali bo'lgan bitta istisno: hajmi allaqachon ma'lum bo'lganda slice va map'larni oldindan o'lchamlash, buni yozish hech narsa turmaydi va u haqiqatan ichiga qancha qo'yishni bilganingizdan keyin makeni chaqirishning shunchaki to'g'ri usuli.
Xotira boshqaruvi boshidan oxirigacha ko'rib chiqildi — qiymatlar qayerda yashashi, pointer'lar va ulashish mexanizmlari, hamda GC'ning xotirani tozalash jarayoni. Endi esa tilning yana bir muhim ustuniga o'tamiz: Go tilida merossiz (without inheritance) polimorfizmga erishish — kichik va avtomatik (implicit) implementatsiya qilinadigan interfeyslar olami.