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

Βασικά στοιχεία Bytecode

Καλώς ήλθατε σε μια άλλη δόση του "Under The Hood". Αυτή η στήλη δίνει στους προγραμματιστές Java μια ματιά του τι συμβαίνει κάτω από τα τρέχοντα προγράμματα Java. Το άρθρο αυτού του μήνα ρίχνει μια πρώτη ματιά στο σύνολο εντολών bytecode της εικονικής μηχανής Java (JVM). Το άρθρο καλύπτει πρωτόγονους τύπους που χρησιμοποιούνται από bytecodes, bytecodes που μετατρέπονται μεταξύ τύπων και bytecodes που λειτουργούν στη στοίβα. Τα επόμενα άρθρα θα συζητήσουν άλλα μέλη της οικογένειας bytecode.

Η μορφή bytecode

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

Η ροή bytecode μιας μεθόδου είναι μια ακολουθία οδηγιών για την εικονική μηχανή Java. Κάθε οδηγία αποτελείται από ένα byte κώδικας πράξης ακολουθούμενο από μηδέν ή περισσότερο τελεστές. Ο κώδικας op υποδεικνύει τη δράση που πρέπει να αναληφθεί. Εάν απαιτούνται περισσότερες πληροφορίες πριν από την ανάληψη δράσης από το JVM, αυτές οι πληροφορίες κωδικοποιούνται σε έναν ή περισσότερους τελεστές που ακολουθούν αμέσως τον opcode.

Κάθε τύπος opcode έχει μνημονικό. Στο τυπικό στυλ γλώσσας συναρμολόγησης, οι ροές bytecodes Java μπορούν να αναπαρασταθούν από τα μνημονικά τους ακολουθούμενα από οποιεσδήποτε τιμές τελεστών. Για παράδειγμα, η ακόλουθη ροή bytecodes μπορεί να αποσυναρμολογηθεί σε μνημονικά:

// Ροή Bytecode: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // Αποσυναρμολόγηση: iconst_0 // 03 istore_0 // 3b iinc 0, 1 // 84 00 01 iload_0 // 1a iconst_2 // 05 imul // 68 istore_0 // 3b goto -7 // a7 ff f9 

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

Όλοι οι υπολογισμοί στο JVM επικεντρώνονται στη στοίβα. Επειδή η JVM δεν έχει καταχωρητές για την αποθήκευση αυθαίρετων τιμών, όλα πρέπει να προωθηθούν στη στοίβα για να μπορούν να χρησιμοποιηθούν σε υπολογισμό. Επομένως, οι οδηγίες Bytecode λειτουργούν κυρίως στη στοίβα. Για παράδειγμα, στην παραπάνω ακολουθία bytecode μια τοπική μεταβλητή πολλαπλασιάζεται με δύο πιέζοντας πρώτα την τοπική μεταβλητή στη στοίβα με το iload_0 οδηγίες, στη συνέχεια πιέζοντας δύο στη στοίβα με iconst_2. Αφού ωθήσουν και οι δύο ακέραιοι στη στοίβα, το Ιμουλ Η οδηγία αναδύει αποτελεσματικά τους δύο ακέραιους αριθμούς από τη στοίβα, τους πολλαπλασιάζει και ωθεί το αποτέλεσμα πίσω στη στοίβα. Το αποτέλεσμα αναδύεται από την κορυφή της στοίβας και αποθηκεύεται πίσω στην τοπική μεταβλητή από το istore_0 εντολή. Το JVM σχεδιάστηκε ως μηχάνημα που βασίζεται σε στοίβα και όχι ως μηχάνημα που βασίζεται σε μητρώο για να διευκολύνει την αποτελεσματική εφαρμογή σε αρχιτεκτονικές που δεν διαθέτουν μητρώο, όπως το Intel 486.

Πρωτόγονοι τύποι

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

ΤύποςΟρισμός
ψηφιόλεξηένας-byte υπέγραψε ακέραιος αριθμός συμπληρώματος δύο
μικρόςδύο byte υπέγραψε ακέραιος αριθμός συμπληρώματος δύο
int4-byte υπέγραψε ακέραιος αριθμός συμπληρώματος δύο
μακρύςΟ ακέραιος αριθμός συμπληρώθηκε με δύο byte
φλοτέρ4-byte IEEE 754 μονής ακρίβειας πλωτήρα
διπλό8-byte IEEE 754 διπλής ακρίβειας πλωτήρα
απανθρακώνωΧωρίς υπογραφή χαρακτήρας Unicode 2 byte

