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

Συμβουλή Java 130: Γνωρίζετε το μέγεθος των δεδομένων σας;

Πρόσφατα, βοήθησα στο σχεδιασμό μιας εφαρμογής διακομιστή Java που έμοιαζε με μια βάση δεδομένων στη μνήμη. Δηλαδή, μεροληψήσαμε τον σχεδιασμό προς αποθήκευση τόνων δεδομένων στη μνήμη για να παρέχουμε εξαιρετικά γρήγορη απόδοση ερωτήματος.

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

Σημείωση: Μπορείτε να κατεβάσετε τον πηγαίο κώδικα αυτού του άρθρου από τους πόρους.

Το εργαλείο

Δεδομένου ότι η Java κρύβει σκόπιμα πολλές πτυχές της διαχείρισης μνήμης, ανακαλύπτοντας πόση μνήμη καταναλώνουν τα αντικείμενα σας χρειάζεται δουλειά. Θα μπορούσατε να χρησιμοποιήσετε το Runtime.freeMemory () μέθοδος για τη μέτρηση των διαφορών μεγέθους σωρού πριν και μετά την κατανομή πολλών αντικειμένων. Διάφορα άρθρα, όπως το "Ερώτηση της εβδομάδας αρ. 107" του Ramchander Varadarajan (Sun Microsystems, Σεπτέμβριος 2000) και το "Memory Matters" του Tony Sintes (JavaWorld, Δεκέμβριος 2001), αναλύστε αυτήν την ιδέα. Δυστυχώς, η λύση του πρώην άρθρου αποτυγχάνει επειδή η εφαρμογή χρησιμοποιεί λάθος Χρόνος εκτέλεσης μέθοδος, ενώ η λύση του τελευταίου άρθρου έχει τις δικές της ατέλειες:

  • Μια κλήση προς Runtime.freeMemory () αποδεικνύεται ανεπαρκής επειδή ένας JVM μπορεί να αποφασίσει να αυξήσει το τρέχον μέγεθος σωρού του ανά πάσα στιγμή (ειδικά όταν εκτελεί συλλογή απορριμμάτων). Εκτός αν το συνολικό μέγεθος σωρού είναι ήδη στο μέγιστο μέγεθος -Xmx, πρέπει να το χρησιμοποιήσουμε Runtime.totalMemory () - Runtime.freeMemory () ως το χρησιμοποιημένο μέγεθος σωρού.
  • Εκτέλεση ενός μόνο Runtime.gc () η κλήση ενδέχεται να μην είναι αρκετά επιθετική για να ζητήσετε τη συλλογή απορριμμάτων. Θα μπορούσαμε, για παράδειγμα, να ζητήσουμε την εκτέλεση οριστικοποιητών αντικειμένων. Και από τότε Runtime.gc () δεν έχει τεκμηριωθεί για αποκλεισμό έως ότου ολοκληρωθεί η συλλογή, είναι καλή ιδέα να περιμένετε έως ότου σταθεροποιηθεί το αντιληπτό μέγεθος σωρού.
  • Εάν η κλάση προφίλ δημιουργεί οποιαδήποτε στατικά δεδομένα ως μέρος της αρχικοποίησης κλάσης ανά κατηγορία (συμπεριλαμβανομένων των στατικών αρχικών κλάσεων και πεδίων), η σωστή μνήμη που χρησιμοποιείται για την παρουσία πρώτης τάξης μπορεί να περιλαμβάνει αυτά τα δεδομένα. Πρέπει να αγνοήσουμε το σωρό που καταναλώνεται από την πρώτη κατηγορία.

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

public class Sizeof {public static void main (String [] args) ρίχνει Εξαίρεση {// Ζεσταίνουμε όλες τις τάξεις / μεθόδους που θα χρησιμοποιήσουμε το runGC (); μεταχειρισμένο Memory (); // Πίνακας για να διατηρείτε ισχυρές αναφορές σε κατανεμημένα αντικείμενα τελικό int count = 100000; Object [] object = νέο αντικείμενο [count]; μακρύς σωρός1 = 0; // Κατανομή μετρήσεων + 1 αντικειμένων, απορρίψτε το πρώτο για αντικείμενα (int i = -1; i = 0) [i] = αντικείμενο; αλλιώς {αντικείμενο = null; // Απορρίψτε το αντικείμενο προθέρμανσης runGC (); heap1 = usedMemory (); // Πάρτε ένα στιγμιότυπο πριν από το σωρό}} runGC (); long heap2 = usedΜνήμη (); // Λάβετε ένα στιγμιότυπο μετά το σωρό: τελικό int μέγεθος = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'before' heap:" + heap1 + ", 'after' heap:" + heap2); System.out.println ("heap delta:" + (heap2 - heap1) + ", {" + Objects [0] .getClass () + "} size =" + size + "bytes"); για (int i = 0; i <count; ++ i) αντικείμενα [i] = null; αντικείμενα = null; } ιδιωτικό static void runGC () ρίχνει Εξαίρεση {// Βοηθά να καλέσετε το Runtime.gc () // χρησιμοποιώντας διάφορες μεθόδους κλήσεις: για (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () ρίχνει την εξαίρεση {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; για (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread (). Απόδοση (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} ιδιωτικό static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } ιδιωτικό στατικό τελικό Runtime s_runtime = Runtime.getRuntime (); } // Τέλος μαθήματος 

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

Σημειώστε προσεκτικά τα μέρη όπου επικαλούμαι runGC (). Μπορείτε να επεξεργαστείτε τον κωδικό μεταξύ του σωρός1 και σωρός2 δηλώσεις για την παρουσίαση οποιουδήποτε ενδιαφέροντος.

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

Τα αποτελέσματα

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

Σημείωση: Τα ακόλουθα αποτελέσματα βασίζονται στο Sun's JDK 1.3.1 για Windows. Λόγω του τι είναι και δεν είναι εγγυημένο από τη γλώσσα Java και τις προδιαγραφές JVM, δεν μπορείτε να εφαρμόσετε αυτά τα συγκεκριμένα αποτελέσματα σε άλλες πλατφόρμες ή άλλες εφαρμογές Java.

java.lang.Object

Λοιπόν, η ρίζα όλων των αντικειμένων έπρεπε να είναι η πρώτη μου υπόθεση. Για java.lang.Object, Παίρνω:

«πριν» σωρός: 510696, «μετά» σωρός: 1310696 σωρός δέλτα: 800000, {class java.lang.Object} μέγεθος = 8 bytes 

Λοιπόν, μια πεδιάδα Αντικείμενο παίρνει 8 bytes. Φυσικά, κανείς δεν πρέπει να περιμένει το μέγεθος να είναι 0, καθώς κάθε περίπτωση πρέπει να φέρει γύρω από πεδία που υποστηρίζουν βασικές λειτουργίες όπως ισούται με (), hashCode (), περιμένετε () / ειδοποιήστε (), και ούτω καθεξής.

java.lang.Integer

Οι συνάδελφοί μου και εγώ συχνά τυλίγουμε ντόπια ints σε Ακέραιος αριθμός παρουσίες, ώστε να μπορούμε να τις αποθηκεύσουμε σε συλλογές Java. Πόσο μας κοστίζει στη μνήμη;

«πριν» σωρός: 510696, «μετά» σωρός: 2110696 σωρός δέλτα: 1600000, {class java.lang.Integer} μέγεθος = 16 byte 

Το αποτέλεσμα των 16 byte είναι λίγο χειρότερο από ό, τι περίμενα γιατί ένα int Η τιμή μπορεί να χωρέσει σε μόλις 4 επιπλέον byte. Χρησιμοποιώντας ένα Ακέραιος αριθμός μου κοστίζει 300 τοις εκατό γενικά μνήμη σε σύγκριση με όταν μπορώ να αποθηκεύσω την τιμή ως πρωτόγονος τύπος.

java.lang.Long

Μακρύς θα πρέπει να πάρει περισσότερη μνήμη από Ακέραιος αριθμός, αλλά δεν:

«πριν» σωρός: 510696, «μετά» σωρός: 2110696 σωρός δέλτα: 1600000, {class java.lang.Long} μέγεθος = 16 bytes 

Είναι σαφές ότι το πραγματικό μέγεθος αντικειμένου στο σωρό υπόκειται σε ευθυγράμμιση μνήμης χαμηλού επιπέδου που γίνεται από μια συγκεκριμένη υλοποίηση JVM για έναν συγκεκριμένο τύπο CPU. Μοιάζει με Μακρύς είναι 8 byte των Αντικείμενο γενικά, συν 8 bytes περισσότερο για την πραγματική μεγάλη τιμή. Σε αντίθεση, Ακέραιος αριθμός είχε μια αχρησιμοποίητη τρύπα 4-byte, πιθανότατα επειδή το JVM I χρησιμοποιεί δυνάμεις ευθυγράμμισης αντικειμένων σε όριο λέξεων 8-byte.

Πίνακες

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

μήκος: 0, {class [I} size = 16 bytes length: 1, {class [I} size = 16 bytes length: 2, {class [I} size = 24 bytes length: 3, {class [I} size = Μήκος 24 bytes: 4, {class [I} size = 32 bytes length: 5, {class [I} size = 32 bytes length: 6, {class [I} size = 40 bytes length: 7, {class [I} μέγεθος = 40 bytes μήκος: 8, {class [I} size = 48 bytes length: 9, {class [I} size = 48 bytes length: 10, {class [I} size = 56 bytes 

και για απανθρακώνω συστοιχίες:

μήκος: 0, {class [C} size = 16 bytes length: 1, {class [C} size = 16 bytes length: 2, {class [C} size = 16 bytes length: 3, {class [C} size = Μήκος 24 bytes: 4, {class [C} size = 24 bytes length: 5, {class [C} size = 24 bytes length: 6, {class [C} size = 24 bytes length: 7, {class [C} μέγεθος = 32 bytes μήκος: 8, {class [C} size = 32 bytes length: 9, {class [C} size = 32 bytes length: 10, {class [C} size = 32 bytes] 

Πάνω, τα στοιχεία της ευθυγράμμισης 8 byte εμφανίζονται ξανά. Επίσης, εκτός από το αναπόφευκτο Αντικείμενο 8-byte overhead, ένας πρωτόγονος πίνακας προσθέτει άλλα 8 byte (εκ των οποίων τουλάχιστον 4 byte υποστηρίζουν το μήκος πεδίο). Και χρησιμοποιώντας int [1] φαίνεται να μην προσφέρει πλεονεκτήματα στη μνήμη έναντι ενός Ακέραιος αριθμός παράδειγμα, εκτός ίσως ως μεταβλητή έκδοση των ίδιων δεδομένων.

Πολυδιάστατες συστοιχίες

Οι πολυδιάστατες συστοιχίες προσφέρουν μια άλλη έκπληξη. Οι προγραμματιστές χρησιμοποιούν συνήθως κατασκευές όπως int [dim1] [dim2] στην αριθμητική και επιστημονική πληροφορική. Σε ένα int [dim1] [dim2] παρουσία πίνακα, κάθε ένθετο int [dim2] ο πίνακας είναι ένα Αντικείμενο από μόνη της. Καθένα προσθέτει τη συνηθισμένη γενική συστοιχία 16 byte. Όταν δεν χρειάζομαι μια τριγωνική ή κουρελιασμένη συστοιχία, που αντιπροσωπεύει καθαρή επιβάρυνση. Η επίδραση αυξάνεται όταν οι διαστάσεις του πίνακα διαφέρουν πολύ. Για παράδειγμα, α int [128] [2] Το παράδειγμα διαρκεί 3.600 byte. Σε σύγκριση με τα 1.040 byte an int [256] χρήσεις παρουσίας (η οποία έχει την ίδια χωρητικότητα), 3.600 byte αντιπροσωπεύουν 246 τοις εκατό γενικά. Στην ακραία περίπτωση του byte [256] [1], ο γενικός συντελεστής είναι σχεδόν 19! Συγκρίνετε αυτό με την κατάσταση C / C ++ στην οποία η ίδια σύνταξη δεν προσθέτει επιβάρυνση αποθήκευσης.

java.lang.String

Ας δοκιμάσουμε ένα άδειο Σειρά, κατασκευάστηκε για πρώτη φορά ως νέα χορδή ():

«πριν» σωρός: 510696, «μετά» σωρός: 4510696 σωρός δέλτα: 4000000, {class java.lang.String} μέγεθος = 40 bytes 

Το αποτέλεσμα αποδεικνύεται αρκετά καταθλιπτικό. Ενα άδειο Σειρά διαρκεί 40 byte - αρκετή μνήμη για να χωρέσει 20 χαρακτήρες Java.

Πριν δοκιμάσω Σειράs με περιεχόμενο, χρειάζομαι μια βοηθητική μέθοδο για να δημιουργήσω ΣειράΕγγυημένο ότι δεν θα απαλλαγεί. Χρησιμοποιείτε μόνο κυριολεκτικά όπως:

 αντικείμενο = "συμβολοσειρά με 20 χαρακτήρες"; 

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

 δημόσιο στατικό String createString (τελικό int μήκος) {char [] αποτέλεσμα = νέο char [μήκος]; για (int i = 0; i <length; ++ i) αποτέλεσμα [i] = (char) i; επιστροφή νέας συμβολοσειράς (αποτέλεσμα) } 

Αφού οπλισμού με αυτό Σειρά μέθοδο δημιουργού, λαμβάνω τα ακόλουθα αποτελέσματα:

μήκος: 0, {class java.lang.String} μέγεθος = 40 bytes μήκος: 1, {class java.lang.String} μέγεθος = 40 bytes μήκος: 2, {class java.lang.String} μέγεθος = 40 bytes μήκος: 3, {class java.lang.String} μέγεθος = 48 bytes μήκος: 4, {class java.lang.String} μέγεθος = 48 bytes μήκος: 5, {class java.lang.String} μέγεθος = 48 bytes μήκος: 6, {class java.lang.String} μέγεθος = 48 bytes μήκος: 7, {class java.lang.String} μέγεθος = 56 bytes μήκος: 8, {class java.lang.String} μέγεθος = 56 bytes μήκος: 9, {class java.lang.String} μέγεθος = 56 bytes μήκος: 10, {class java.lang.String} μέγεθος = 56 bytes 

Τα αποτελέσματα δείχνουν σαφώς ότι α ΣειράΗ αύξηση της μνήμης παρακολουθεί την εσωτερική της απανθρακώνω ανάπτυξη του πίνακα. Ωστόσο, το Σειρά Η τάξη προσθέτει άλλα 24 byte γενικά. Για μια απελευθέρωση Σειρά μεγέθους 10 χαρακτήρων ή λιγότερο, το προστιθέμενο γενικό κόστος σε σχέση με το ωφέλιμο ωφέλιμο φορτίο (2 byte για κάθε ένα απανθρακώνω συν 4 byte για το μήκος), κυμαίνεται από 100 έως 400 τοις εκατό.

Φυσικά, η ποινή εξαρτάται από τη διανομή δεδομένων της εφαρμογής σας. Κάπως υποψιάστηκα ότι 10 χαρακτήρες αντιπροσωπεύουν το τυπικό Σειρά διάρκεια για μια ποικιλία εφαρμογών. Για να πάρω ένα συγκεκριμένο σημείο δεδομένων, οργάνωσα το SwingSet2 demo (τροποποιώντας το Σειρά υλοποίηση κλάσης απευθείας) που συνοδεύει το JDK 1.3.x για να παρακολουθείτε τα μήκη του Σειράδημιουργεί. Μετά από λίγα λεπτά παιχνιδιού με το demo, μια απόρριψη δεδομένων έδειξε ότι περίπου 180.000 Χορδές ήταν instantiated. Η ταξινόμησή τους σε κάδους μεγέθους επιβεβαίωσε τις προσδοκίες μου:

[0-10]: 96481 [10-20]: 27279 [20-30]: 31949 [30-40]: 7917 [40-50]: 7344 [50-60]: 3545 [60-70]: 1581 [70-80]: 1247 [80-90]: 874 ... 

Αυτό είναι σωστό, περισσότερο από το 50 τοις εκατό όλων Σειρά τα μήκη έπεσαν στον κάδο 0-10, το πολύ καυτό σημείο του Σειρά αναποτελεσματικότητα στην τάξη!

Στην πραγματικότητα, ΣειράΜπορεί να καταναλώνει ακόμη περισσότερη μνήμη από ό, τι υποδηλώνουν τα μήκη τους Σειράδημιουργήθηκε από StringBuffers (είτε ρητά είτε μέσω του «+» φορέα συνεννόησης) πιθανώς έχουν απανθρακώνω πίνακες με μήκη μεγαλύτερα από τα αναφερόμενα Σειρά μήκη επειδή StringBufferΣυνήθως ξεκινά με χωρητικότητα 16, και στη συνέχεια διπλασιάζεται προσαρτώ() λειτουργίες. Έτσι, για παράδειγμα, createString (1) + " καταλήγει με ένα απανθρακώνω συστοιχία μεγέθους 16, όχι 2.

Τι κάνουμε?

"Όλα αυτά είναι πολύ καλά, αλλά δεν έχουμε άλλη επιλογή από το να χρησιμοποιήσουμε Σειράs και άλλοι τύποι που παρέχονται από την Java, έτσι; "Σας ακούω να ρωτάτε. Ας μάθουμε.

Μαθήματα περιτυλίγματος