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

Sizeof για Java

26 Δεκεμβρίου 2003

Ε: Έχει η Java έναν τελεστή σαν το sizeof () στο C;

ΕΝΑ: Μια επιφανειακή απάντηση είναι ότι η Java δεν παρέχει κάτι σαν C μέγεθος του(). Ωστόσο, ας εξετάσουμε Γιατί ένας προγραμματιστής Java μπορεί περιστασιακά να το θέλει.

Ο προγραμματιστής C διαχειρίζεται τον εαυτό του τις περισσότερες εκχωρήσεις μνήμης βάσης δεδομένων και μέγεθος του() είναι απαραίτητο για τη γνώση των μεγεθών του μπλοκ μνήμης για κατανομή. Επιπλέον, οι κατανεμητές μνήμης C αρέσουν malloc () μην κάνετε σχεδόν τίποτα όσον αφορά την αρχικοποίηση αντικειμένων: ένας προγραμματιστής πρέπει να ορίσει όλα τα πεδία αντικειμένων που είναι δείκτες σε περαιτέρω αντικείμενα. Αλλά όταν όλα λέγονται και κωδικοποιούνται, η κατανομή μνήμης C / C ++ είναι αρκετά αποτελεσματική.

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

Φυσικά, αυτό λειτουργεί μόνο για απλές εφαρμογές Java. Σε σύγκριση με το C / C ++, οι αντίστοιχες υποδομές Java τείνουν να καταλαμβάνουν περισσότερη φυσική μνήμη. Στην ανάπτυξη εταιρικού λογισμικού, η προσέγγιση της μέγιστης διαθέσιμης εικονικής μνήμης στα σημερινά JVM 32-bit είναι ένας κοινός περιορισμός κλιμάκωσης. Έτσι, ένας προγραμματιστής Java θα μπορούσε να επωφεληθεί μέγεθος του() ή κάτι παρόμοιο για να παρακολουθεί κανείς εάν οι υποδομές δεδομένων του είναι πολύ μεγάλες ή περιέχουν σημεία συμφόρησης μνήμης. Ευτυχώς, η αντανάκλαση Java σάς επιτρέπει να γράφετε ένα τέτοιο εργαλείο αρκετά εύκολα.

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

Πλάνη: Το Sizeof () δεν απαιτείται επειδή τα μεγέθη βασικών τύπων Java είναι σταθερά

Ναι, μια Java int είναι 32 bit σε όλα τα JVM και σε όλες τις πλατφόρμες, αλλά αυτό είναι μόνο μια απαίτηση προδιαγραφής γλώσσας για το προγραμματιστής αντιληπτός πλάτος αυτού του τύπου δεδομένων. Τέτοιο int είναι ουσιαστικά ένας αφηρημένος τύπος δεδομένων και μπορεί να υποστηρίζεται από, για παράδειγμα, μια λέξη φυσικής μνήμης 64-bit σε μια μηχανή 64-bit. Το ίδιο ισχύει και για τους μη πρωταγωνιστικούς τύπους: η προδιαγραφή γλώσσας Java δεν λέει τίποτα για το πώς τα πεδία τάξης θα πρέπει να ευθυγραμμιστούν στη φυσική μνήμη ή ότι μια σειρά από booleans δεν θα μπορούσε να εφαρμοστεί ως συμπαγής bitvector στο JVM.

Πλάνη: Μπορείτε να μετρήσετε το μέγεθος ενός αντικειμένου σειριοποιώντας το σε ροή byte και κοιτάζοντας το μήκος ροής που προκύπτει

Ο λόγος που αυτό δεν λειτουργεί είναι επειδή η διάταξη σειριοποίησης είναι μόνο μια απομακρυσμένη αντανάκλαση της πραγματικής διάταξης στη μνήμη. Ένας εύκολος τρόπος για να το δείτε είναι να κοιτάξετε πώς Σειράγίνονται σειριοποιημένες: στη μνήμη κάθε απανθρακώνω είναι τουλάχιστον 2 byte, αλλά σε σειριακή μορφή ΣειράΕίναι κωδικοποιημένα UTF-8 και έτσι κάθε περιεχόμενο ASCII παίρνει το μισό χώρο.

Μια άλλη προσέγγιση εργασίας

Ίσως θυμάστε "Συμβουλή Java 130: Γνωρίζετε το μέγεθος των δεδομένων σας;" που περιέγραψε μια τεχνική που βασίζεται στη δημιουργία μεγάλου αριθμού πανομοιότυπων παρουσιών τάξης και τη προσεκτική μέτρηση της προκύπτουσας αύξησης στο μέγεθος σωρού που χρησιμοποιήθηκε από το JVM. Όταν ισχύει, αυτή η ιδέα λειτουργεί πολύ καλά και στην πραγματικότητα θα τη χρησιμοποιήσω για να ξεκινήσω την εναλλακτική προσέγγιση σε αυτό το άρθρο.

Σημειώστε ότι το Java Tip 130's Μέγεθος του Η κλάση απαιτεί ένα ήσυχο JVM (έτσι ώστε η δραστηριότητα σωρού οφείλεται μόνο σε κατανομές αντικειμένων και συλλογές απορριμμάτων που ζητούνται από το νήμα μέτρησης) και απαιτεί μεγάλο αριθμό πανομοιότυπων παρουσιών αντικειμένων. Αυτό δεν λειτουργεί όταν θέλετε να κάνετε μέγεθος ενός μεγάλου αντικειμένου (ίσως ως μέρος μιας εξόδου εντοπισμού σφαλμάτων) και ειδικά όταν θέλετε να εξετάσετε τι πραγματικά το έκανε τόσο μεγάλο.

Ποιο είναι το μέγεθος ενός αντικειμένου;

Η παραπάνω συζήτηση υπογραμμίζει ένα φιλοσοφικό σημείο: δεδομένου ότι συνήθως ασχολείστε με γραφήματα αντικειμένων, ποιος είναι ο ορισμός του μεγέθους ενός αντικειμένου; Είναι μόνο το μέγεθος της παρουσίας αντικειμένου που εξετάζετε ή το μέγεθος ολόκληρου του γραφήματος δεδομένων που έχει τις ρίζες του στην παρουσία αντικειμένου; Το τελευταίο είναι αυτό που συνήθως έχει μεγαλύτερη σημασία στην πράξη. Όπως θα δείτε, τα πράγματα δεν είναι πάντα τόσο ξεκάθαρα, αλλά για αρχάριους μπορείτε να ακολουθήσετε αυτήν την προσέγγιση:

  • Ένα στιγμιότυπο αντικειμένου μπορεί να έχει μέγεθος (περίπου) με το σύνολο όλων των μη στατικών πεδίων δεδομένων του (συμπεριλαμβανομένων των πεδίων που ορίζονται σε superclasses)
  • Σε αντίθεση, ας πούμε, C ++, οι μέθοδοι τάξης και η αρετή τους δεν επηρεάζουν το μέγεθος του αντικειμένου
  • Τα superinterfaces κλάσης δεν επηρεάζουν το μέγεθος του αντικειμένου (δείτε τη σημείωση στο τέλος αυτής της λίστας)
  • Το πλήρες μέγεθος αντικειμένου μπορεί να ληφθεί ως κλείσιμο σε ολόκληρο το γράφημα αντικειμένων που έχει τις ρίζες του στο αρχικό αντικείμενο
Σημείωση: Η εφαρμογή οποιασδήποτε διεπαφής Java επισημαίνει απλώς την εν λόγω κλάση και δεν προσθέτει δεδομένα στον ορισμό της. Στην πραγματικότητα, το JVM δεν επιβεβαιώνει καν ότι μια εφαρμογή διασύνδεσης παρέχει όλες τις μεθόδους που απαιτούνται από τη διεπαφή: αυτό είναι αυστηρά ευθύνη του μεταγλωττιστή στις τρέχουσες προδιαγραφές.

Για να ξεκινήσω τη διαδικασία, για πρωτόγονους τύπους δεδομένων χρησιμοποιώ φυσικά μεγέθη όπως μετρήθηκαν από το Java Tip 130's Μέγεθος του τάξη. Όπως αποδεικνύεται, για τα κοινά JVM 32-bit μια πεδιάδα java.lang.Object καταλαμβάνει 8 byte και οι βασικοί τύποι δεδομένων είναι συνήθως του λιγότερο φυσικού μεγέθους που μπορούν να ικανοποιήσουν τις απαιτήσεις γλώσσας (εκτός boolean καταλαμβάνει ένα ολόκληρο byte):

 // java.lang.Μέγεθος κελύφους αντικειμένου σε byte: δημόσιο στατικό τελικό int OBJECT_SHELL_SIZE = 8; δημόσιο στατικό τελικό int OBJREF_SIZE = 4; δημόσιο τελικό τελικό int LONG_FIELD_SIZE = 8; δημόσιο τελικό τελικό INT_FIELD_SIZE = 4; δημόσιο στατικό τελικό int SHORT_FIELD_SIZE = 2; δημόσιο στατικό τελικό int CHAR_FIELD_SIZE = 2; δημόσιο τελικό τελικό int BYTE_FIELD_SIZE = 1; δημόσιο στατικό τελικό int BOOLEAN_FIELD_SIZE = 1; δημόσιο στατικό τελικό int DOUBLE_FIELD_SIZE = 8; δημόσιο στατικό τελικό int FLOAT_FIELD_SIZE = 4; 

(Είναι σημαντικό να συνειδητοποιήσουμε ότι αυτές οι σταθερές δεν είναι κωδικοποιημένες για πάντα και πρέπει να μετρηθούν ανεξάρτητα για ένα δεδομένο JVM.) Φυσικά, το αφελές σύνολο των μεγεθών πεδίου αντικειμένου παραμελεί τα ζητήματα ευθυγράμμισης μνήμης στο JVM. Η ευθυγράμμιση της μνήμης έχει σημασία (όπως φαίνεται, για παράδειγμα, για πρωτόγονους τύπους συστοιχιών στο Java Tip 130), αλλά νομίζω ότι δεν είναι επικερδές να κυνηγούμε τέτοιες λεπτομέρειες χαμηλού επιπέδου. Όχι μόνο αυτές οι λεπτομέρειες εξαρτώνται από τον πωλητή JVM, αλλά δεν βρίσκονται υπό τον έλεγχο του προγραμματιστή. Στόχος μας είναι να αποκτήσουμε μια καλή εκτίμηση του μεγέθους του αντικειμένου και ελπίζουμε να λάβουμε μια ιδέα όταν ένα πεδίο τάξης μπορεί να είναι περιττό. ή όταν ένα χωράφι θα πρέπει να κατοικεί ή όταν απαιτείται μια πιο συμπαγής ένθετη υποδομή κλπ. Για απόλυτη φυσική ακρίβεια μπορείτε πάντα να επιστρέψετε στο Μέγεθος του τάξη στην Java Συμβουλή 130.

Για να βοηθήσουμε στο προφίλ που αποτελεί μια παρουσία αντικειμένου, το εργαλείο μας δεν θα υπολογίσει μόνο το μέγεθος, αλλά θα δημιουργήσει επίσης μια χρήσιμη υποδομή δεδομένων ως υποπροϊόν: ένα γράφημα που αποτελείται από IObjectProfileNodeμικρό:

διεπαφή IObjectProfileNode {Object αντικείμενο (); Όνομα συμβολοσειράς (); int μέγεθος (); int refcount (); IObjectProfileNode γονέας (); IObjectProfileNode [] παιδιά (); Κέλυφος IObjectProfileNode (); IObjectProfileNode [] διαδρομή (); IObjectProfileNode root (); int μήκος μήκους (); boolean traverse (φίλτρο INodeFilter, επισκέπτης INodeVisitor); Χορδές απόρριψης (); } // Τέλος διεπαφής 

IObjectProfileNodes διασυνδέονται σχεδόν με τον ίδιο ακριβώς τρόπο όπως το αρχικό γράφημα αντικειμένων, με IObjectProfileNode.object () επιστρέφοντας το πραγματικό αντικείμενο που αντιπροσωπεύει κάθε κόμβος. IObjectProfileNode.size () επιστρέφει το συνολικό μέγεθος (σε byte) του δευτερεύοντος δέντρου που έχει τις ρίζες του στην παρουσία αντικειμένου αυτού του κόμβου. Εάν μια παρουσία αντικειμένου συνδέεται με άλλα αντικείμενα μέσω μη μηδενικών πεδίων παρουσίας ή μέσω αναφορών που περιέχονται σε πεδία πίνακα, τότε IObjectProfileNode.children () θα είναι μια αντίστοιχη λίστα θυγατρικών κόμβων γραφημάτων, ταξινομημένων με φθίνουσα σειρά μεγέθους. Αντίθετα, για κάθε κόμβο εκτός του αρχικού, IObjectProfileNode.parent () επιστρέφει τον γονέα του. Ολόκληρη η συλλογή του IObjectProfileNodeΈτσι τεμαχίζει και χωρίζει σε τετράγωνα το αρχικό αντικείμενο και δείχνει πώς χωρίζεται η αποθήκευση δεδομένων μέσα σε αυτό. Επιπλέον, τα ονόματα των κόμβων γραφήματος προέρχονται από τα πεδία κλάσης και εξετάζουν τη διαδρομή ενός κόμβου μέσα στο γράφημα (IObjectProfileNode.path ()) σας επιτρέπει να εντοπίσετε τους συνδέσμους ιδιοκτησίας από την αρχική παρουσία αντικειμένου σε οποιοδήποτε εσωτερικό κομμάτι δεδομένων.

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

 Object obj = new String [] {new String ("JavaWorld"), new String ("JavaWorld")}; 

Καθε java.lang.String Το παράδειγμα έχει ένα εσωτερικό πεδίο τύπου απανθρακώνω[] αυτό είναι το πραγματικό περιεχόμενο συμβολοσειράς. Ο τρόπος Σειρά το copy konstror λειτουργεί σε Java 2 Platform, Standard Edition (J2SE) 1.4, και τα δύο Σειρά παρουσίες εντός του παραπάνω πίνακα θα μοιραστούν το ίδιο απανθρακώνω[] πίνακας που περιέχει το {'J', 'a', 'v', 'a', 'W', 'o', 'r', 'l', 'd'} ακολουθία χαρακτήρων. Και οι δύο χορδές κατέχουν αυτήν τη σειρά εξίσου, οπότε τι πρέπει να κάνετε σε τέτοιες περιπτώσεις;

Αν θέλω πάντα να εκχωρήσω έναν μόνο γονέα σε έναν κόμβο γραφήματος, τότε αυτό το πρόβλημα δεν έχει καθολικά τέλεια απάντηση. Ωστόσο, στην πράξη, πολλές τέτοιες παρουσίες αντικειμένων θα μπορούσαν να εντοπιστούν σε έναν μόνο «φυσικό» γονέα. Μια τέτοια φυσική ακολουθία συνδέσμων είναι συνήθως κοντύτερος από τις άλλες, πιο κυκλικές διαδρομές. Σκεφτείτε ότι τα δεδομένα που επισημαίνονται από τα πεδία παρουσίας ανήκουν περισσότερο σε αυτήν την παρουσία παρά σε οτιδήποτε άλλο. Σκεφτείτε ότι οι καταχωρήσεις σε έναν πίνακα ανήκουν περισσότερο στον ίδιο πίνακα. Έτσι, εάν μπορεί να επιτευχθεί μια εσωτερική παρουσία αντικειμένου μέσω πολλών διαδρομών, επιλέγουμε τη συντομότερη διαδρομή. Εάν έχουμε πολλά μονοπάτια ίσου μήκους, λοιπόν, απλά επιλέγουμε το πρώτο που ανακαλύφθηκε. Στη χειρότερη περίπτωση, αυτή είναι τόσο καλή μια γενική στρατηγική όπως οποιαδήποτε.

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

Μετά από όλα αυτά τα προκαταρκτικά, ακολουθεί μια εφαρμογή εγχειριδίου ενός τέτοιου διαγράμματος γραφήματος. (Ορισμένες λεπτομέρειες και βοηθητικές μέθοδοι έχουν παραλειφθεί. Δείτε τη λήψη αυτού του άρθρου για πλήρεις λεπτομέρειες.):