Οι πρωτόγονοι τύποι εμφανίζονται ως τελεστές σε ροές bytecode. Όλοι οι πρωτόγονοι τύποι που καταλαμβάνουν περισσότερα από 1 byte αποθηκεύονται σε σειρά μεγάλου endian στη ροή bytecode, πράγμα που σημαίνει ότι τα byte υψηλότερης τάξης προηγούνται των byte χαμηλότερης τάξης. Για παράδειγμα, για να ωθήσετε τη σταθερή τιμή 256 (hex 0100) στη στοίβα, θα χρησιμοποιήσετε το γουλιά opcode ακολουθούμενο από ένα σύντομο τελεστέο. Το σύντομο εμφανίζεται στη ροή bytecode, που φαίνεται παρακάτω, ως "01 00", επειδή το JVM είναι μεγάλο-endian. Εάν το JVM ήταν λίγο-endian, το κοντό θα εμφανίζεται ως "00 01".

 // Ρεύμα Bytecode: 17 01 00 // Διασυναρμολόγηση: sipush 256; // 17 01 00 

Οι κώδικες Java γενικά υποδεικνύουν τον τύπο των τελεστών τους. Αυτό επιτρέπει στους τελεστές να είναι μόνοι τους, χωρίς να χρειάζεται να αναγνωρίζουν τον τύπο τους στο JVM. Για παράδειγμα, αντί να έχει ένα opcode που ωθεί μια τοπική μεταβλητή στη στοίβα, το JVM έχει πολλά. Κωδικοί φορτωμένος, φορτίο, φορτίο, και φορτώσω ωθήστε τις τοπικές μεταβλητές τύπου int, long, float και double, αντίστοιχα, στη στοίβα.

Σπρώχνοντας σταθερές στη στοίβα

Πολλοί opcodes ωθούν σταθερές στη στοίβα. Οι κώδικες δείχνουν τη σταθερή τιμή που πρέπει να ωθήσετε με τρεις διαφορετικούς τρόπους. Η σταθερή τιμή είναι είτε έμμεση στον ίδιο τον κώδικα, ακολουθεί τον opcode στη ροή bytecode ως τελεστή, είτε λαμβάνεται από τη σταθερή ομάδα.

Μερικοί από τους κώδικες από μόνοι τους υποδεικνύουν έναν τύπο και μια σταθερή τιμή για ώθηση. Για παράδειγμα, το iconst_1 Το opcode λέει στο JVM να ωθήσει μια ακέραια τιμή. Τέτοιοι bytecodes ορίζονται για ορισμένους συχνά ωθούμενους αριθμούς διαφόρων τύπων. Αυτές οι οδηγίες καταλαμβάνουν μόνο 1 byte στη ροή bytecode. Αυξάνουν την αποτελεσματικότητα της εκτέλεσης bytecode και μειώνουν το μέγεθος των ροών bytecode. Οι opcodes που ωθούν ints και floats εμφανίζονται στον παρακάτω πίνακα:

Κώδικας πράξηςOperand (s)Περιγραφή
iconst_m1(κανένας)σπρώχνει το int -1 στη στοίβα
iconst_0(κανένας)ωθεί το int 0 στη στοίβα
iconst_1(κανένας)ωθεί το int 1 στη στοίβα
iconst_2(κανένας)ωθεί το int 2 στη στοίβα
iconst_3(κανένας)ωθεί το int 3 στη στοίβα
iconst_4(κανένας)ωθεί το int 4 στη στοίβα
iconst_5(κανένας)ωθεί το int 5 στη στοίβα
fconst_0(κανένας)ωθεί το float 0 στη στοίβα
fconst_1(κανένας)ωθεί το float 1 στη στοίβα
fconst_2(κανένας)ωθεί το float 2 στη στοίβα

Οι opcodes που εμφανίζονται στον προηγούμενο πίνακα push ints και floats, που είναι τιμές 32-bit. Κάθε υποδοχή στη στοίβα Java έχει πλάτος 32 bit. Επομένως, κάθε φορά που ένα int ή float ωθείται στη στοίβα, καταλαμβάνει μία υποδοχή.

Οι opcodes που εμφανίζονται στον επόμενο πίνακα σπρώχνουν longs και double. Οι μεγάλες και οι διπλές τιμές καταλαμβάνουν 64 bits. Κάθε φορά που ένα μακρύ ή διπλό ωθείται στη στοίβα, η τιμή του καταλαμβάνει δύο υποδοχές στη στοίβα. Οι κώδικες Opc που υποδεικνύουν μια συγκεκριμένη μεγάλη ή διπλή τιμή για προώθηση εμφανίζονται στον ακόλουθο πίνακα:

Κώδικας πράξηςOperand (s)Περιγραφή
lconst_0(κανένας)ωθεί το μακρύ 0 στη στοίβα
lconst_1(κανένας)ωθεί το μακρύ 1 στη στοίβα
dconst_0(κανένας)ωθεί το διπλό 0 στη στοίβα
dconst_1(κανένας)ωθεί το διπλό 1 στη στοίβα

Ένας άλλος opcode ωθεί μια σιωπηρή σταθερή τιμή στη στοίβα. ο aconst_null Το opcode, που φαίνεται στον παρακάτω πίνακα, ωθεί μια αναφορά μηδενικού αντικειμένου στη στοίβα. Η μορφή μιας αναφοράς αντικειμένου εξαρτάται από την εφαρμογή JVM. Μια αναφορά αντικειμένου θα αναφέρεται κάπως σε ένα αντικείμενο Java στον σωρό που συλλέγεται σκουπίδια. Μια αναφορά μηδενικού αντικειμένου δείχνει ότι μια μεταβλητή αναφοράς αντικειμένου δεν αναφέρεται προς το παρόν σε κανένα έγκυρο αντικείμενο. ο aconst_null Το opcode χρησιμοποιείται στη διαδικασία εκχώρησης null σε μια μεταβλητή αναφοράς αντικειμένου.

Κώδικας πράξηςOperand (ες)Περιγραφή
aconst_null(κανένας)ωθεί μια αναφορά μηδενικού αντικειμένου στη στοίβα

Δύο opcodes δηλώνουν τη σταθερά που πρέπει να πιέσετε με έναν τελεστή που ακολουθεί αμέσως τον opcode. Αυτοί οι κώδικες, εμφανίζονται στον ακόλουθο πίνακα, χρησιμοποιούνται για την προώθηση ακέραιων σταθερών που βρίσκονται εντός του έγκυρου εύρους για byte ή σύντομους τύπους. Το byte ή το σύντομο που ακολουθεί το opcode επεκτείνεται σε ένα int πριν προωθηθεί στη στοίβα, επειδή κάθε υποδοχή στη στοίβα Java έχει πλάτος 32 bit. Οι λειτουργίες σε bytes και σορτς που έχουν ωθηθεί στη στοίβα γίνονται στην πραγματικότητα στα ισοδύναμα τους.

Κώδικας πράξηςOperand (s)Περιγραφή
διπλόςbyte1επεκτείνει το byte1 (έναν τύπο byte) σε ένα int και το ωθεί στη στοίβα
γουλιάbyte1, byte2επεκτείνεται byte1, byte2 (ένας σύντομος τύπος) σε ένα int και το ωθεί στη στοίβα

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

Ο σταθερός δείκτης συγκέντρωσης είναι μια μη υπογεγραμμένη τιμή που ακολουθεί αμέσως τον opcode στη ροή bytecode. Κωδικοί lcd1 και lcd2 σπρώξτε ένα αντικείμενο 32-bit στη στοίβα, όπως ένα int ή float. Η διαφορά μεταξύ lcd1 και lcd2 είναι αυτό lcd1 μπορεί να αναφέρεται μόνο σε σταθερές τοποθεσίες συγκέντρωσης μία έως 255 επειδή ο δείκτης είναι μόλις 1 byte. (Η σταθερή θέση συγκέντρωσης μηδέν δεν χρησιμοποιείται.) lcd2 έχει ευρετήριο 2 byte, οπότε μπορεί να αναφέρεται σε οποιαδήποτε σταθερή τοποθεσία συγκέντρωσης. lcd2w έχει επίσης ευρετήριο 2 byte, και χρησιμοποιείται για αναφορά σε οποιαδήποτε σταθερή τοποθεσία συγκέντρωσης που περιέχει ένα μακρύ ή διπλό, το οποίο καταλαμβάνει 64 bits. Οι opcodes που ωθούν τις σταθερές από το σταθερό pool εμφανίζονται στον ακόλουθο πίνακα:

Κώδικας πράξηςOperand (ες)Περιγραφή
ldc1indexbyte1ωθεί την είσοδο 32-bit stable_pool που καθορίζεται από το indexbyte1 στη στοίβα
ldc2indexbyte1, indexbyte2ωθεί την είσοδο 32-bit stable_pool που καθορίζεται από το indexbyte1, το indexbyte2 στη στοίβα
ldc2windexbyte1, indexbyte2ωθεί την είσοδο 64-bit stable_pool που καθορίζεται από το indexbyte1, indexbyte2 στη στοίβα

Σπρώχνοντας τοπικές μεταβλητές στη στοίβα

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

Η στοίβα Java είναι μια τελευταία στοίβα πρώτης από 32 κουλοχέρηδες. Επειδή κάθε υποδοχή στη στοίβα καταλαμβάνει 32 bit, όλες οι τοπικές μεταβλητές καταλαμβάνουν τουλάχιστον 32 bit. Οι τοπικές μεταβλητές τύπου long και double, οι οποίες είναι ποσότητες 64-bit, καταλαμβάνουν δύο υποδοχές στη στοίβα. Οι τοπικές μεταβλητές τύπου byte ή short αποθηκεύονται ως τοπικές μεταβλητές τύπου int, αλλά με μια τιμή που ισχύει για τον μικρότερο τύπο. Για παράδειγμα, μια τοπική μεταβλητή που αντιπροσωπεύει έναν τύπο byte θα περιέχει πάντα μια τιμή που ισχύει για ένα byte (-128 <= τιμή <= 127).

Κάθε τοπική μεταβλητή μιας μεθόδου έχει ένα μοναδικό ευρετήριο. Η τοπική μεταβλητή ενότητα του πλαισίου στοίβας μιας μεθόδου μπορεί να θεωρηθεί ως ένας πίνακας υποδοχών 32-bit, καθεμία από τις οποίες μπορεί να αντιμετωπιστεί από το ευρετήριο του πίνακα. Οι τοπικές μεταβλητές τύπου long ή double, οι οποίες καταλαμβάνουν δύο κουλοχέρηδες, αναφέρονται στο κάτω μέρος των δύο ευρετηρίων υποδοχής. Για παράδειγμα, ένα διπλό που καταλαμβάνει κουλοχέρηδες δύο και τρία θα αναφέρεται από έναν δείκτη δύο.

Υπάρχουν αρκετοί opcodes που ωθούν int και μεταφέρουν τοπικές μεταβλητές στη στοίβα τελεστών. Ορισμένοι opcodes ορίζονται που αναφέρονται σιωπηρά σε μια τοπικά μεταβλητή θέση που χρησιμοποιείται συνήθως. Για παράδειγμα, iload_0 φορτώνει την τοπική μεταβλητή στη θέση μηδέν. Άλλες τοπικές μεταβλητές ωθούνται στη στοίβα από έναν opcode που παίρνει τον τοπικό δείκτη μεταβλητών από το πρώτο byte που ακολουθεί τον opcode. ο φορτωμένος Η οδηγία είναι ένα παράδειγμα αυτού του τύπου opcode. Το πρώτο byte που ακολουθεί φορτωμένος ερμηνεύεται ως ευρετήριο 8-bit χωρίς υπογραφή που αναφέρεται σε τοπική μεταβλητή.

Μη υπογεγραμμένα τοπικά μεταβλητά ευρετήρια 8-bit, όπως αυτό που ακολουθεί το φορτωμένος εντολή, περιορίστε τον αριθμό των τοπικών μεταβλητών σε μια μέθοδο σε 256. Μια ξεχωριστή εντολή, που ονομάζεται πλατύς, μπορεί να επεκτείνει έναν δείκτη 8-bit κατά άλλα 8 bit. Αυτό αυξάνει το τοπικό μεταβλητό όριο στα 64 kilobytes. ο πλατύς Το opcode ακολουθείται από έναν τελεστή 8-bit. ο πλατύς Το opcode και ο τελεστής του μπορούν να προηγούνται μιας εντολής, όπως φορτωμένος, παίρνει ένα 8-bit μη υπογεγραμμένο τοπικό μεταβλητό ευρετήριο. Το JVM συνδυάζει τον τελεστή 8-bit του πλατύς οδηγίες με τον τελεστή 8-bit του φορτωμένος εντολή για να αποδώσει ένα μη υπογεγραμμένο τοπικό μεταβλητό ευρετήριο 16 bit.

Οι opcodes που ωθούν int και μεταφέρουν τοπικές μεταβλητές στη στοίβα εμφανίζονται στον παρακάτω πίνακα:

Κώδικας πράξηςOperand (ες)Περιγραφή
φορτωμένοςvindexωθεί int από την τοπική μεταβλητή θέση vindex
iload_0(κανένας)ωθεί το int από την τοπική μεταβλητή θέση μηδέν
iload_1(κανένας)ωθεί int από την τοπική μεταβλητή θέση μία
iload_2(κανένας)ωθεί int από την τοπική μεταβλητή θέση δύο
iload_3(κανένας)ωθεί int από την τοπική μεταβλητή θέση τρία
φορτίοvindexωθεί το float από την τοπική μεταβλητή θέση vindex
fload_0(κανένας)ωθεί το float από την τοπική μεταβλητή θέση μηδέν
fload_1(κανένας)ωθεί το float από την τοπική μεταβλητή θέση 1
fload_2(κανένας)ωθεί το float από την τοπική μεταβλητή θέση δύο
fload_3(κανένας)ωθεί το float από την τοπική μεταβλητή θέση τρία

Ο επόμενος πίνακας δείχνει τις οδηγίες που ωθούν τις τοπικές μεταβλητές τύπου long και διπλασιάζονται στη στοίβα. Αυτές οι οδηγίες μεταφέρουν 64 bits από το τοπικό μεταβλητό τμήμα του πλαισίου στοίβας στο τμήμα operand.