Η αυτόματη διαχείριση μνήμης μέσω συλλογής σκουπιδιών είναι βασικός μηχανισμός της πλατφόρμας Android, βασισμένος στην εικονική μηχανή ART. Σύμφωνα με Google Android Documentation, 2026, ο συλλέκτης σκουπιδιών απελευθερώνει τον προγραμματιστή από τη χειροκίνητη διαχείριση μνήμης, διαγράφοντας αυτόματα αντικείμενα στα οποία δεν υπάρχουν πλέον αναφορές. Χωρίς GC, κάθε δέσμευση αντικειμένου θα απαιτούσε ρητή κλήση free ή delete, κάτι που στο οικοσύστημα Java με εκατομμύρια αντικείμενα ανά δευτερόλεπτο είναι φυσικά αδύνατο.
Βασικά σημεία
Garbage Collection (GC) — είναι η αυτόματη διαδικασία εντοπισμού και απελευθέρωσης μνήμης που καταλαμβάνεται από αντικείμενα που δεν χρησιμοποιούνται πλέον από το πρόγραμμα. Στο πλαίσιο της ανάπτυξης κινητών, το GC εφαρμόζεται στην πλατφόρμα Android μέσω της εικονικής μηχανής ART, καθώς και στην τυπική Java Virtual Machine.
Σε αντίθεση με γλώσσες με χειροκίνητη διαχείριση μνήμης (C, C++), όπου ο προγραμματιστής πρέπει να καλεί ρητά free ή delete, το GC αναλαμβάνει πλήρως την παρακολούθηση του κύκλου ζωής των αντικειμένων. Ο προγραμματιστής δημιουργεί νέα αντικείμενα μέσω του τελεστή new, ενώ ο συλλέκτης καθορίζει πότε ένα αντικείμενο γίνεται μη προσβάσιμο — δηλαδή δεν έχει πλέον καμία ενεργή αναφορά.
Βασική μετρική απόδοσης GC — ο χρόνος παύσης (pause time) και η απόδοση (throughput). Η παύση είναι το διάστημα κατά το οποίο αναστέλλεται η εκτέλεση της εφαρμογής για τη διεξαγωγή συλλογής. Σε κινητό περιβάλλον, παύσεις μεγαλύτερες από 8–16 χιλιοστά του δευτερολέπτου είναι αισθητές ως χαμένα καρέ (jank).
Σύμφωνα με Google I/O 2019, το ART στο Android 10 μείωσε τις τυπικές παύσεις GC σε 2–4 ms, που είναι 70% λιγότερο σε σύγκριση με το Dalvik στο Android 4.4. Ωστόσο, η λανθασμένη διαχείριση μνήμης — συχνή δέσμευση αντικειμένων σε βρόχους, δημιουργία προσωρινών στιγμιοτύπων χωρίς λόγο — παραμένει η κύρια αιτία προβλημάτων απόδοσης.
Όλες οι υλοποιήσεις GC σε Java και Android βασίζονται σε διάφορους θεμελιώδεις αλγόριθμους, οι οποίοι συνδυάζονται για την επίτευξη ισορροπίας μεταξύ χρόνου παύσης και πληρότητας καθαρισμού. Η κατανόηση αυτών των αλγόριθμων είναι απαραίτητη για τη σύνταξη GC-friendly κώδικα.
Mark-and-Sweep — ο απλούστερος αλγόριθμος, που λειτουργεί σε δύο φάσεις. Στη φάση Mark, ο συλλέκτης διασχίζει το γράφημα αντικειμένων ξεκινώντας από τις ριζικές αναφορές (root set) — τοπικές μεταβλητές, στατικά πεδία, στοίβες νημάτων. Κάθε προσβάσιμο αντικείμενο σημειώνεται με σημαία live. Στη φάση Sweep, ο συλλέκτης διασχίζει ολόκληρο το σωρό και απελευθερώνει τη μνήμη των μη σημειωμένων αντικειμένων.
Μειονέκτημα — κατακερματισμός μνήμης: μετά το Sweep, οι ελεύθερες περιοχές εναλλάσσονται με κατειλημμένες, δυσχεραίνοντας τη δέσμευση μεγάλων αντικειμένων. Σε κινητά σενάρια αυτό είναι κρίσιμο, καθώς ο σωρός είναι συνήθως μικρός (64–512 MB σε Android).
Copying Collection διαιρεί το σωρό σε δύο ημίχωρους (semi-spaces). Τα ενεργά αντικείμενα αντιγράφονται από τον έναν ημίχωρο στον άλλο συμπαγώς, χωρίς κενά. Μετά την αντιγραφή, ο παλιός ημίχωρος κηρύσσεται εξ ολοκλήρου ελεύθερος. Ο αλγόριθμος εξαλείφει πλήρως τον κατακερματισμό, αλλά απαιτεί διπλάσια μνήμη.
Σε κινητά περιβάλλοντα, το Copying Collection εφαρμόζεται από γενεαλογικούς συλλέκτες για γρήγορο καθαρισμό νεαρών αντικειμένων, τα οποία στατιστικά πεθαίνουν νωρίς (υπόθεση ασθενούς γενιάς).
Generational Collection διαιρεί το σωρό σε γενιές: Young Generation (νεαρά αντικείμενα) και Old Generation (παλιά, που επέζησαν πολλών συλλογών). Η συλλογή της νεαρής γενιάς (Minor GC) εκτελείται συχνά και γρήγορα, καθώς τα περισσότερα αντικείμενα πεθαίνουν νεαρά. Η συλλογή της παλιάς γενιάς (Major GC ή Full GC) γίνεται σπανιότερα αλλά διαρκεί περισσότερο.
// Επίδειξη generational GC: τα νεαρά αντικείμενα πεθαίνουν γρήγορα
void processItems(List<Item> items) {
List<Result> results = new ArrayList<>(); // ζει για όλη τη μέθοδο
for (Item item : items) {
Result r = new Result(item.getValue()); // πεθαίνει ακαριαία
if (r.isValid()) {
process(r); // r γίνεται σκουπίδι
}
}
saveResults(results); // results μεταφέρεται στο Old Gen
}
Σε αυτό το παράδειγμα, τα αντικείμενα Result δημιουργούνται μέσα στο βρόχο και γίνονται αμέσως σκουπίδια — είναι ιδανικοί υποψήφιοι για Young GC. Το αντικείμενο results ζει περισσότερο και μεταναστεύει στην Old Generation. Ο διαχωρισμός γενεών επιτρέπει στο Minor GC να καθαρίζει νεαρά αντικείμενα σε χιλιοστά του δευτερολέπτου, χωρίς να αγγίζει τον παλιό σωρό.
Το Android έχει εξελιχθεί από Dalvik VM σε ART (Android Runtime), και η υλοποίηση GC είναι μία από τις βασικές διαφορές μεταξύ τους. Η κατανόηση της αρχιτεκτονικής GC στο Android βοηθά στη σύνταξη κώδικα που ελαχιστοποιεί τις παύσεις σε πραγματικές συσκευές.
| Χαρακτηριστικό | Dalvik (έως 4.4) | ART (5.0+) |
|---|---|---|
| Τύπος GC | Mark-and-Sweep με Concurrent Mark | Generational + Concurrent |
| Τυπική παύση | 10–30 ms | 2–4 ms |
| Συμπαγοποίηση | Όχι (μόνο αυξανόμενος κατακερματισμός) | Ναι (στο παρασκήνιο, χωρίς διακοπή εφαρμογής) |
| AOT-μεταγλώττιση | JIT (Just-In-Time) | AOT + JIT (υβριδικό) |
Dalvik χρησιμοποιούσε συνδυασμό Mark-and-Sweep με concurrent φάση. Το Concurrent Mark επέτρεπε στην εφαρμογή να συνεχίσει τη λειτουργία κατά τη διάσχιση του γραφήματος αντικειμένων, αλλά η φάση Sweep απαιτούσε διακοπή όλων των νημάτων (Stop-The-World). Σε συσκευές με μικρή RAM (512 MB – 1 GB), οι παύσεις έφταναν τα 30 ms, προκαλώντας αισθητές καθυστερήσεις στη διεπαφή. Επιπλέον, το Dalvik δεν συμπαγοποιούσε το σωρό, έτσι μετά από μακρά λειτουργία αυξανόταν ο κατακερματισμός και η δέσμευση μεγάλων αντικειμένων (π.χ. Bitmap) μπορούσε να προκαλέσει OutOfMemoryError παρά την επαρκή συνολική ελεύθερη μνήμη.
ART (Android Runtime) εισήγαγε γενεαλογικό συλλέκτη με concurrent συμπαγοποίηση. Ο σωρός διαιρείται σε τρεις περιοχές: Young, Mature (ανάλογο Old Generation) και Large Object Space (για αντικείμενα μεγέθους άνω των 12 KB). Η συλλογή Young Region γίνεται παράλληλα χωρίς διακοπή νημάτων στις περισσότερες περιπτώσεις. Στο Android 10+ εμφανίστηκε η Concurrent Copying — συμπαγοποίηση που εκτελείται σε νήμα παρασκηνίου χωρίς Stop-The-World.
Χάρη στην αρχιτεκτονική ART, οι τυπικές παύσεις GC μειώθηκαν σε 2–4 ms, και σε σενάρια με επικράτηση νεαρών αντικειμένων — σε 0.5–1 ms. Αυτό επέτρεψε σε συσκευές Android να παρέχουν σταθερά 60 FPS ακόμα και με ενεργή διαχείριση μνήμης.
Στο οικοσύστημα Java υπάρχουν διάφορες υλοποιήσεις GC, η καθεμία με το δικό της προφίλ απόδοσης. Για ανάπτυξη Android, η επιλογή περιορίζεται στο ART, αλλά η γνώση Java GC είναι χρήσιμη για τη σύνταξη διακομιστικού μέρους κινητών εφαρμογών και για ανάπτυξη σε Kotlin Multiplatform.
Serial GC — μονονηματικός συλλέκτης με πλήρη διακοπή εφαρμογής (Stop-The-World). Κάθε λειτουργία Mark, Sweep και Compact εκτελείται από ένα νήμα. Η απόδοση είναι χαμηλή — δεν εφαρμόζεται σε κινητούς διακομιστές. Κατάλληλο μόνο για μικρές εφαρμογές με σωρό έως 100 MB.
Parallel GC (γνωστός και ως Throughput Collector) χρησιμοποιεί πολλά νήματα για όλες τις φάσεις συλλογής. Εστιάζει στη μέγιστη απόδοση (throughput) — ελαχιστοποιεί το χρόνο που αφιερώνεται στο GC σε σχέση με το χρόνο λειτουργίας της εφαρμογής. Ενεργοποιείται με τη σημαία -XX:+UseParallelGC στο JVM.
G1 (Garbage-First) GC — ο προεπιλεγμένος συλλέκτης σε Java 9+. Ο σωρός διαιρείται σε περιοχές 1–32 MB. Το G1 προβλέπει το χρόνο παύσης και στοχεύει να τηρήσει το καθορισμένο όριο (προεπιλογή 200 ms). Προτεραιότητα: πρώτα καθαρίζονται οι περιοχές με τον μεγαλύτερο όγκο σκουπιδιών (εξού και η ονομασία). Το G1 είναι αποτελεσματικό για διακομιστές με μεγάλο σωρό (4–64 GB) και προβλέψιμες παύσεις.
// Ενεργοποίηση G1 GC με στοχευόμενη παύση 100 ms
// java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar
public class MemoryMonitor {
private static final long THRESHOLD = 512 * 1024 * 1024; // 512 MB
public void checkHeapUsage() {
Runtime rt = Runtime.getRuntime();
long used = rt.totalMemory() - rt.freeMemory();
if (used > THRESHOLD) {
System.out.println("Heap usage exceeded threshold: " + used);
System.out.println("Consider reducing allocations");
}
}
}
Η παρακολούθηση του σωρού μέσω Runtime επιτρέπει την έγκαιρη ανίχνευση διαρροών μνήμης. Εάν το used υπερβαίνει το 80% του μέγιστου σωρού σε σταθερή λειτουργία — αυτό είναι σήμα πιθανής διαρροής ή υπερβολικής κατανάλωσης μνήμης από την εφαρμογή.
Ακόμα και το σύγχρονο ART GC δεν λύνει όλα τα προβλήματα — η λανθασμένη χρήση μνήμης παραμένει η κύρια αιτία jank και ANR (Application Not Responding). Ας δούμε τα βασικά σενάρια και μεθόδους βελτιστοποίησης.
GC Pauses — διακοπές νημάτων εφαρμογής κατά τη διάρκεια συλλογής. Στην οθόνη αυτό εκδηλώνεται ως χαμένα καρέ, όταν ο χρόνος μεταξύ δύο καρέ υπερβαίνει τα 16.6 ms (60 FPS). Εάν το GC διαρκεί 30 ms, σχεδιάζεται μόνο ένα καρέ αντί για δύο — ο χρήστης βλέπει τραύλισμα στη διεπαφή.
Κύριες αιτίες μεγάλων παύσεων: μεγάλος αριθμός ζωντανών αντικειμένων στην Old Generation, κατακερματισμός σωρού, συχνά Full GC. Για διάγνωση χρησιμοποιούνται τα Android Studio Profiler και systrace.
Βασικός κανόνας GC-friendly κώδικα — ελαχιστοποίηση του αριθμού δεσμευόμενων αντικειμένων. Κάθε νέο αντικείμενο απαιτεί όχι μόνο δέσμευση μνήμης αλλά και επακόλουθη συλλογή. Ακόμα κι αν το GC είναι γρήγορο, 1000 επιπλέον δεσμεύσεις ανά δευτερόλεπτο σημαίνουν 1000 ελέγχους για τον συλλέκτη.
Διαρροή μνήμης συμβαίνει όταν ένα αντικείμενο παραμένει προσβάσιμο ενώ δεν χρειάζεται πλέον. Το GC δεν μπορεί να διαγράψει ένα τέτοιο αντικείμενο και η μνήμη εξαντλείται σταδιακά. Τυπικές αιτίες: μη αποεγγεγραμμένοι ακροατές, στατικές αναφορές σε Activity, ανώνυμες κλάσεις που συλλαμβάνουν εξωτερικό πλαίσιο και μη κλειστοί Cursor/InputStream.
// Διαρροή μνήμης: ανώνυμη κλάση κρατάει αναφορά στο Activity
public void startTask() {
new Thread(new Runnable() { // έμμεσα κρατάει this (Activity)
@Override
public void run() {
// μακροχρόνια λειτουργία...
System.out.println("Done");
}
}).start();
}
// Διόρθωση: στατική εμφωλευμένη κλάση + WeakReference
private static class TaskRunnable implements Runnable {
private WeakReference<Activity> activityRef;
TaskRunnable(Activity activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
public void run() {
Activity act = activityRef.get();
if (act != null) {
// ασφαλής εργασία με Activity
}
}
}
Σε αυτό το παράδειγμα, το ανώνυμο Runnable συλλαμβάνει έμμεση αναφορά στο Activity. Όσο το νήμα είναι ζωντανό — το Activity δεν μπορεί να συλλεχθεί από το GC, ακόμα κι αν ο χρήστης έχει κλείσει την οθόνη. Η διόρθωση WeakReference + static class σπάει αυτή την αλυσίδα και επιτρέπει στο Activity να αποδεσμευτεί.
Συχνές ερωτήσεις
Το GC στο Android (ART) είναι γενεαλογικός συλλέκτης με concurrent συμπαγοποίηση, βελτιστοποιημένος για κινητές συσκευές με περιορισμένη μνήμη. Το Java GC (G1, ZGC) είναι συλλέκτες διακομιστή με μεγάλους σωρούς και προβλέψιμες παύσεις. ART GC δεν χρησιμοποιεί σημαίες JVM — όλες οι ρυθμίσεις γίνονται αυτόματα σε επίπεδο λειτουργικού συστήματος.
Stop-The-World — η στιγμή που ο συλλέκτης διακόπτει όλα τα νήματα της εφαρμογής για να διασχίσει με ασφάλεια το γράφημα αντικειμένων ή να απελευθερώσει μνήμη. Όσο μεγαλύτερο είναι το STW, τόσο πιο αισθητό είναι το jank. Το ART μείωσε τον τυπικό χρόνο STW σε 2–4 ms χάρη στη γενεαλογική αρχιτεκτονική.
Χρησιμοποιήστε το Android Studio Memory Profiler — δείχνει την αύξηση του σωρού, τον αριθμό δεσμεύσεων και επιτρέπει τη λήψη Heap Dump. Για βαθιά ανάλυση χρησιμοποιήστε LeakCanary — βιβλιοθήκη που εντοπίζει αυτόματα διαρροές και δείχνει την αλυσίδα αναφορών που εμποδίζουν τη συλλογή GC.
Full GC — πλήρης συλλογή όλων των γενεών του σωρού, συμπεριλαμβανομένης της Old Generation. Σε κινητές εφαρμογές, το Full GC μπορεί να διαρκέσει 50–200 ms, προκαλώντας αισθητό jank ή ANR. Κύριες αιτίες: κατακερματισμός σωρού, διαρροές μνήμης, υπέρβαση ορίου Old Generation.
Kotlin παρέχει coroutines με δομημένη ταυτοχρονικότητα — η ακύρωση του scope ακυρώνει αυτόματα όλα τα θυγατρικά coroutines, αποτρέποντας διαρροές. Επίσης, στο Kotlin υπάρχει ο τελεστής lazy για τεμπέλικη αρχικοποίηση και τελεστές εμβέλειας που μειώνουν τον αριθμό προσωρινών αντικειμένων.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης