मेमोरी लीक (memory leak) — वह स्थिति जब एप्लिकेशन उन ऑब्जेक्ट्स द्वारा घेरी गई मेमोरी को मुक्त नहीं करता जिनकी अब आवश्यकता नहीं है। मोबाइल डेवलपमेंट में यह विशेष रूप से महत्वपूर्ण है: सीमित heap और swap की अनुपस्थिति OutOfMemoryError और एप्लिकेशन क्रैश का कारण बनती है। Purdue University (2022) के अनुसार, Google Play में 35% Android एप्लिकेशन में कम से कम एक मेमोरी लीक होता है। आइए सामान्य परिदृश्यों, निदान उपकरणों और समाधान विधियों पर चर्चा करें।
मुख्य बिंदु
मेमोरी लीक — वह स्थिति जब आवंटित मेमोरी प्रोग्राम द्वारा ऑब्जेक्ट की आवश्यकता न होने के बाद भी सिस्टम को वापस नहीं की जाती। कचरा संग्रहकर्ता ऐसे ऑब्जेक्ट को जीवित मानता है क्योंकि GC Root से एक सक्रिय संदर्भ श्रृंखला उसकी ओर इशारा करती है।
Java/Kotlin में, कचरा संग्रहकर्ता स्वचालित रूप से काम करता है, लेकिन यह निर्धारित नहीं कर सकता कि कोई ऑब्जेक्ट तार्किक रूप से अनावश्यक है यदि उसका तकनीकी संदर्भ मौजूद है। डेवलपर को अनावश्यक कनेक्शन को स्पष्ट रूप से तोड़ना चाहिए। Swift/Objective-C में, ARC स्वचालित रूप से संदर्भ गिनता है, लेकिन retain cycles काउंटर को शून्य होने से रोकती हैं।
लीक का मुख्य खतरा संचयी प्रभाव है। प्रत्येक लीक थोड़ी मात्रा में मेमोरी खपत करता है, लेकिन बार-बार स्क्रीन परिवर्तन (स्क्रीन रोटेशन, Activity खोलना/बंद करना) के साथ लीक जमा होते रहते हैं जब तक कि heap सीमा समाप्त न हो जाए।
लीक — ऑब्जेक्ट कोड के लिए अप्राप्य है लेकिन GC द्वारा हटाया नहीं गया। ब्लोट — ऑब्जेक्ट तार्किक रूप से आवश्यक है लेकिन अत्यधिक मात्रा में संग्रहीत है। ब्लोट का उदाहरण: 30 MB के कार्य सेट के साथ 100 MB की इमेज कैश। दोनों समस्याएं OOM की ओर ले जाती हैं, लेकिन कारण और उपचार के तरीके अलग हैं।
ART (Android Runtime) समवर्ती संघनन के साथ पीढ़ीगत कचरा संग्रह का उपयोग करता है। मेमोरी को युवा पीढ़ी (Young), पुरानी पीढ़ी (Old) और बड़े ऑब्जेक्ट्स (Large) में विभाजित किया गया है। कई GC चक्रों से बचने वाले ऑब्जेक्ट्स Old generation में चले जाते हैं, जहाँ संग्रह कम बार होता है — इससे सामान्य चक्र तेज होते हैं।
GC तब शुरू होता है जब heap एक निश्चित अधिभोग सीमा (आमतौर पर 75-85%) तक पहुँचता है। GC के दौरान, एप्लिकेशन के सभी थ्रेड रुक जाते हैं (STW — Stop The World)। जितने अधिक जीवित ऑब्जेक्ट, उतना लंबा विराम। लीक जीवित ऑब्जेक्ट्स की संख्या बढ़ाते हैं, GC विराम को लंबा करते हैं।
संग्रहकर्ता GC Roots से ग्राफ को ट्रैवर्स करके जीवित ऑब्जेक्ट्स निर्धारित करता है: स्थैतिक फ़ील्ड, सक्रिय थ्रेड्स के स्टैक वेरिएबल, JNI संदर्भ। इन जड़ों से संदर्भों के माध्यम से पहुँचा जा सकने वाला कोई भी ऑब्जेक्ट जीवित माना जाता है — भले ही डेवलपर जानता हो कि इसकी अब आवश्यकता नहीं है।
// उदाहरण: GC Root के रूप में स्थैतिक संग्रह — स्थायी लीक
object GlobalHolder {
val listeners = mutableListOf<WeakReference<Any>>()
}
class LeakingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
GlobalHolder.listeners.add(WeakReference(this))
// WeakReference GC को नहीं रोकता — सही व्यवहार
}
}
WeakReference समस्या का समाधान करता है: GC जीवित ऑब्जेक्ट्स निर्धारित करते समय कमजोर संदर्भों को अनदेखा करता है। यदि किसी ऑब्जेक्ट पर केवल कमजोर संदर्भ शेष हैं, तो वह निकटतम GC चक्र में एकत्र किया जाएगा।
Activity Context — Android में सबसे व्यापक लीक परिदृश्य। यदि सिंगलटन, स्थैतिक फ़ील्ड या लंबे समय तक चलने वाली सेवा Activity Context का संदर्भ रखती है, तो सभी Views सहित पूरी Activity GC द्वारा एकत्र नहीं की जा सकती। समाधान: लंबे समय तक चलने वाले ऑब्जेक्ट्स के लिए Application Context का उपयोग करें।
Handler और भेजे गए संदेश — Handler.postDelayed(runnable, delay) Main Looper कतार में एक संदेश डालता है। यदि विलंब समाप्त होने से पहले Activity नष्ट हो जाती है, तो संदेश अभी भी कतार में है और Runnable → अनाम वर्ग → बाहरी वर्ग (Activity) के माध्यम से संदर्भ रखता है।
class SafeActivity : AppCompatActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private val callback = Runnable { /* update UI */ }
override fun onResume() {
super.onResume()
mainHandler.postDelayed(callback, 5000)
}
override fun onPause() {
mainHandler.removeCallbacks(callback) // अनिवार्य: कतार साफ़ करें
super.onPause()
}
}
आंतरिक वर्ग — एक गैर-स्थैतिक आंतरिक वर्ग में बाहरी वर्ग के इंस्टेंस का अंतर्निहित संदर्भ होता है। यदि बाहरी वर्ग Activity है और आंतरिक वर्ग कहीं बाहर पास किया जाता है (उदाहरण के लिए, RecyclerView.Adapter में), तो Activity एकत्र नहीं की जा सकती।
Android Studio Memory Profiler — वास्तविक समय heap निगरानी के लिए अंतर्निहित उपकरण। उपयोग की गई मेमोरी का ग्राफ़, आवंटनों की संख्या और प्रकार के अनुसार ऑब्जेक्ट दिखाता है। MAT में विश्लेषण के लिए heap dump रिकॉर्ड करने और HPROF प्रारूप में निर्यात करने की अनुमति देता है।
Eclipse MAT (Memory Analyzer Tool) — डेस्कटॉप heap dump विश्लेषक। स्वचालित रूप से Leak Suspects रिपोर्ट बनाता है, जो सबसे बड़े retained size वाले ऑब्जेक्ट्स को उजागर करता है और प्रत्येक संदिग्ध ऑब्जेक्ट के लिए संभावित GC root chain सुझाता है।
Xcode Memory Graph Debugger — iOS के लिए। एप्लिकेशन को रोकता है और ऑब्जेक्ट ग्राफ़ को विज़ुअलाइज़ करता है। Retain cycles को लाल रंग में हाइलाइट किया जाता है; किसी भी ऑब्जेक्ट पर क्लिक करके उसकी retain count और संदर्भ देखे जा सकते हैं।
| उपकरण | क्षमताएँ | जटिलता |
|---|---|---|
| Memory Profiler | रीयल-टाइम ग्राफ़, heap dump, Object Allocation Tracking | निम्न |
| Eclipse MAT | Dominator tree, Leak Suspects, OQL क्वेरी | मध्यम |
| LeakCanary | स्वचालित पहचान, सूचना में लीक ट्रेस | न्यूनतम |
| Xcode Memory Graph | retain cycles का विज़ुअल ग्राफ़, जीवित ऑब्जेक्ट सूची | निम्न |
Uber Engineering Blog के अनुसार, CI/CD पाइपलाइन में स्वचालित मेमोरी प्रोफाइलिंग (LeakCanary + heap dump विश्लेषण) को एकीकृत करने से 3 महीनों के भीतर उत्पादन में मेमोरी-संबंधित घटनाओं में 60% की कमी आती है।
Context बदलें — यदि कोई ऑब्जेक्ट Activity से अधिक जीवित रहता है, तो applicationContext का उपयोग करें। सभी लंबे समय तक चलने वाले ऑब्जेक्ट्स (सिंगलटन, रिपॉजिटरी, डेटाबेस हेल्पर्स) को Application Context प्राप्त करना चाहिए, Activity Context नहीं। अपवाद: UI घटक जिन्हें Activity के विशिष्ट थीम या संसाधनों तक पहुँच की आवश्यकता होती है।
Lifecycle-aware घटक — LifecycleObserver, DefaultLifecycleObserver या रिएक्टिव एक्सटेंशन का उपयोग onDestroy पर स्वचालित रूप से सब्सक्रिप्शन रद्द करता है। Android Jetpack lifecycleScope और viewModelScope प्रदान करता है, जो संबंधित जीवनचक्र घटना द्वारा साफ़ किए जाते हैं।
स्थैतिक आंतरिक वर्ग — यदि आंतरिक वर्ग को बाहरी वर्ग के फ़ील्ड तक पहुँच की आवश्यकता नहीं है, तो इसे static बनाएँ। स्थैतिक आंतरिक वर्ग का बाहरी वर्ग पर कोई अंतर्निहित संदर्भ नहीं होता। यदि पहुँच आवश्यक है, तो स्पष्ट संदर्भ के लिए WeakReference का उपयोग करें।
class MyActivity : AppCompatActivity() {
// ❌ गैर-स्थैतिक आंतरिक वर्ग — MyActivity का अंतर्निहित संदर्भ
inner class BadListener : SomeListener {
override fun onEvent() { /*...*/ }
}
// ✅ स्थैतिक आंतरिक वर्ग — कोई अंतर्निहित संदर्भ नहीं
class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
override fun onEvent() { /*...*/ }
}
}
iOS में, कैप्चर सूचियों का उपयोग करें: क्लोज़र में [weak self] जो अपने निर्माता से अधिक जीवित रह सकते हैं। डेलिगेट के लिए, कमजोर संदर्भों का उपयोग करें (weak var delegate)। उन क्लोज़र के लिए जिनके केवल self के जीवनकाल के दौरान ही कॉल किए जाने की गारंटी है, [unowned self] का उपयोग किया जा सकता है, लेकिन सावधानी से — डीलोकेटेड ऑब्जेक्ट तक पहुँचने से क्रैश होगा।
अक्सर पूछे जाने वाले प्रश्न
Android में, कई स्क्रीन परिवर्तन (Activity A → B → A → B) करें और adb shell dumpsys meminfo package_name जाँचें। यदि Total PSS लगातार बढ़ता है और मूल मान पर वापस नहीं आता — तो लीक है। iOS में, समान रूप से: दृश्य निरीक्षण के लिए Xcode में Debug Memory Graph का उपयोग करें।
हाँ, यदि घटक नष्ट होने पर CoroutineScope रद्द नहीं किया गया। GlobalScope में लॉन्च किया गया कोरूटीन Activity के finish() के बाद भी निष्पादित होता रहता है। समाधान: viewModelScope (onCleared में रद्द) या lifecycleScope (onDestroy में रद्द) का उपयोग करें। कस्टम स्कोप के लिए, LifecycleOwner के माध्यम से lifecycle-aware स्कोप बनाएँ।
Bitmap पिक्सेल डेटा को Java heap के बजाय native heap में संग्रहीत करता है। इसका मतलब है कि Java GC Bitmap का वास्तविक आकार नहीं देखता। यदि Bitmap पर recycle() कॉल नहीं किया जाता या संदर्भ शून्य नहीं किया जाता, तो native मेमोरी मुक्त नहीं होती। छोटी प्रतियां लोड करने के लिए BitmapFactory का inSampleSize के साथ और स्वचालित कैश प्रबंधन के लिए Glide/Coil का उपयोग करें।
स्थैतिक फ़ील्ड — एक GC Root है। यह तब तक जीवित रहता है जब तक क्लास लोड है (Android में — जब तक Process जीवित है)। यदि स्थैतिक फ़ील्ड Activity, Bitmap, View या किसी अन्य भारी ऑब्जेक्ट को संदर्भित करता है, तो वह ऑब्जेक्ट कभी GC द्वारा एकत्र नहीं किया जाएगा। स्थैतिक फ़ील्ड एक शाश्वत संदर्भ है। समाधान: केवल WeakReference संग्रहीत करें या onDestroy में स्थैतिक फ़ील्ड को शून्य करें।
ARC स्वचालित रूप से ऑब्जेक्ट्स को मुक्त करता है जब मजबूत संदर्भ गणना शून्य हो जाती है। ARC के तहत लीक का एकमात्र तरीका retain cycle है। हमेशा parent→child संदर्भों के लिए weak का उपयोग करें जहाँ child parent से अधिक जीवित रह सकता है (डेलिगेट, data source)। क्लोज़र के लिए, कैप्चर सूची [weak self] का उपयोग करें और क्लोज़र के अंदर self को nil के लिए जाँचें।
सारांश
हम एक मोबाइल एप्लिकेशन टर्नकी विकसित करेंगे
IT Sectr 2017 से स्टार्टअप और व्यवसायों के लिए iOS और Android एप्लिकेशन बनाता है। हम आपको सलाह देंगे और सर्वोत्तम समाधान प्रस्तावित करेंगे।
यह भी पढ़ें