Διαρροή μνήμης: τι είναι, τυπικά σενάρια και διάγνωση

Συγγραφέας: 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 MB με σύνολο εργασίας 30 MB. Και τα δύο προβλήματα οδηγούν σε 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()
    }
}

Inner Classes — μια μη στατική εσωτερική κλάση έχει σιωπηρή αναφορά στο στιγμιότυπο της εξωτερικής κλάσης. Αν η εξωτερική κλάση είναι 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) στο pipeline CI/CD μειώνει τον αριθμό περιστατικών που σχετίζονται με μνήμη στην παραγωγή κατά 60% εντός 3 μηνών.

Μέθοδοι εξάλειψης διαρροών

Αντικατάσταση Context — αν το αντικείμενο ζει περισσότερο από το Activity, χρησιμοποιήστε applicationContext. Όλα τα αντικείμενα μακράς διάρκειας (singleton, αποθετήρια, database helpers) πρέπει να λαμβάνουν Application Context, όχι Activity Context. Εξαίρεση: στοιχεία UI που χρειάζονται πρόσβαση σε θέμα ή πόρους συγκεκριμένους για το Activity.

Lifecycle-aware στοιχεία — η χρήση LifecycleObserver, DefaultLifecycleObserver ή reactivex ακυρώνει αυτόματα τις συνδρομές στο onDestroy. Το Android Jetpack παρέχει lifecycleScope και viewModelScope, τα οποία καθαρίζονται από το αντίστοιχο συμβάν.

Static inner class — αν η εσωτερική κλάση δεν χρειάζεται πρόσβαση στα πεδία της εξωτερικής, κάντε την 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 χρησιμοποιήστε capture lists: [weak self] σε closures που μπορεί να επιβιώσουν του δημιουργού. Για delegates χρησιμοποιήστε αδύναμες αναφορές (weak var delegate). Για closures που είναι εγγυημένο ότι καλούνται μόνο κατά τη διάρκεια ζωής του 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 δημιουργήστε lifecycle-aware scopes μέσω LifecycleOwner.

Πώς επηρεάζει το Bitmap τις διαρροές;

Το Bitmap αποθηκεύει δεδομένα pixel σε native heap, όχι σε Java heap. Αυτό σημαίνει ότι το Java GC δεν βλέπει το πραγματικό μέγεθος του Bitmap. Αν δεν κληθεί recycle() στο Bitmap ή η αναφορά δεν μηδενιστεί, η native μνήμη δεν θα απελευθερωθεί. Χρησιμοποιήστε BitmapFactory με inSampleSize για φόρτωση μικρογραφιών και Glide/Coil για αυτόματη διαχείριση προσωρινής μνήμης.

Τι είναι η διαρροή μέσω στατικού πεδίου;

Το στατικό πεδίο — είναι GC Root. Ζει όσο η κλάση είναι φορτωμένη (στο Android — όσο ζει το Process). Αν ένα στατικό πεδίο αναφέρεται σε Activity, Bitmap, View ή οποιοδήποτε άλλο βαρύ αντικείμενο, αυτό το αντικείμενο δεν θα συλλεχθεί ποτέ από το GC. Το στατικό πεδίο — αιώνια αναφορά. Λύση: αποθηκεύστε μόνο WeakReference ή μηδενίστε το στατικό πεδίο στο onDestroy.

Πώς να αποφύγω διαρροές σε iOS με ARC;

Το ARC απελευθερώνει αυτόματα αντικείμενα όταν ο μετρητής ισχυρών αναφορών πέσει στο μηδέν. Retain cycle — ο μόνος τρόπος διαρροής στο ARC. Χρησιμοποιείτε πάντα weak για αναφορές parent→child, όπου το child πρέπει να επιβιώσει του parent (delegates, data source). Για closures χρησιμοποιήστε capture list [weak self] και ελέγξτε το self για nil μέσα στο closure.

Σύνοψη

  • Διαρροή μνήμης — το αντικείμενο δεν είναι προσβάσιμο από τον κώδικα, αλλά δεν αφαιρείται από το GC επειδή υπάρχει ενεργή αναφορά από GC Root
  • GC Roots περιλαμβάνουν στατικά πεδία, μεταβλητές στοίβας και αναφορές JNI· οποιοδήποτε αντικείμενο προσβάσιμο από αυτές είναι ζωντανό
  • Διαρροή Context — το πιο διαδεδομένο πρόβλημα στο Android: μετάδοση Activity Context σε singleton ή στατικό πεδίο
  • Handler και Inner Class — η δεύτερη πιο συχνή αιτία: μη ακυρωμένα μηνύματα στην ουρά Looper κρατούν αναφορά στο Activity
  • LeakCanary — το τυπικό εργαλείο αυτόματου εντοπισμού· κάνει heap dump και δείχνει την ακριβή αλυσίδα GC root
  • lifecycleScope και viewModelScope λύνουν το πρόβλημα διαρροών μέσω κορουτινών — αυτόματη ακύρωση στο destroy
  • Κάντε προφίλ μνήμης στο CI/CD: LeakCanary σε debug + ανάλυση heap dump στη δοκιμαστική εκτέλεση πρέπει να μπλοκάρει το merge σε νέες διαρροές

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης