মেমোরি লিক (Memory Leak) — এমন একটি অবস্থা যেখানে অ্যাপ্লিকেশন সেই অবজেক্টের রেফারেন্স ধরে রাখে যেগুলির আর প্রয়োজন নেই, ফলে গার্বেজ কালেক্টর (GC) অধিকৃত মেমোরি মুক্ত করতে পারে না। LeakCanary অনুসারে, ভালোভাবে লেখা অ্যাপ্লিকেশনেও প্রতি 10,000 লাইন কোডে 3–5টি লিক পাওয়া যায়। প্রতিটি লিক ধীরে ধীরে উপলব্ধ মেমোরি হ্রাস করে, যা মন্থরতা এবং OutOfMemoryError-এর দিকে নিয়ে যায়।
মূল বিষয়
মেমোরি লিক (Memory Leak) — এমন একটি অবস্থা যেখানে একটি অবজেক্ট শক্তিশালী রেফারেন্স (Strong Reference) শৃঙ্খলের মাধ্যমে অ্যাক্সেসযোগ্য থাকে, যদিও এটি যৌক্তিকভাবে অ্যাপ্লিকেশনের জন্য আর প্রয়োজনীয় নয়। গার্বেজ কালেক্টর (GC) এই ধরনের অবজেক্টকে জীবিত মনে করে এবং এটি যে মেমোরি দখল করে তা মুক্ত করে না। ফলস্বরূপ, উপলব্ধ হিপ মেমোরি ক্রমাগত হ্রাস পায় এবং GC বিরতির ফ্রিকোয়েন্সি বেড়ে যায়।
পার্থক্য — ম্যানুয়াল মেমোরি ব্যবস্থাপনা সহ ভাষার (C, C++) থেকে ভিন্ন, Java/Kotlin-এ লিক ভুলে যাওয়া free() নয়, বরং ভুলে যাওয়া রেফারেন্স। যতক্ষণ GC রুট থেকে লিক হওয়া অবজেক্ট পর্যন্ত একটি স্ট্রং রেফারেন্স বিদ্যমান, GC এটিকে প্রয়োজনীয় মনে করে। সাধারণ GC রুট: স্ট্যাটিক ফিল্ড, সক্রিয় থ্রেড, কল স্ট্যাক, JNI গ্লোবাল রেফারেন্স।
লিকের বিপদ তাদের সঞ্চয়ী প্রভাব। একটি 100 KB-র লিক লক্ষণীয় নয়, কিন্তু এরকম 100টি লিক 10 MB দখল করে, এবং অ্যাপ্লিকেশন ঘন ঘন GC-র কারণে মন্থর হতে শুরু করে। লিকের সমালোচনামূলক ভর OutOfMemoryError এবং অ্যাপ্লিকেশন ক্র্যাশের দিকে নিয়ে যায়। লিকের লক্ষণ: Profiler গ্রাফে মেমোরি খরচের ক্রমাগত বৃদ্ধি, STW (Stop The World) সহ ঘন ঘন GC বিরতি এবং UI কর্মক্ষমতার অবনতি।
পাঁচ ধরনের লিক মোবাইল ডেভেলপমেন্টে 95% ক্ষেত্রে কভার করে। প্রতিটির নিজস্ব কারণ এবং কোডে বৈশিষ্ট্যপূর্ণ প্যাটার্ন রয়েছে।
সবচেয়ে পরিচিত লিক Android-এ — Activity বা Context-এর স্ট্যাটিক রেফারেন্স সংরক্ষণ করা। সাধারণ কোড: একটি স্ট্যাটিক Activity ফিল্ড যা onDestroy()-এ নাল করা হয় না। যতক্ষণ স্ট্যাটিক ফিল্ড বেঁচে থাকে, পুরো Activity তার View ট্রি সহ বেঁচে থাকে, যা 1–10 MB দখল করতে পারে। এটি ক্লাসিক লিক যা LeakCanary প্রথমে খুঁজে পায়।
সমাধান: Activity বা Context কখনও স্ট্যাটিক ফিল্ডে সংরক্ষণ করবেন না। সিঙ্গলটনের জন্য Application Context ব্যবহার করুন যা Activity-র চেয়ে বেশি বাঁচে। Activity-র রেফারেন্স প্রয়োজন হলে WeakReference<Activity> ব্যবহার করুন।
object MySingleton {
private var weakActivity: WeakReference<Activity>? = null
fun attach(activity: Activity) {
weakActivity = WeakReference(activity)
}
}
বেনামী ক্লাস এবং নন-স্ট্যাটিক অভ্যন্তরীণ ক্লাস অন্তর্নিহিতভাবে ধারণকারী ক্লাসের রেফারেন্স ধরে রাখে। Handler-এ পাস করা Runnable যা onDestroy()-এর পরে সম্পাদিত হয়, পুরো Activity-কে ধরে রাখে। Activity-কে ক্যাপচার করা Retrofit কলব্যাকও একই কাজ করে। এটি লিকের সবচেয়ে কপট ধরন — অন্তর্নিহিত রেফারেন্স কোডে দৃশ্যমান নয়।
Kotlin object এক্সপ্রেশন এবং ল্যাম্বডাও বাইরের ক্লাসের রেফারেন্স ক্যাপচার করে। অভ্যন্তরীণ ক্লাসগুলিকে স্ট্যাটিক (বা Kotlin-এ টপ-লেভেল) করুন এবং বাইরের রেফারেন্স WeakReference-এর মাধ্যমে পাস করুন। ল্যাম্বডার জন্য viewLifecycleOwner-এর সাথে Lifecycle-aware পদ্ধতি ব্যবহার করুন।
সিস্টেম সার্ভিসে সাবস্ক্রাইব করা আনসাবস্ক্রাইব না করে — এটি সরাসরি লিক। SensorManager, LocationManager, NotificationListener onResume()-এ নিবন্ধিত কিন্তু onPause()-এ unregister কল না করলে Activity ধরে রাখে। একইভাবে: RxJava Disposable যা CompositeDisposable-এ যোগ করা হয়নি, এবং GlobalScope-এর মাধ্যমে চালু করা coroutine।
Lifecycle-aware উপাদান ব্যবহার করুন: LifecycleOwner-এর সাথে observe() onDestroy()-এ স্বয়ংক্রিয়ভাবে আনসাবস্ক্রাইব হয়। RxJava-র জন্য — DisposableObserver-সহ viewLifecycleOwner.lifecycle.addObserver। coroutine-এর জন্য — lifecycleScope.launch() জীবনচক্রের সাথে আবদ্ধ।
// Lifecycle-এর মাধ্যমে স্বয়ংক্রিয় আনসাবস্ক্রাইব
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// lifecycleScope-সহ coroutine
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
Bitmap হিপ মেমোরির একটি উল্লেখযোগ্য পরিমাণ দখল করে: একটি FullHD বিটম্যাপ হল 1920 × 1080 × 4 বাইট = 8.3 MB। যদি তালিকার প্রতিটি আইটেমের জন্য Bitmap তৈরি করা হয় এবং লুকানোর সময় recycle() কল না করা হয়, তবে মেমোরি দ্রুত শেষ হয়ে যায়। Android-এর পুরনো সংস্করণে (3.0-এর আগে) Bitmap নেটিভ মেমোরিতে সংরক্ষিত হত, কিন্তু আধুনিক সংস্করণে এটি Dalvik/ART হিপ-এ থাকে, এবং GC এটি কেবল তখনই মুক্ত করতে পারে যদি কোনও স্ট্রং রেফারেন্স না থাকে।
ছবি লোড করার জন্য Glide বা Coil ব্যবহার করুন — এই লাইব্রেরিগুলি ক্যাশিং এবং রিসাইক্লিং স্বয়ংক্রিয়ভাবে পরিচালনা করে। সরাসরি Bitmap নিয়ে কাজ করলে, বড় ছবির জন্য bitmap.recycle() কল করুন যা আর প্রদর্শিত হচ্ছে না, এবং হ্রাসকৃত কপি লোড করতে inSampleSize ব্যবহার করুন।
Fragment-এর দুটি জীবনচক্র আছে: Fragment নিজের এবং তার View-এর। onDestroyView()-এর পর View ট্রি ধ্বংস হয়, কিন্তু Fragment নিজে মেমোরিতে থাকতে পারে যদি বাহ্যিক রেফারেন্স থাকে। একটি সাধারণ ভুল ViewPager অ্যাডাপ্টার বা ন্যাভিগেশন গ্রাফে Fragment-এর রেফারেন্স সংরক্ষণ করা যা ধ্বংসের সময় পরিষ্কার হয় না।
দীর্ঘজীবী অবজেক্টের ফিল্ডে Fragment-এর রেফারেন্স কখনও সংরক্ষণ করবেন না। নেস্টেড ফ্র্যাগমেন্টের জন্য childFragmentManager এবং তাদের মধ্যে ডেটা স্থানান্তরের জন্য LifecycleOwner-সহ observe() ব্যবহার করুন। ViewPager2 API স্তরে এই সমস্যার সমাধান করেছে: FragmentTransactionAdapter সঠিকভাবে জীবনচক্র পরিচালনা করে।
লিক সনাক্তকরণের জন্য দুটি তথ্য যাচাই প্রয়োজন: প্রত্যাশিত জীবনকালের পরে মেমোরি ফিরে আসে না এবং নির্দিষ্ট ধরণের অবজেক্টের সংখ্যা হ্রাস ছাড়াই বৃদ্ধি পায়। রোগনির্ণয় প্রক্রিয়ায় তিনটি পর্যায় অন্তর্ভুক্ত।
প্রথম পর্যায় — Android Studio-তে Memory Profiler-এর মাধ্যমে দৃশ্য পরীক্ষা। Memory ট্যাব খুলুন, লক্ষ্য কর্ম সম্পাদন করুন (স্ক্রিন খুলুন এবং বন্ধ করুন), GC (Garbage Collection) টিপুন এবং দেখুন মেমোরি প্রাথমিক স্তরে ফিরে আসে কিনা। যদি 3–4টি খোলা-বন্ধ চক্রের পরে মেমোরি ধারাবাহিকভাবে বাড়ে — তবে লিক আছে।
দ্বিতীয় পর্যায় — Heap Dump নেওয়া। Memory Profiler-এ Dump Java Heap টিপুন। ফলস্বরূপ .hprof ফাইল Android Studio-তে খুলুন: আপনি আকার এবং রেফারেন্স সহ হিপের সব অবজেক্ট দেখতে পাবেন। সেই ক্লাসগুলি খুঁজুন যার সংখ্যা স্ক্রিন বন্ধ করার পরে শূন্য হওয়া উচিত। উদাহরণস্বরূপ, বন্ধ করার পরে সংখ্যা 2 বিশিষ্ট MainActivity — স্পষ্ট লিক।
তৃতীয় পর্যায় — Retained Size এবং GC Root বিশ্লেষণ। Android Studio-তে Retained Size বিশ্লেষণ করুন: আপনি যদি এই অবজেক্টটি সরিয়ে ফেলেন তবে কত মেমোরি মুক্ত হবে। GC Root থেকে অবজেক্টের পথ দেখায় কী এটি ধরে রেখেছে: Static field → HashMap → Activity — এবং আপনি লিক পয়েন্ট দেখতে পান। Reference উইজেট প্যানেল অবজেক্টের সব ধারক দেখায়।
চারটি টুল স্বয়ংক্রিয় সনাক্তকরণ থেকে গভীর Heap Dump বিশ্লেষণ পর্যন্ত লিক খোঁজা কভার করে।
| টুল | পদ্ধতি | ফলাফলের বিন্যাস |
|---|---|---|
| LeakCanary | স্বয়ংক্রিয় পর্যবেক্ষণ | Heap Dump + লিক stack trace |
| Android Memory Profiler | ম্যানুয়াল পর্যবেক্ষণ | মেমোরি গ্রাফ + Heap Dump |
| MAT (Eclipse) | গভীর বিশ্লেষণ | Dominator Tree রিপোর্ট + GC Root পথ |
| Perfetto | সিস্টেম-ব্যাপী ট্রেসিং | সময়রেখা + নেটিভ মেমোরি |
LeakCanary যেকোনো Android প্রকল্পের জন্য অপরিহার্য। এটি Activity/Fragment জীবনচক্রের শেষে স্বয়ংক্রিয়ভাবে লিক সনাক্ত করে এবং stack trace-সহ সঠিক লিক অবস্থান দেখায়। একীকরণ: build.gradle-এ এক লাইন। LeakCanary 2.x-এর ম্যানুয়াল আরম্ভের প্রয়োজন নেই — এটি স্বয়ংক্রিয়ভাবে Application Watcher নিবন্ধন করে।
লিক প্রতিরোধ নিয়ম এবং টুলের একটি সেটের মাধ্যমে উন্নয়ন প্রক্রিয়ায় তৈরি করা হয় যা প্রতিটি ধাপে কোড পরীক্ষা করে।
কখনও Activity, Fragment বা View-এর রেফারেন্স স্ট্যাটিক ফিল্ড, সিঙ্গলটন বা দীর্ঘজীবী অবজেক্টে সংরক্ষণ করবেন না। যদি রেফারেন্স এড়ানো না যায়, তবে WeakReference ব্যবহার করুন বা ViewModel-এর মাধ্যমে ডেটা সংরক্ষণ করুন, যা যতক্ষণ প্রয়োজন ঠিক ততক্ষণ বাঁচে এবং সরাসরি View ধরে না।
Android Architecture Components-এর ViewModel এবং LiveData আর্কিটেকচার স্তরে জীবনচক্র সমস্যার সমাধান করে। ViewModel স্ক্রিন রোটেশন থেকে বেঁচে যায় এবং View রেফারেন্স ধারণ করে না। LiveData onDestroy()-এ স্বয়ংক্রিয়ভাবে পর্যবেক্ষককে আনসাবস্ক্রাইব করে। সিস্টেম সার্ভিসে ম্যানুয়াল সাবস্ক্রিপশনের পরিবর্তে এগুলি ব্যবহার করুন।
কোড পর্যালোচনায় মনোযোগ দিন: Context/View টাইপের স্ট্যাটিক ফিল্ড, বেনামী ক্লাস, Activity ক্যাপচার করা ল্যাম্বডা, ম্যানুয়াল সাবস্ক্রিপশন, কম্পোজিট ছাড়া RxJava disposable, Bundle-এর মাধ্যমে Fragment সংরক্ষণ। Kotlin-এ, জীবনচক্র বন্ধন ছাড়া launch-এ coroutine অতিরিক্ত পরীক্ষা করুন।
LeakCanary পরীক্ষার পাইপলাইনের অংশ হিসাবে কাজ করতে পারে: LeakCanary-সহ গ্রহণযোগ্যতা পরীক্ষা চালান এবং লিক পাওয়া গেলে বিল্ড ব্যর্থ করুন। এটি লিক প্রোডাকশনে পৌঁছাতে বাধা দেয়। Android Lint-এর StaticFieldLeak নিয়ম দিয়ে পরীক্ষা সম্পূরক করুন — এটি স্ট্যাটিক বিশ্লেষণ স্তরে সম্ভাব্য লিক খুঁজে পায়।
// পরীক্ষায় LeakCanary
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // লিক থাকলে ব্যর্থ করুন
}
}
সচরাচর জিজ্ঞাসিত প্রশ্ন
লিক কারণ, এবং OutOfMemoryError ফলাফল। একটি লিক OOM-এর দিকে নিয়ে যায় না, কিন্তু ডজনখানেক লিকের সঞ্চয় Heap শেষ করে দেয়। OOM একটি মারাত্মক ব্যতিক্রম, যেখানে লিক একটি প্যাটার্ন যা সময়ের সাথে এটির দিকে নিয়ে যায়।
Android Memory Profiler-এর মাধ্যমে: স্ক্রিন 5 বার খুলুন এবং বন্ধ করুন, প্রতিটি বন্ধের পরে GC কল করুন। যদি মেমোরি বেসলাইন স্তরে ফিরে না আসে — লিক আছে। Heap Dump নিন এবং তালিকায় সেই Activity ক্লাসটি খুঁজুন যার সংখ্যা বন্ধের পরে 0-এর বেশি।
আংশিকভাবে। Kotlin null-safety সমস্যা সমাধান করে কিন্তু স্ট্রং রেফারেন্স পরিচালনা করে না। lifecycleScope এবং viewModelScope-সহ coroutine ব্যাকগ্রাউন্ড টাস্ক থেকে লিক প্রতিরোধ করে, যেখানে sealed class এবং data class লিকের দিকে নেওয়া অবস্থার সংখ্যা হ্রাস করে। প্রধান সুরক্ষা ভাষার বৈশিষ্ট্য নয়, বরং আর্কিটেকচারাল প্যাটার্ন।
LeakCanary মাঝে মাঝে মিথ্যা পজিটিভ দেয়: একটি অবজেক্ট অস্থায়ীভাবে সিস্টেম দ্বারা ধরা হতে পারে (উদাহরণস্বরূপ, InputMethodManager শেষ View ধরে রাখে)। ম্যানুয়ালি পরীক্ষা করুন: যদি Retained Size < 1 KB হয় এবং GC Root একটি সিস্টেম সার্ভিস হয়, তবে এটি সম্ভবত মিথ্যা পজিটিভ।
না। লিক GC-সহ যেকোনো প্ল্যাটফর্মে সম্ভব: iOS (Swift/Objective-C), Flutter (Dart), ওয়েব ব্রাউজার (JavaScript)। প্রক্রিয়াগুলি একই — GC Root থেকে স্ট্রং রেফারেন্স। iOS-এ, ARC স্বয়ংক্রিয়ভাবে মেমোরি পরিচালনা করে, কিন্তু অবজেক্টের মধ্যে retain cycle একই লিক তৈরি করে।
সারাংশ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন