Προγραμματισμός

Οριστικοποίηση και εκκαθάριση αντικειμένων

Πριν από τρεις μήνες, ξεκίνησα μια μίνι σειρά άρθρων σχετικά με το σχεδιασμό αντικειμένων με μια συζήτηση των αρχών σχεδιασμού που επικεντρώθηκαν στην κατάλληλη αρχικοποίηση στην αρχή της ζωής ενός αντικειμένου. Σε αυτό Τεχνικές σχεδιασμού άρθρο, θα επικεντρωθώ στις αρχές σχεδίασης που θα σας βοηθήσουν να διασφαλίσετε τον κατάλληλο καθαρισμό στο τέλος της ζωής ενός αντικειμένου.

Γιατί να καθαρίσετε;

Κάθε αντικείμενο σε ένα πρόγραμμα Java χρησιμοποιεί υπολογιστικούς πόρους που είναι πεπερασμένοι. Προφανώς, όλα τα αντικείμενα χρησιμοποιούν κάποια μνήμη για να αποθηκεύουν τις εικόνες τους στο σωρό. (Αυτό ισχύει ακόμη και για αντικείμενα που δεν δηλώνουν μεταβλητές παρουσίας. Κάθε εικόνα αντικειμένου πρέπει να περιλαμβάνει κάποιο είδος δείκτη προς δεδομένα κλάσης και μπορεί να περιλαμβάνει και άλλες πληροφορίες που εξαρτώνται από την εφαρμογή.) Αλλά τα αντικείμενα ενδέχεται επίσης να χρησιμοποιούν άλλους περιορισμένους πόρους εκτός από τη μνήμη. Για παράδειγμα, ορισμένα αντικείμενα ενδέχεται να χρησιμοποιούν πόρους όπως λαβές αρχείων, περιβάλλοντα γραφικών, υποδοχές και ούτω καθεξής. Όταν σχεδιάζετε ένα αντικείμενο, πρέπει να βεβαιωθείτε ότι απελευθερώνει τελικά τους πεπερασμένους πόρους που χρησιμοποιεί, έτσι ώστε το σύστημα να μην εξαντλείται από αυτούς τους πόρους.

Επειδή η Java είναι μια γλώσσα που συλλέγεται σκουπίδια, η απελευθέρωση της μνήμης που σχετίζεται με ένα αντικείμενο είναι εύκολη. Το μόνο που χρειάζεται να κάνετε είναι να αφήσετε όλες τις αναφορές στο αντικείμενο. Επειδή δεν χρειάζεται να ανησυχείτε για την απελευθέρωση ενός αντικειμένου, όπως πρέπει σε γλώσσες όπως C ή C ++, δεν χρειάζεται να ανησυχείτε για καταστροφή της μνήμης ελευθερώνοντας κατά λάθος το ίδιο αντικείμενο δύο φορές. Πρέπει, ωστόσο, να βεβαιωθείτε ότι έχετε απελευθερώσει όλες τις αναφορές στο αντικείμενο. Εάν δεν το κάνετε, μπορείτε να καταλήξετε σε διαρροή μνήμης, όπως ακριβώς και η διαρροή μνήμης που λαμβάνετε σε ένα πρόγραμμα C ++ όταν ξεχάσετε να απελευθερώσετε ρητά αντικείμενα. Ωστόσο, αρκεί να απελευθερώνετε όλες τις αναφορές σε ένα αντικείμενο, δεν χρειάζεται να ανησυχείτε για την "απελευθέρωση" αυτής της μνήμης.

Παρομοίως, δεν χρειάζεται να ανησυχείτε για την απελευθέρωση ρητά τυχόν συστατικών αντικειμένων που αναφέρονται από τις μεταβλητές παρουσίας ενός αντικειμένου που δεν χρειάζεστε πλέον. Η απελευθέρωση όλων των αναφορών στο άχρηστο αντικείμενο θα ακυρώσει ουσιαστικά τυχόν αναφορές αντικειμένων συστατικών που περιέχονται στις μεταβλητές παρουσίας του αντικειμένου. Εάν οι πλέον μη έγκυρες αναφορές ήταν οι μόνες παραμένουσες αναφορές σε αυτά τα συστατικά αντικείμενα, τα συστατικά αντικείμενα θα είναι επίσης διαθέσιμα για συλλογή απορριμμάτων. Κομμάτι κέικ, σωστά;

Οι κανόνες συλλογής απορριμμάτων

Παρόλο που η συλλογή απορριμμάτων πράγματι κάνει τη διαχείριση μνήμης στην Java πολύ πιο εύκολη από ό, τι σε C ή C ++, δεν μπορείτε να ξεχάσετε εντελώς τη μνήμη όταν προγραμματίζετε στην Java. Για να μάθετε πότε μπορεί να χρειαστεί να σκεφτείτε τη διαχείριση μνήμης στην Java, πρέπει να μάθετε λίγο για τον τρόπο επεξεργασίας της συλλογής απορριμμάτων στις προδιαγραφές Java.

Δεν απαιτείται εντολή συλλογής απορριμμάτων

Το πρώτο πράγμα που πρέπει να γνωρίζετε είναι ότι ανεξάρτητα από το πόσο επιμελώς κάνετε αναζήτηση μέσω της προδιαγραφής Java Virtual Machine (Spec JVM), δεν θα μπορείτε να βρείτε οποιαδήποτε πρόταση που διατάζει, Κάθε JVM πρέπει να έχει έναν συλλέκτη απορριμμάτων. Η προδιαγραφή Java Virtual Machine δίνει στους σχεδιαστές της VM πολλά περιθώρια για να αποφασίσουν πώς θα διαχειριστούν οι εφαρμογές τους τη μνήμη, συμπεριλαμβανομένης της απόφασης εάν θα χρησιμοποιήσουν ή όχι τη συλλογή απορριμμάτων καθόλου. Έτσι, είναι πιθανό ότι ορισμένα JVM (όπως μια έξυπνη κάρτα JVM με γυμνά οστά) ενδέχεται να απαιτούν τα προγράμματα που εκτελούνται σε κάθε συνεδρία να "ταιριάζουν" στη διαθέσιμη μνήμη.

Φυσικά, μπορείτε πάντα να εξαντλήσετε τη μνήμη, ακόμη και σε ένα εικονικό σύστημα μνήμης. Το JVM Spec δεν αναφέρει πόση μνήμη θα είναι διαθέσιμη σε ένα JVM. Απλώς δηλώνει ότι όποτε μια JVM κάνει εξαντληθεί η μνήμη, θα πρέπει να ρίξει ένα Σφάλμα OutOfMemory.

Παρ 'όλα αυτά, για να δώσουμε στις εφαρμογές Java την καλύτερη ευκαιρία εκτέλεσης χωρίς να εξαντληθεί η μνήμη, οι περισσότεροι JVM θα χρησιμοποιήσουν έναν συλλέκτη απορριμμάτων. Ο συλλέκτης απορριμμάτων ανακτά τη μνήμη που καταλαμβάνεται από αντικείμενα χωρίς αναφορές στο σωρό, έτσι ώστε η μνήμη να μπορεί να χρησιμοποιηθεί ξανά από νέα αντικείμενα και συνήθως αποσυνθέτει τον σωρό καθώς εκτελείται το πρόγραμμα.

Ο αλγόριθμος συλλογής απορριμμάτων δεν έχει οριστεί

Μια άλλη εντολή που δεν θα βρείτε στην προδιαγραφή JVM είναι Όλα τα JVM που χρησιμοποιούν τη συλλογή απορριμμάτων πρέπει να χρησιμοποιούν τον αλγόριθμο XXX. Οι σχεδιαστές κάθε JVM αποφασίζουν πώς θα λειτουργεί η συλλογή απορριμμάτων στις υλοποιήσεις τους. Ο αλγόριθμος συλλογής απορριμμάτων είναι ένας τομέας στον οποίο οι πωλητές JVM μπορούν να προσπαθήσουν να κάνουν την υλοποίησή τους καλύτερη από την ανταγωνιστική. Αυτό είναι σημαντικό για εσάς ως προγραμματιστής Java για τον ακόλουθο λόγο:

Επειδή γενικά δεν γνωρίζετε πώς θα εκτελεστεί η συλλογή απορριμμάτων μέσα σε ένα JVM, δεν ξέρετε πότε θα γίνει συλλογή συγκεκριμένων αντικειμένων.

Και λοιπόν? μπορεί να ρωτήσετε. Ο λόγος που ίσως σας ενδιαφέρει όταν ένα αντικείμενο συλλέγεται σκουπίδια έχει να κάνει με τους οριστικοποιητές. (ΕΝΑ οριστικοποιητής ορίζεται ως μια κανονική μέθοδος παρουσίας Java που ονομάζεται Οριστικοποιώ() που επιστρέφει άκυρο και δεν παίρνει επιχειρήματα.) Οι προδιαγραφές Java κάνουν την ακόλουθη υπόσχεση για οριστικοποιητές:

Πριν από την ανάκτηση της μνήμης που καταλαμβάνεται από ένα αντικείμενο που έχει οριστικοποιητή, ο συλλέκτης απορριμμάτων θα επικαλεστεί την οριστικοποίηση αυτού του αντικειμένου.

Δεδομένου ότι δεν γνωρίζετε πότε θα συλλέγονται αντικείμενα, αλλά γνωρίζετε ότι τα οριστικοποιήσιμα αντικείμενα θα οριστικοποιηθούν καθώς συλλέγονται σκουπίδια, μπορείτε να κάνετε την ακόλουθη μεγάλη αφαίρεση:

Δεν ξέρετε πότε θα οριστικοποιηθούν τα αντικείμενα.

Πρέπει να αποτυπώσετε αυτό το σημαντικό γεγονός στον εγκέφαλό σας και να του επιτρέψετε για πάντα να ενημερώνει τα σχέδια αντικειμένων Java.

Οριστικοποιητές προς αποφυγή

Ο κεντρικός κανόνας σχετικά με τους οριστικοποιητές είναι ο εξής:

Μην σχεδιάζετε τα προγράμματα Java έτσι ώστε η ορθότητα να εξαρτάται από την "έγκαιρη" οριστικοποίηση.

Με άλλα λόγια, μην γράφετε προγράμματα που θα σπάσουν εάν ορισμένα αντικείμενα δεν οριστικοποιηθούν από συγκεκριμένα σημεία στη ζωή της εκτέλεσης του προγράμματος. Εάν γράψετε ένα τέτοιο πρόγραμμα, μπορεί να λειτουργήσει σε ορισμένες υλοποιήσεις του JVM αλλά να αποτύχει σε άλλους.

Μην βασίζεστε σε οριστικοποιητές για την απελευθέρωση πόρων χωρίς μνήμη

Ένα παράδειγμα ενός αντικειμένου που παραβιάζει αυτόν τον κανόνα είναι αυτό που ανοίγει ένα αρχείο στον κατασκευαστή του και κλείνει το αρχείο σε αυτό Οριστικοποιώ() μέθοδος. Αν και αυτός ο σχεδιασμός φαίνεται τακτοποιημένος, τακτοποιημένος και συμμετρικός, δημιουργεί δυνητικά ένα ύπουλο σφάλμα. Ένα πρόγραμμα Java γενικά θα έχει μόνο έναν πεπερασμένο αριθμό χειρισμών αρχείων. Όταν χρησιμοποιούνται όλες αυτές οι λαβές, το πρόγραμμα δεν θα μπορεί να ανοίξει άλλα αρχεία.

Ένα πρόγραμμα Java που χρησιμοποιεί ένα τέτοιο αντικείμενο (ένα που ανοίγει ένα αρχείο στον κατασκευαστή του και το κλείνει στον οριστικοποιητή του) μπορεί να λειτουργεί καλά σε ορισμένες υλοποιήσεις JVM. Σε τέτοιες υλοποιήσεις, η οριστικοποίηση θα συνέβαινε αρκετά συχνά ώστε να διατηρείται διαθέσιμος επαρκής αριθμός λαβών αρχείων ανά πάσα στιγμή. Αλλά το ίδιο πρόγραμμα μπορεί να αποτύχει σε ένα διαφορετικό JVM του οποίου ο συλλέκτης απορριμμάτων δεν ολοκληρώνεται αρκετά συχνά για να αποτρέψει την εκτέλεση του προγράμματος από τις λαβές αρχείων. Ή, αυτό που είναι ακόμη πιο ύπουλο, το πρόγραμμα μπορεί να λειτουργήσει σε όλες τις υλοποιήσεις JVM τώρα, αλλά αποτυγχάνει σε μια κρίσιμη κατάσταση για αποστολή λίγα χρόνια (και κύκλους απελευθέρωσης) στο δρόμο.

Άλλοι βασικοί κανόνες οριστικοποίησης

Δύο άλλες αποφάσεις που απομένουν στους σχεδιαστές της JVM επιλέγουν το νήμα (ή νήματα) που θα εκτελέσει τους οριστικοποιητές και τη σειρά με την οποία θα εκτελεστούν οι οριστικοποιητές. Οι οριστικοποιητές μπορούν να εκτελεστούν με οποιαδήποτε σειρά - διαδοχικά από ένα μόνο νήμα ή ταυτόχρονα από πολλά νήματα. Εάν το πρόγραμμά σας εξαρτάται κατά κάποιον τρόπο από την ορθότητα της εκτέλεσης των οριστικοποιητών σε μια συγκεκριμένη σειρά ή από ένα συγκεκριμένο νήμα, μπορεί να λειτουργήσει σε ορισμένες υλοποιήσεις JVM αλλά να αποτύχει σε άλλες.

Θα πρέπει επίσης να έχετε κατά νου ότι η Java θεωρεί ότι ένα αντικείμενο θα οριστικοποιηθεί εάν το Οριστικοποιώ() Η μέθοδος επιστρέφει κανονικά ή ολοκληρώνεται απότομα ρίχνοντας μια εξαίρεση. Οι συλλέκτες απορριμμάτων αγνοούν τυχόν εξαιρέσεις που ρίχνονται από τους οριστικοποιητές και με κανέναν τρόπο δεν ενημερώνουν την υπόλοιπη εφαρμογή ότι απορρίφθηκε μια εξαίρεση. Εάν πρέπει να διασφαλίσετε ότι ένας συγκεκριμένος οριστικοποιητής ολοκληρώνει πλήρως μια συγκεκριμένη αποστολή, πρέπει να γράψετε αυτόν τον οριστικοποιητή έτσι ώστε να χειρίζεται τυχόν εξαιρέσεις που ενδέχεται να προκύψουν πριν από την ολοκλήρωση της αποστολής του.

Ένας ακόμη βασικός κανόνας σχετικά με τους οριστικοποιητές αφορά αντικείμενα που έχουν απομείνει στο σωρό στο τέλος της διάρκειας ζωής της εφαρμογής. Από προεπιλογή, ο συλλέκτης απορριμμάτων δεν θα εκτελεί τους οριστικοποιητές οποιωνδήποτε αντικειμένων που έχουν απομείνει στο σωρό όταν τερματίζεται η εφαρμογή. Για να αλλάξετε αυτήν την προεπιλογή, πρέπει να καλέσετε το runFinalizersOnExit () μέθοδος τάξης Χρόνος εκτέλεσης ή Σύστημα, πέρασμα αληθής ως η μοναδική παράμετρος. Εάν το πρόγραμμά σας περιέχει αντικείμενα των οποίων οι οριστικοποιητές πρέπει να επικαλεσθούν απολύτως πριν από την έξοδο του προγράμματος, φροντίστε να το κάνετε runFinalizersOnExit () κάπου στο πρόγραμμά σας.

Λοιπόν, για ποιο λόγο είναι οι οριστικοποιητές;

Μέχρι τώρα μπορεί να έχετε την αίσθηση ότι δεν έχετε μεγάλη χρήση για τους οριστικοποιητές. Αν και είναι πιθανό ότι τα περισσότερα από τα μαθήματα που σχεδιάζετε δεν θα περιλαμβάνουν οριστικοποιητή, υπάρχουν ορισμένοι λόγοι για τη χρήση οριστικοποιητών.

Μια λογική, αν και σπάνια, εφαρμογή για οριστικοποιητή είναι η απελευθέρωση της μνήμης που κατανέμεται με εγγενείς μεθόδους. Εάν ένα αντικείμενο επικαλείται μια εγγενή μέθοδο που εκχωρεί μνήμη (ίσως μια συνάρτηση C που καλεί malloc ()), ο οριστικοποιητής αυτού του αντικειμένου θα μπορούσε να επικαλεστεί μια εγγενή μέθοδο που ελευθερώνει αυτήν τη μνήμη (κλήσεις Ελεύθερος()). Σε αυτήν την περίπτωση, θα χρησιμοποιούσατε τον οριστικοποιητή για να ελευθερώσετε τη μνήμη που εκχωρήθηκε για λογαριασμό ενός αντικειμένου - μνήμη που δεν θα ανακτηθεί αυτόματα από τον συλλέκτη απορριμμάτων.

Μια άλλη, πιο συνηθισμένη, χρήση οριστικοποιητών είναι η παροχή ενός εναλλακτικού μηχανισμού για την απελευθέρωση πεπερασμένων πόρων χωρίς μνήμη, όπως λαβές αρχείων ή υποδοχές. Όπως αναφέρθηκε προηγουμένως, δεν πρέπει να βασίζεστε σε οριστικοποιητές για την απελευθέρωση πεπερασμένων πόρων χωρίς μνήμη. Αντ 'αυτού, θα πρέπει να παρέχετε μια μέθοδο που θα απελευθερώνει τον πόρο. Αλλά μπορεί επίσης να θέλετε να συμπεριλάβετε έναν οριστικοποιητή που ελέγχει για να βεβαιωθείτε ότι ο πόρος έχει ήδη κυκλοφορήσει και, εάν δεν έχει, προχωρά και τον αποδεσμεύει. Ένας τέτοιος οριστικοποιητής προστατεύει από (και ελπίζουμε ότι δεν θα ενθαρρύνει) την ατημέλητη χρήση της τάξης σας. Εάν ένας προγραμματιστής πελάτη ξεχάσει να επικαλεστεί τη μέθοδο που παρείχατε για την απελευθέρωση του πόρου, ο οριστικοποιητής θα αποδεσμεύσει τον πόρο εάν το αντικείμενο συλλέγεται ποτέ σκουπίδια. ο Οριστικοποιώ() μέθοδος του LogFileManager τάξη, που εμφανίζεται αργότερα σε αυτό το άρθρο, είναι ένα παράδειγμα αυτού του είδους οριστικοποίησης.

Αποφύγετε την κατάχρηση οριστικοποίησης

Η ύπαρξη οριστικοποίησης παράγει μερικές ενδιαφέρουσες επιπλοκές για τα JVM και μερικές ενδιαφέρουσες δυνατότητες για προγραμματιστές Java. Αυτό που δίνει η οριστικοποίηση στους προγραμματιστές είναι η δύναμη της ζωής και του θανάτου των αντικειμένων. Εν ολίγοις, είναι δυνατό και απολύτως νόμιμο στην Java να αναστηθούν αντικείμενα σε οριστικοποιητές - να τα ξαναζωντανεύουν κάνοντάς τα να αναφερθούν ξανά. (Ένας τρόπος με τον οποίο θα μπορούσε να επιτευχθεί κάτι τέτοιο είναι με την προσθήκη μιας αναφοράς στο αντικείμενο που θα οριστικοποιηθεί σε μια στατική συνδεδεμένη λίστα που εξακολουθεί να είναι "ζωντανή".) Παρόλο που μια τέτοια δύναμη μπορεί να είναι δελεαστική να ασκηθείτε επειδή σας κάνει να νιώθετε σημαντικοί, ο κανόνας είναι να αντισταθείτε στον πειρασμό να χρησιμοποιήσετε αυτήν τη δύναμη. Γενικά, η ανάσταση αντικειμένων σε οριστικοποιητές αποτελεί κατάχρηση οριστικοποιητή.

Η κύρια δικαιολογία για αυτόν τον κανόνα είναι ότι οποιοδήποτε πρόγραμμα που χρησιμοποιεί την ανάσταση μπορεί να επανασχεδιαστεί σε ένα πιο κατανοητό πρόγραμμα που δεν χρησιμοποιεί την ανάσταση. Μια επίσημη απόδειξη αυτού του θεωρήματος αφήνεται ως άσκηση στον αναγνώστη (πάντα ήθελα να το πω), αλλά με ένα άτυπο πνεύμα, θεωρήστε ότι η ανάσταση του αντικειμένου θα είναι τόσο τυχαία και απρόβλεπτη όσο η οριστικοποίηση του αντικειμένου. Ως εκ τούτου, ένας σχεδιασμός που χρησιμοποιεί την ανάσταση θα είναι δύσκολο να καταλάβουμε από τον επόμενο προγραμματιστή συντήρησης που συμβαίνει μαζί - ο οποίος μπορεί να μην κατανοεί πλήρως τις ιδιοσυγκρασίες της συλλογής απορριμμάτων στην Java.

Εάν πιστεύετε ότι πρέπει απλώς να ξαναζωντανέψετε ένα αντικείμενο, σκεφτείτε να κλωνοποιήσετε ένα νέο αντίγραφο του αντικειμένου αντί να αναστήσετε το ίδιο παλιό αντικείμενο. Ο λόγος πίσω από αυτό το κομμάτι συμβουλών είναι ότι οι συλλέκτες σκουπιδιών στο JVM επικαλούνται το Οριστικοποιώ() μέθοδος ενός αντικειμένου μόνο μία φορά. Εάν το αντικείμενο αναβιώσει και γίνει διαθέσιμο για συλλογή απορριμμάτων για δεύτερη φορά, το αντικείμενο Οριστικοποιώ() δεν θα επικαλεστεί ξανά τη μέθοδο.