মেমরি লিক (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 → anonymous class → outer class (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 অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন