نشت حافظه: چیست، سناریوهای معمول و تشخیص

نویسنده: IT Sectr منتشر شده: 2026-07-29 زمان مطالعه: 10 دقیقه

نشت حافظه (memory leak) — وضعیتی است که برنامه حافظه اشغال شده توسط اشیایی که دیگر مورد نیاز نیستند را آزاد نمی‌کند. در توسعه موبایل این به ویژه حیاتی است: heap محدود و نبود swap منجر به OutOfMemoryError و سقوط برنامه می‌شود. طبق داده‌های Purdue University (2022)، 35٪ از برنامه‌های Android در Google Play حداقل دارای یک نشت حافظه هستند. سناریوهای معمول، ابزارهای تشخیص و روش‌های رفع را بررسی می‌کنیم.

نکات اصلی

  • GC Root — نقطه ورودی که زباله‌روب از طریق آن اشیای زنده را تعیین می‌کند
  • نشت Context — ارسال Activity Context به singleton منجر به نگه‌داشتن کل سلسله‌مراتب View می‌شود
  • Handler با postDelayed — اگر Activity نابود شود، Handler اجازه نمی‌دهد به GC برود
  • Heap dump — روش اصلی تحلیل نشت‌ها از طریق MAT یا Android Profiler
  • SoftReference — جایگزین WeakReference برای کش‌هایی که در کمبود حافظه به طور خودکار پاک می‌شوند

نشت حافظه در برنامه‌های موبایل چیست؟

نشت حافظه — وضعیتی است که حافظه تخصیص داده شده پس از اینکه شیء دیگر برای برنامه مورد نیاز نیست به سیستم بازگردانده نمی‌شود. زباله‌روب چنین شیئی را زنده در نظر می‌گیرد، زیرا یک زنجیره مرجع فعال از GC Root به آن منتهی می‌شود.

در Java/Kotlin زباله‌روب به طور خودکار کار می‌کند، اما نمی‌تواند تعیین کند که یک شیء منطقاً مورد نیاز نیست اگر یک مرجع فنی به آن وجود داشته باشد. توسعه‌دهنده باید به صراحت ارتباطات غیرضروری را قطع کند. در Swift/Objective-C ARC به طور خودکار مراجع را شمارش می‌کند، اما retain cycles شمارشگر را صفر نمی‌کند.

خطر اصلی نشت‌ها — اثر تجمعی است. هر نشت مقدار کمی حافظه مصرف می‌کند، اما با جابجایی‌های مکرر بین صفحه‌ها (چرخش صفحه، باز/بسته کردن Activity) نشت‌ها انباشته می‌شوند تا زمانی که محدودیت heap تمام شود.

تفاوت نشت با تورم چیست؟

نشت — شیء برای کد قابل دسترسی نیست، اما توسط GC حذف نشده است. تورم — شیء منطقاً مورد نیاز است، اما به مقدار اضافی ذخیره شده است. مثال تورم: کش تصاویر 100 مگابایتی با مجموعه کاری 30 مگابایت. هر دو مشکل منجر به OOM می‌شوند، اما دلایل و روش‌های درمان متفاوت است.

زباله‌روب چگونه کار می‌کند و چرا نشت رخ می‌دهد؟

ART (Android Runtime) از جمع‌آوری زباله نسل‌محور با concurrent compaction استفاده می‌کند. حافظه به نسل جوان (Young)، پیر (Old) و اشیای بزرگ (Large) تقسیم می‌شود. اشیایی که چندین چرخه GC را پشت سر گذاشته‌اند به Old generation منتقل می‌شوند، جایی که جمع‌آوری کمتر انجام می‌شود — این کار چرخه‌های معمولی را سرعت می‌بخشد.

GC زمانی شروع می‌شود که heap به آستانه مشخصی از اشغال می‌رسد (معمولاً 75-85٪). در طول GC تمام رشته‌های برنامه متوقف می‌شوند (STW — Stop The World). هرچه اشیای زنده بیشتر باشد، مکث طولانی‌تر است. نشت‌ها تعداد اشیای زنده را افزایش می‌دهند و مکث‌های GC را طولانی‌تر می‌کنند.

زباله‌روب اشیای زنده را با پیمایش گراف از GC Roots تعیین می‌کند: فیلدهای ایستا، متغیرهای پشته‌ای رشته‌های فعال، مراجع JNI. هر شیء قابل دسترسی از طریق مراجع از این ریشه‌ها زنده در نظر گرفته می‌شود — حتی اگر توسعه‌دهنده بداند که دیگر مورد نیاز نیست.

kotlin
// مثال: مجموعه ایستا به عنوان 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 جمع‌آوری می‌شود.

سناریوهای معمول نشت در Android و iOS

Activity Context — گسترده‌ترین سناریوی نشت در Android. اگر singleton، فیلد ایستا یا سرویس طولانی‌مدت مرجعی به Activity Context ذخیره کند، کل Activity با تمام Viewها نمی‌تواند توسط GC جمع‌آوری شود. راه‌حل: برای اشیای طولانی‌مدت از Application Context استفاده کنید.

Handler و پیام‌های ارسال شده — Handler.postDelayed(runnable, delay) یک پیام در صف Main Looper قرار می‌دهد. اگر Activity قبل از اتمام تاخیر نابود شود، پیام همچنان در صف است و مرجع را از طریق Runnable → کلاس ناشناس → کلاس خارجی (Activity) نگه می‌دارد.

kotlin
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 نمی‌تواند جمع‌آوری شود.

  • TimerTask و ScheduledExecutorService — وظایفی که قبل از نابودی Activity برنامه‌ریزی شده‌اند
  • BroadcastReceiver — لغو ثبت‌نام نشده در onPause/onDestroy به نگه‌داشتن Context ادامه می‌دهد
  • ViewModel با مرجع به View — ViewModel بیشتر از Activity عمر می‌کند، مرجع به View منجر به نشت می‌شود
  • Retrofit Call — اگر Call لغو نشود، پاسخ به Fragment نابود شده می‌رسد

ابزارهای تشخیص نشت حافظه

Android Studio Memory Profiler — ابزار داخلی برای نظارت بر heap در زمان واقعی. نمودار حافظه اشغال شده، تعداد تخصیص‌ها و اشیا بر اساس نوع را نشان می‌دهد. امکان ضبط heap dump و خروجی به فرمت HPROF برای تحلیل در MAT را فراهم می‌کند.

Eclipse MAT (Memory Analyzer Tool) — تحلیل‌گر رومیزی heap dump. به طور خودکار Leak Suspects Report را می‌سازد که اشیا با بیشترین retained size را برجسته می‌کند و زنجیره GC root احتمالی را برای هر شیء مشکوک پیشنهاد می‌دهد.

Xcode Memory Graph Debugger — برای iOS. برنامه را متوقف می‌کند و گراف اشیا را تجسم می‌بخشد. Retain cycles با رنگ قرمز برجسته می‌شوند، می‌توان روی هر شیء کلیک کرد و retain count و مراجع آن را مشاهده کرد.

ابزارقابلیت‌هاپیچیدگی
Memory Profilerنمودار زمان واقعی، heap dump، ردیابی تخصیص اشیاکم
Eclipse MATدرخت غالب، مظنونان نشت، کوئری‌های OQLمتوسط
LeakCanaryتشخیص خودکار، ردپای نشت در اعلانحداقل
Xcode Memory Graphگراف بصری retain cycles، لیست اشیای زندهکم

طبق Uber Engineering Blog، پیاده‌سازی پروفایل‌بندی خودکار حافظه (LeakCanary + تحلیل heap dump) در خط لوله CI/CD تعداد حوادث مرتبط با حافظه در تولید را طی 3 ماه 60٪ کاهش می‌دهد.

روش‌های رفع نشت

جایگزینی Context — اگر شیء بیشتر از Activity عمر می‌کند، از applicationContext استفاده کنید. همه اشیای طولانی‌مدت (singletonها، مخزن‌ها، کمک‌کننده‌های پایگاه داده) باید Application Context دریافت کنند، نه Activity Context. استثنا: کامپوننت‌های UI که به تم یا منابع خاص Activity نیاز دارند.

کامپوننت‌های Lifecycle-aware — استفاده از LifecycleObserver، DefaultLifecycleObserver یا reactivex به طور خودکار اشتراک‌ها را در onDestroy لغو می‌کند. Android Jetpack lifecycleScope و viewModelScope را فراهم می‌کند که با رویداد مربوطه پاک می‌شوند.

کلاس داخلی ایستا — اگر کلاس داخلی به فیلدهای کلاس خارجی نیاز ندارد، آن را static کنید. کلاس داخلی ایستا مرجع ضمنی به کلاس خارجی ندارد. اگر دسترسی لازم است، از WeakReference برای مرجع صریح استفاده کنید.

kotlin
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] در بستارهایی که ممکن است بیشتر از ایجادکننده عمر کنند. برای delegateها از مراجع ضعیف استفاده کنید (weak var delegate). برای بستارهایی که تضمیناً فقط در طول عمر self فراخوانی می‌شوند، می‌توان از [unowned self] استفاده کرد، اما با احتیاط — دسترسی به شیء آزاد شده باعث crash می‌شود.

سوالات متداول

چگونه بدون ابزارهای خاص نشت را پیدا کنیم؟

در Android چند جابجایی بین صفحه‌ها انجام دهید (Activity A → B → A → B) و adb shell dumpsys meminfo package_name را بررسی کنید. اگر Total PSS به طور پایدار افزایش یابد و به مقدار اولیه برنگردد — نشت وجود دارد. در iOS به طور مشابه: برای بررسی بصری از Debug Memory Graph در Xcode استفاده کنید.

آیا کوروتین Kotlin می‌تواند باعث نشت شود؟

بله، اگر CoroutineScope هنگام نابودی کامپوننت لغو نشود. کوروتین راه‌اندازی شده در GlobalScope حتی پس از finish() Activity به اجرا ادامه می‌دهد. راه‌حل: از viewModelScope (لغو شده در onCleared) یا lifecycleScope (لغو شده در onDestroy) استفاده کنید. برای Scopeهای سفارشی خود از طریق LifecycleOwner scopeهای lifecyle-aware ایجاد کنید.

Bitmap چگونه بر نشت‌ها تأثیر می‌گذارد؟

Bitmap داده‌های پیکسل را در heap بومی ذخیره می‌کند، نه در Java heap. این بدان معناست که Java GC اندازه واقعی Bitmap را نمی‌بیند. اگر recycle() روی Bitmap فراخوانی نشود یا مرجع صفر نشود، حافظه بومی آزاد نخواهد شد. برای بارگذاری نسخه‌های کوچک‌شده از BitmapFactory با inSampleSize و برای مدیریت خودکار کش از Glide/Coil استفاده کنید.

نشت از طریق فیلد ایستا چیست؟

فیلد ایستا — یک GC Root است. تا زمانی که کلاس بارگذاری شده است (در Android — تا زمانی که Process زنده است) زندگی می‌کند. اگر فیلد ایستا به Activity، Bitmap، View یا هر شیء سنگین دیگری ارجاع دهد، این شیء هرگز توسط GC جمع‌آوری نخواهد شد. فیلد ایستا — یک مرجع ابدی است. راه‌حل: فقط WeakReference ذخیره کنید یا فیلد ایستا را در onDestroy صفر کنید.

چگونه در iOS با ARC از نشت جلوگیری کنیم؟

ARC به طور خودکار اشیا را زمانی که شمارنده مراجع قوی به صفر می‌رسد آزاد می‌کند. Retain cycle — تنها راه نشت در ARC است. همیشه برای مراجع parent→child از weak استفاده کنید، جایی که child باید بیشتر از parent عمر کند (delegateها، data source). برای بستارها از لیست ضبط [weak self] استفاده کنید و self را در داخل بستار برای nil بررسی کنید.

خلاصه

  • نشت حافظه — شیء برای کد قابل دسترسی نیست، اما توسط GC حذف نشده زیرا یک مرجع فعال از GC Root به آن وجود دارد
  • GC Roots شامل فیلدهای ایستا، متغیرهای پشته و مراجع JNI هستند؛ هر شیء قابل دسترسی از آن‌ها زنده است
  • نشت Context — گسترده‌ترین مشکل در Android: ارسال Activity Context به singleton یا فیلد ایستا
  • Handler و کلاس داخلی — دومین علت از نظر فراوانی: پیام‌های لغو نشده در صف Looper مرجع به Activity را نگه می‌دارند
  • LeakCanary — ابزار استاندارد تشخیص خودکار؛ heap dump می‌گیرد و زنجیره دقیق GC root را نشان می‌دهد
  • lifecycleScope و viewModelScope مشکل نشت از طریق کوروتین‌ها را حل می‌کنند — لغو خودکار در destroy
  • حافظه را در CI/CD پروفایل کنید: LeakCanary در debug + تحلیل heap dump در اجرای تست باید merge را در نشت‌های جدید مسدود کند

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید