| Contents | Prev | Next | Index | The JavaTM Virtual Machine Specification |
CHAPTER 7
The Java virtual machine is designed to support the Java programming language. Sun's JDK releases and Java 2 SDK contain both a compiler from source code written in the Java programming language to the instruction set of the Java virtual machine, and a runtime system that implements the Java virtual machine itself. Understanding how one compiler utilizes the Java virtual machine is useful to the prospective compiler writer, as well as to one trying to understand the Java virtual machine itself.
Although this chapter concentrates on compiling source code written in the Java programming language, the Java virtual machine does not assume that the instructions it executes were generated from such code. While there have been a number of efforts aimed at compiling other languages to the Java virtual machine, the current version of the Java virtual machine was not designed to support a wide range of languages. Some languages may be hosted fairly directly by the Java virtual machine. Other languages may be implemented only inefficiently.
Note that the term "compiler" is sometimes used when referring to a translator from the instruction set of a Java virtual machine to the instruction set of a specific CPU. One example of such a translator is a just-in-time (JIT) code generator, which generates platform-specific instructions only after Java virtual machine code has been loaded. This chapter does not address issues associated with code generation, only those associated with compiling source code written in the Java programming language to Java virtual machine instructions.
javac compiler in Sun's JDK release 1.0.2 generates for
the examples. The Java virtual machine code is written in the informal "virtual
machine assembly language" output by Sun's javap utility,
distributed with the JDK software and the Java 2 SDK. You can use
javap to generate additional examples of compiled methods.
The format of the examples should be familiar to anyone who has read assembly code. Each instruction takes the form
The <index> is the index of the opcode of the instruction in the array that contains the bytes of Java virtual machine code for this method. Alternatively, the <index> may be thought of as a byte offset from the beginning of the method. The <opcode> is the mnemonic for the instruction's opcode, and the zero or more <operandN> are the operands of the instruction. The optional <comment> is given in end-of-line comment syntax:<index> <opcode> [<operand1> [<operand2>...]] [<comment>]
Some of the material in the comments is emitted by8 bipush 100 // Pushintconstant100
javap; the rest is supplied by the authors. The <index>
prefacing each instruction may be used as the target of a control transfer
instruction. For instance, a goto 8 instruction transfers control to the
instruction at index 8. Note that the actual operands of Java virtual machine
control transfer instructions are offsets from the addresses of the opcodes of
those instructions; these operands are displayed by javap (and are
shown in this chapter) as more easily read offsets into their methods.
We preface an operand representing a runtime constant pool index with a hash sign and follow the instruction by a comment identifying the runtime constant pool item referenced, as in
or10 ldc #1 // Pushfloatconstant100.0
For the purposes of this chapter, we do not worry about specifying details such as operand sizes.9 invokevirtual #4 // MethodExample.addTwo(II)I
The spin method simply spins around an empty
for loop 100 times:
A compiler might compilevoid spin() {int i;for (i = 0; i < 100; i++) {; // Loop body is empty}}
spin to
The Java virtual machine is stack-oriented, with most operations taking one or more operands from the operand stack of the Java virtual machine's current frame or pushing results back onto the operand stack. A new frame is created each time a method is invoked, and with it is created a new operand stack and set of local variables for use by that method (see Section 3.6, "Frames"). At any one point of the computation, there are thus likely to be many frames and equally many operand stacks per thread of control, corresponding to many nested method invocations. Only the operand stack in the current frame is active.Methodvoidspin()0 iconst_0 // Pushintconstant01 istore_1 // Store into local variable 1 (i=0) 2 goto 8 // First time through don't increment 5 iinc 1 1 // Increment local variable 1 by 1 (i++) 8 iload_1 // Push local variable 1 (i) 9 bipush 100 // Pushintconstant10011 if_icmplt 5 // Compare and loop if less than (i<100) 14 return // Returnvoidwhen done
The instruction set of the Java virtual machine
distinguishes operand types by using distinct bytecodes for operations on its
various data types. The method spin operates only on values of type
int. The instructions in its compiled code chosen to operate on
typed data (iconst_0, istore_1, iinc, iload_1, if_icmplt) are all specialized
for type int.
The two constants in spin, 0 and
100, are pushed onto the operand stack using two different
instructions. The 0 is pushed using an iconst_0 instruction, one of
the family of iconst_<i> instructions. The 100 is pushed
using a bipush instruction, which fetches the value it pushes as an immediate
operand.
The Java virtual machine frequently takes advantage of the
likelihood of certain operands (int constants -1, 0, 1, 2, 3, 4 and
5 in the case of the iconst_<i> instructions) by making those operands
implicit in the opcode. Because the iconst_0 instruction knows it is going to
push an int 0, iconst_0 does not need to store an
operand to tell it what value to push, nor does it need to fetch or decode an
operand. Compiling the push of 0 as bipush 0 would have been
correct, but would have made the compiled code for spin one byte
longer. A simple virtual machine would have also spent additional time fetching
and decoding the explicit operand each time around the loop. Use of implicit
operands makes compiled code more compact and efficient.
The int i in spin is
stored as Java virtual machine local variable 1. Because most Java virtual
machine instructions operate on values popped from the operand stack rather than
directly on local variables, instructions that transfer values between local
variables and the operand stack are common in code compiled for the Java virtual
machine. These operations also have special support in the instruction set. In
spin, values are transferred to and from local variables using the
istore_1 and iload_1 instructions, each of which implicitly operates on local
variable 1. The istore_1 instruction pops an int from the operand
stack and stores it in local variable 1. The iload_1 instruction pushes the
value in local variable 1 onto the operand stack.
The use (and reuse) of local variables is the responsibility of the compiler writer. The specialized load and store instructions should encourage the compiler writer to reuse local variables as much as is feasible. The resulting code is faster, more compact, and uses less space in the frame.
Certain very frequent operations on local variables are
catered to specially by the Java virtual machine. The iinc instruction
increments the contents of a local variable by a one-byte signed value. The iinc
instruction in spin increments the first local variable (its first
operand) by 1 (its second operand). The iinc instruction is very handy when
implementing looping constructs.
The for loop of spin is
accomplished mainly by these instructions:
The bipush instruction pushes the value 100 onto the operand stack as an5 iinc 1 1 // Increment local 1 by 1 (i++) 8 iload_1 // Push local variable 1 (i) 9 bipush 100 // Pushintconstant10011 if_icmplt 5 // Compare and loop if less than (i<100)
int, then the if_icmplt instruction pops that value off
the operand stack and compares it against i. If the comparison succeeds (the
variable i is less than 100), control is transferred
to index 5 and the next iteration of the for loop begins.
Otherwise, control passes to the instruction following the if_icmplt.
If the spin example had used a data type other
than int for the loop counter, the compiled code would necessarily
change to reflect the different data type. For instance, if instead of an
int the spin example uses a double, as
shown,
the compiled code isvoid dspin() {double i;for (i = 0.0; i < 100.0; i++) {; // Loop body is empty}}
The instructions that operate on typed data are now specialized for typeMethodvoiddspin()0 dconst_0 // Pushdoubleconstant0.01 dstore_1 // Store into local variables 1 and 2 2 goto 9 // First time through don't increment 5 dload_1 // Push local variables 1 and 2 6 dconst_1 // Pushdoubleconstant1.07 dadd // Add; there is no dinc instruction 8 dstore_1 // Store result in local variables 1 and 2 9 dload_1 // Push local variables 1 and 2 10 ldc2_w #4 // Pushdoubleconstant100.013 dcmpg // There is no if_dcmplt instruction 14 iflt 5 // Compare and loop if less than (i<100.0) 17 return // Returnvoidwhen done
double. (The ldc2_w instruction will be
discussed later in this chapter.)
Recall that double values occupy two local
variables, although they are only accessed using the lesser index of the two
local variables. This is also the case for values of type long.
Again for example,
becomesdouble doubleLocals(double d1, double d2) {return d1 + d2;}
Note that local variables of the local variable pairs used to storeMethoddoubledoubleLocals(double,double)0 dload_1 // First argument in local variables 1 and 2 1 dload_3 // Second argument in local variables 3 and 4 2 dadd 3 dreturn
double values in
doubleLocals must never be manipulated individually.
The Java virtual machine's opcode size of 1 byte results in its compiled code being very compact. However, 1-byte opcodes also mean that the Java virtual machine instruction set must stay small. As a compromise, the Java virtual machine does not provide equal support for all data types: it is not completely orthogonal (see Table 3.2, "Type support in the Java virtual machine instruction set").
For example, the comparison of values of type
int in the for statement of example spin
can be implemented using a single if_icmplt instruction; however, there is no
single instruction in the Java virtual machine instruction set that performs a
conditional branch on values of type double. Thus,
dspin must implement its comparison of values of type
double using a dcmpg instruction followed by an iflt instruction.
The Java virtual machine provides the most direct support
for data of type int. This is partly in anticipation of efficient
implementations of the Java virtual machine's operand stacks and local variable
arrays. It is also motivated by the frequency of int data in
typical programs. Other integral types have less direct support. There are no
byte, char, or short versions of the
store, load, or add instructions, for instance. Here is the spin
example written using a short:
It must be compiled for the Java virtual machine, as follows, using instructions operating on another type, most likelyvoid sspin() {short i;for (i = 0; i < 100; i++) {; // Loop body is empty}}
int, converting between short and int
values as necessary to ensure that the results of operations on
short data stay within the appropriate range:
The lack of direct support forMethodvoidsspin()0 iconst_0 1 istore_1 2 goto 10 5 iload_1 // Theshortis treated as though anint6 iconst_1 7 iadd 8 i2s // Truncateinttoshort9 istore_1 10 iload_1 11 bipush 100 13 if_icmplt 5 16 return
byte,
char, and short types in the Java virtual machine is
not particularly painful, because values of those types are internally promoted
to int (byte and short are sign-extended
to int, char is zero-extended). Operations on
byte, char, and short data can thus be
done using int instructions. The only additional cost is that of
truncating the values of int operations to valid ranges.
The long and floating-point types have an
intermediate level of support in the Java virtual machine, lacking only the full
complement of conditional control transfer instructions.
align2grain
method aligns an int value to a given power of 2:
Operands for arithmetic operations are popped from the operand stack, and the results of operations are pushed back onto the operand stack. Results of arithmetic subcomputations can thus be made available as operands of their nesting computation. For instance, the calculation ofint align2grain(int i, int grain) {return ((i + grain-1) & ~(grain-1));}
~(grain-1) is handled by these
instructions:
First5 iload_2 // Pushgrain6 iconst_1 // Pushintconstant17 isub // Subtract; push result 8 iconst_m1 // Pushintconstant -19 ixor // Do XOR; push result
grain - 1 is calculated using
the contents of local variable 2 and an immediate int value
1. These operands are popped from the operand stack and their
difference pushed back onto the operand stack. The difference is thus
immediately available for use as one operand of the ixor instruction. (Recall
that ~x == -1^x.) Similarly, the result of the ixor
instruction becomes an operand for the subsequent iand instruction.
The code for the entire method follows:
Methodintalign2grain(int,int)0 iload_1 1 iload_2 2 iadd 3 iconst_1 4 isub 5 iload_2 6 iconst_1 7 isub 8 iconst_m1 9 ixor 10 iand 11 ireturn
int, long, float, and
double, as well as references to instances of class
String, are managed using the ldc, ldc_w, and ldc2_w instructions.
The ldc and ldc_w instructions are used to access values in
the runtime constant pool (including instances of class String) of
types other than double and long. The ldc_w
instruction is used in place of ldc only when there is a large number of runtime
constant pool items and a larger index is needed to access an item. The ldc2_w
instruction is used to access all values of types double and
long; there is no non-wide variant.
Integral constants of types byte,
char, or short, as well as small int
values, may be compiled using the bipush, sipush, or iconst_<i>
instructions, as seen earlier (Ħħ7.2).
Certain small floating-point constants may be compiled using the
fconst_<f> and dconst_<d> instructions.
In all of these cases, compilation is straightforward. For instance, the constants for
are set up as follows:void useManyNumeric() {int i = 100;int j = 1000000;long l1 = 1;long l2 = 0xffffffff;double d = 2.2;...do some calculations...}
MethodvoiduseManyNumeric()0 bipush 100 // Push a smallintwith bipush 2 istore_1 3 ldc #1 // Pushintconstant1000000; a largerint// value uses ldc 5 istore_2 6 lconst_1 // A tinylongvalue uses short, fast lconst_1 7 lstore_3 8 ldc2_w #6 // Pushlong0xffffffff(that is, anint-1); any //longconstant value can be pushed using ldc2_w 11 lstore 5 13 ldc2_w #8 // Pushdoubleconstant2.200000; uncommon //doublevalues are also pushed using ldc2_w 16 dstore 7 ...do those calculations...
for statements was
shown in an earlier section (Ħħ7.2).
Most of the Java programming language's other control constructs
(if-then-else, do, while,
break, and continue) are also compiled in the obvious
ways. The compilation of switch statements is handled in a separate
section (Section
7.10, "Compiling Switches"), as are the compilation of exceptions (Section
7.12, "Throwing and Handling Exceptions") and the compilation of
finally clauses (Section
7.13, "Compiling finally ").
As a further example, a while loop is compiled
in an obvious way, although the specific control transfer instructions made
available by the Java virtual machine vary by data type. As usual, there is more
support for data of type int, for example:
is compiled tovoid whileInt() {int i = 0;while (i < 100) {i++;}}
Note that the test of theMethodvoidwhileInt()0 iconst_0 1 istore_1 2 goto 8 5 iinc 1 1 8 iload_1 9 bipush 100 11 if_icmplt 5 14 return
while
statement (implemented using the if_icmplt instruction) is at the bottom of the
Java virtual machine code for the loop. (This was also the case in the
spin examples earlier.) The test being at the bottom of the loop
forces the use of a goto instruction to get to the test prior to the first
iteration of the loop. If that test fails, and the loop body is never entered,
this extra instruction is wasted. However, while loops are
typically used when their body is expected to be run, often for many iterations.
For subsequent iterations, putting the test at the bottom of the loop saves a
Java virtual machine instruction each time around the loop: if the test were at
the top of the loop, the loop body would need a trailing goto instruction to get
back to the top.
Control constructs involving other data types are compiled in similar ways, but must use the instructions available for those data types. This leads to somewhat less efficient code because more Java virtual machine instructions are needed, for example:
is compiled tovoid whileDouble() {double i = 0.0;while (i < 100.1) {i++;}}
Each floating-point type has two comparison instructions: fcmpl and fcmpg for typeMethodvoidwhileDouble()0 dconst_0 1 dstore_1 2 goto 9 5 dload_1 6 dconst_1 7 dadd 8 dstore_1 9 dload_1 10 ldc2_w #4 // Pushdoubleconstant100.113 dcmpg // To do the compare and branch we have to use... 14 iflt 5 // ...two instructions 17 return
float, and dcmpl and dcmpg
for type double. The variants differ only in their treatment of
NaN. NaN is unordered, so all floating-point comparisons fail if either of their
operands is NaN. The compiler chooses the variant of the comparison instruction
for the appropriate type that produces the same result whether the comparison
fails on non-NaN values or encounters a NaN.
compiles toint lessThan100(double d) {if (d < 100.0) {return 1;} else {return -1;}}
IfMethodintlessThan100(double)0 dload_1 1 ldc2_w #4 // Pushdoubleconstant100.04 dcmpg // Push 1 ifdis NaN ord\>100.0; // push 0 ifd==100.05 ifge 10 // Branch on 0 or 1 8 iconst_1 9 ireturn 10 iconst_m1 11 ireturn
d is not NaN and is less than
100.0, the dcmpg instruction pushes an int -1 onto the
operand stack, and the ifge instruction does not branch. Whether d
is greater than 100.0 or is NaN, the dcmpg instruction pushes an
int 1 onto the operand stack, and the ifge branches. If
d is equal to 100.0, the dcmpg instruction pushes an
int 0 onto the operand stack, and the ifge branches.
The dcmpl instruction achieves the same effect if the comparison is reversed:
becomesint greaterThan100(double d) {if (d > 100.0) {return 1;} else {return -1;}}
Once again, whether the comparison fails on a non-NaN value or because it is passed a NaN, the dcmpl instruction pushes anMethodintgreaterThan100(double)0 dload_1 1 ldc2_w #4 // Pushdoubleconstant100.04 dcmpl // Push -1 ifdis Nan ord<100.0; // push 0 ifd==100.05 ifle 10 // Branch on 0 or -1 8 iconst_1 9 ireturn 10 iconst_m1 11 ireturn
int
value onto the operand stack that causes the ifle to branch. If both of the dcmp
instructions did not exist, one of the example methods would have had to do more
work to detect NaN.
compiles toint addTwo(int i, int j) {return i + j;}
By convention, an instance method is passed aMethodintaddTwo(int,int)0 iload_1 // Push value of local variable 1 (i) 1 iload_2 // Push value of local variable 2 (j) 2 iadd // Add; leaveintresult on operand stack 3 ireturn // Returnintresult
reference to its instance in local variable 0. In the Java
programming language the instance is accessible via the this
keyword.
Class (static) methods do not have an instance,
so for them this use of local variable zero is unnecessary. A class method
starts using local variables at index zero. If the addTwo method
were a class method, its arguments would be passed in a similar way to the first
version:
compiles tostatic int addTwoStatic(int i, int j) {return i + j;}
The only difference is that the method arguments appear starting in local variable 0 rather than 1.MethodintaddTwoStatic(int,int)0 iload_0 1 iload_1 2 iadd 3 ireturn
addTwo method, defined earlier as an instance method,
we might write
This compiles toint add12and13() {return addTwo(12, 13);}
The invocation is set up by first pushing aMethodintadd12and13()0 aload_0 // Push local variable 0 (this) 1 bipush 12 // Pushintconstant123 bipush 13 // Pushintconstant135 invokevirtual #4 // MethodExample.addtwo(II)I8 ireturn // Returninton top of operand stack; it is // theintresult ofaddTwo()
reference to the current instance, this,
onto the operand stack. The method invocation's arguments, int
values 12 and 13, are then pushed. When the frame for
the addTwo method is created, the arguments passed to the method
become the initial values of the new frame's local variables. That is, the
reference for this and the two arguments, pushed onto
the operand stack by the invoker, will become the initial values of local
variables 0, 1, and 2 of the invoked method.
Finally, addTwo is invoked. When it returns,
its int return value is pushed onto the operand stack of the frame
of the invoker, the add12and13 method. The return value is thus put
in place to be immediately returned to the invoker of add12and13.
The return from add12and13 is handled by the
ireturn instruction of add12and13. The ireturn instruction takes
the int value returned by addTwo, on the operand stack
of the current frame, and pushes it onto the operand stack of the frame of the
invoker. It then returns control to the invoker, making the invoker's frame
current. The Java virtual machine provides distinct return instructions for many
of its numeric and reference data types, as well as a return
instruction for methods with no return value. The same set of return
instructions is used for all varieties of method invocations.
The operand of the invokevirtual instruction (in the example, the runtime constant pool index #4) is not the offset of the method in the class instance. The compiler does not know the internal layout of a class instance. Instead, it generates symbolic references to the methods of an instance, which are stored in the runtime constant pool. Those runtime constant pool items are resolved at run time to determine the actual method location. The same is true for all other Java virtual machine instructions that access class instances.
Invoking addTwoStatic, a class
(static) variant of addTwo, is similar, as shown:
although a different Java virtual machine method invocation instruction is used:int add12and13() {return addTwoStatic(12, 13);}
Compiling an invocation of a class (Methodint add12and13()0 bipush 12 2 bipush 13 4 invokestatic #3 // MethodExample.addTwoStatic(II)I7 ireturn
static)
method is very much like compiling an invocation of an instance method, except
this is not passed by the invoker. The method arguments will thus
be received beginning with local variable 0 (see Section
7.6, "Receiving Arguments"). The invokestatic instruction is always used to
invoke class methods.
The invokespecial instruction must be used to invoke
instance initialization methods (see Section
7.8, "Working with Class Instances"). It is also used when invoking methods
in the superclass (super) and when invoking private
methods. For instance, given classes Near and Far
declared as
the methodclass Near {int it;public int getItNear() {return getIt();}private int getIt() {return it;}}class Far extends Near {int getItFar() {return super.getItNear();}}
Near.getItNear (which invokes
a private method) becomes
The methodMethodintgetItNear()0 aload_0 1 invokespecial #5 // MethodNear.getIt()I4 ireturn
Far.getItFar (which invokes a
superclass method) becomes
Note that methods called using the invokespecial instruction always passMethodintgetItFar()0 aload_0 1 invokespecial #4 // MethodNear.getItNear()I4 ireturn
this to the invoked method as its first argument. As
usual, it is received in local variable 0.
<init>. This specially named method is
known as the instance initialization method (Ħħ3.9).
Multiple instance initialization methods, corresponding to multiple
constructors, may exist for a given class. Once the class instance has been
created and its instance variables, including those of the class and all of its
superclasses, have been initialized to their default values, an instance
initialization method of the new class instance is invoked. For example:
compiles toObject create() {return new Object();}
Class instances are passed and returned (asMethodjava.lang.Objectcreate()0 new #1 // Classjava.lang.Object3 dup 4 invokespecial #4 // Methodjava.lang.Object.<init>()V7 areturn
reference types) very much like numeric values, although type
reference has its own complement of instructions, for example:
becomesint i; // An instance variableMyObj example() {MyObj o = new MyObj();return silly(o);}MyObj silly(MyObj o) {if (o != null) {return o;} else {return o;}}
The fields of a class instance (instance variables) are accessed using the getfield and putfield instructions. IfMethodMyObjexample()0 new #2 // ClassMyObj3 dup 4 invokespecial #5 // MethodMyObj.<init>()V7 astore_1 8 aload_0 9 aload_1 10 invokevirtual #4 // MethodExample.silly(LMyObj;)LMyObj;13 areturn MethodMyObjsilly(MyObj)0 aload_1 1 ifnull 6 4 aload_1 5 areturn 6 aload_1 7 areturn
i is an instance variable of type int, the methods
setIt and getIt, defined as
becomevoid setIt(int value) {i = value;}int getIt() {return i;}
As with the operands of method invocation instructions, the operands of the putfield and getfield instructions (the runtime constant pool index #4) are not the offsets of the fields in the class instance. The compiler generates symbolic references to the fields of an instance, which are stored in the runtime constant pool. Those runtime constant pool items are resolved at run time to determine the location of the field within the referenced object.MethodvoidsetIt(int)0 aload_0 1 iload_1 2 putfield #4 // FieldExample.i I5 return MethodintgetIt()0 aload_0 1 getfield #4 // FieldExample.i I4 ireturn
might be compiled tovoid createBuffer() {int buffer[];int bufsz = 100;int value = 12;buffer = new int[bufsz];buffer[10] = value;value = buffer[11];}
The anewarray instruction is used to create a one-dimensional array of object references, for example:MethodvoidcreateBuffer()0 bipush 100 // Pushintconstant 100 (bufsz) 2 istore_2 // Storebufszin local variable 2 3 bipush 12 // Pushintconstant 12 (value) 5 istore_3 // Storevaluein local variable 3 6 iload_2 // Pushbufsz... 7 newarrayint// ...and create new array ofintof that length 9 astore_1 // Store new array inbuffer10 aload_1 // Pushbuffer11 bipush 10 // Pushintconstant1013 iload_3 // Pushvalue14 iastore // Store value atbuffer[10]15 aload_1 // Pushbuffer16 bipush 11 // Pushintconstant1118 iaload // Push value atbuffer[11]... 19 istore_3 // ...and store it invalue20 return
becomesvoid createThreadArray() {Thread threads[];int count = 10;threads = new Thread[count];threads[0] = new Thread();}
The anewarray instruction can also be used to create the first dimension of a multidimensional array. Alternatively, the multianewarray instruction can be used to create several dimensions at once. For example, the three-dimensional array:Methodvoid createThreadArray()0 bipush 10 // Pushintconstant102 istore_2 // Initializecountto that 3 iload_2 // Pushcount, used by anewarray 4 anewarray class #1 // Create new array of classThread7 astore_1 // Store new array inthreads8 aload_1 // Push value ofthreads9 iconst_0 // Pushintconstant010 new #1 // Create instance of classThread13 dup // Make duplicate reference... 14 invokespecial #5 // ...to pass to instance initialization method // Methodjava.lang.Thread.<init>()V17 aastore // Store newThreadin array at018 return
is created byint[][][] create3DArray() {int grid[][][];grid = new int[10][5][];return grid;}
The first operand of the multianewarray instruction is the runtime constant pool index to the array class type to be created. The second is the number of dimensions of that array type to actually create. The multianewarray instruction can be used to create all the dimensions of the type, as the code forMethodintcreate3DArray()[][][]0 bipush 10 // Pushint10(dimension one) 2 iconst_5 // Pushint5(dimension two) 3 multianewarray #1 dim #2 // Class[[[I, a three // dimensionalintarray; // only create first two // dimensions 7 astore_1 // Store new array... 8 aload_1 // ...then prepare to return it 9 areturn
create3DArray shows. Note that the multidimensional
array is just an object and so is loaded and returned by an aload_1 and areturn
instruction, respectively. For information about array class names, see Section
4.4.1.
All arrays have associated lengths, which are accessed via the arraylength instruction.
switch statements
uses the tableswitch and lookupswitch instructions. The tableswitch instruction
is used when the cases of the switch can be efficiently represented
as indices into a table of target offsets. The default target of
the switch is used if the value of the expression of the
switch falls outside the range of valid indices. For instance,
compiles toint chooseNear(int i) {switch (i) {case 0: return 0;case 1: return 1;case 2: return 2;default: return -1;}}
The Java virtual machine's tableswitch and lookupswitch instructions operate only onMethodintchooseNear(int)0 iload_1 // Push local variable 1 (argumenti) 1 tableswitch 0 to 2: // Valid indices are 0 through 2 0: 28 // Ifiis0, continue at 28 1: 30 // Ifiis1, continue at 30 2: 32 // Ifiis2, continue at 32 default:34 // Otherwise, continue at 34 28 iconst_0 //iwas0; pushintconstant0... 29 ireturn // ...and return it 30 iconst_1 //iwas1; pushintconstant1... 31 ireturn // ...and return it 32 iconst_2 //iwas2; pushintconstant2... 33 ireturn // ...and return it 34 iconst_m1 // otherwise pushintconstant -1... 35 ireturn // ...and return it
int data. Because
operations on byte, char, or short values
are internally promoted to int, a switch whose
expression evaluates to one of those types is compiled as though it evaluated to
type int. If the chooseNear method had been written
using type short, the same Java virtual machine instructions would
have been generated as when using type int. Other numeric types
must be narrowed to type int for use in a switch.
Where the cases of the switch are sparse, the
table representation of the tableswitch instruction becomes inefficient in terms
of space. The lookupswitch instruction may be used instead. The lookupswitch
instruction pairs int keys (the values of the case
labels) with target offsets in a table. When a lookupswitch instruction is
executed, the value of the expression of the switch is compared
against the keys in the table. If one of the keys matches the value of the
expression, execution continues at the associated target offset. If no key
matches, execution continues at the default target. For instance,
the compiled code for
looks just like the code forint chooseFar(int i) {switch (i) {case -100: return -1;case 0: return 0;case 100: return 1;default: return -1;}}
chooseNear,
except for the use of the lookupswitch instruction:
The Java virtual machine specifies that the table of the lookupswitch instruction must be sorted by key so that implementations may use searches more efficient than a linear scan. Even so, the lookupswitch instruction must search its keys for a match rather than simply perform a bounds check and index into a table like tableswitch. Thus, a tableswitch instruction is probably more efficient than a lookupswitch where space considerations permit a choice.MethodintchooseFar(int)0 iload_1 1 lookupswitch 3: -100: 36 0: 38 100: 40 default:42 36 iconst_m1 37 ireturn 38 iconst_0 39 ireturn 40 iconst_1 41 ireturn 42 iconst_m1 43 ireturn
is compiled topublic long nextIndex() {return index++;}private long index = 0;
Note that the Java virtual machine never allows its operand stack manipulation instructions to modify or break up individual values on the operand stack.Methodlong nextIndex()0 aload_0 // Pushthis1 dup // Make a copy of it 2 getfield #4 // One of the copies ofthisis consumed // pushinglongfieldindex, // above the originalthis5 dup2_x1 // Thelongon top of the operand stack is // inserted into the operand stack below the // originalthis6 lconst_1 // Pushlongconstant 1 7 ladd // The index value is incremented... 8 putfield #4 // ...and the result stored back in the field 11 lreturn // The original value ofindexis left on // top of the operand stack, ready to be returned
throw keyword. Its compilation is simple:
becomesvoid cantBeZero(int i) throws TestExc {if (i == 0) {throw new TestExc();}}
Compilation ofMethodvoidcantBeZero(int)0 iload_1 // Push argument 1 (i) 1 ifne 12 // Ifi==0, allocate instance and throw 4 new #1 // Create instance ofTestExc7 dup // One reference goes to the constructor 8 invokespecial #7 // MethodTestExc.<init>()V11 athrow // Second reference is thrown 12 return // Never get here if we threwTestExc
try-catch constructs is straightforward. For example,
is compiled asvoid catchOne() {try {tryItOut();} catch (TestExc e) {handleExc(e);}}
Looking more closely, theMethodvoidcatchOne()0 aload_0 // Beginning oftryblock 1 invokevirtual #6 // MethodExample.tryItOut()V4 return // End oftryblock; normal return 5 astore_1 // Store thrown value in local variable 1 6 aload_0 // Pushthis7 aload_1 // Push thrown value 8 invokevirtual #5 // Invoke handler method: //Example.handleExc(LTestExc;)V11 return // Return after handlingTestExcException table: From To Target Type 0 4 5 ClassTestExc
try block is
compiled just as it would be if the try were not present:
If no exception is thrown during the execution of theMethodvoidcatchOne()0 aload_0 // Beginning oftryblock 1 invokevirtual #4 // MethodExample.tryItOut()V4 return // End oftryblock; normal return
try block, it behaves as though the try were not
there: tryItOut is invoked and catchOne returns.
Following the try block is the Java virtual
machine code that implements the single catch clause:
The invocation of5 astore_1 // Store thrown value in local variable 1 6 aload_0 // Pushthis7 aload_1 // Push thrown value 8 invokevirtual #5 // Invoke handler method: //Example.handleExc(LTestExc;)V11 return // Return after handlingTestExcException table: From To Target Type 0 4 5 ClassTestExc
handleExc, the contents
of the catch clause, is also compiled like a normal method
invocation. However, the presence of a catch clause causes the
compiler to generate an exception table entry. The exception table for the
catchOne method has one entry corresponding to the one argument (an
instance of class TestExc) that the catch clause of
catchOne can handle. If some value that is an instance of
TestExc is thrown during execution of the instructions between
indices 0 and 4 in catchOne, control is transferred to the Java
virtual machine code at index 5, which implements the block of the
catch clause. If the value that is thrown is not an instance of
TestExc, the catch clause of catchOne
cannot handle it. Instead, the value is rethrown to the invoker of
catchOne.
A try may have multiple catch
clauses:
Multiplevoid catchTwo() {try {tryItOut();} catch (TestExc1 e) {handleExc(e);} catch (TestExc2 e) {handleExc(e);}}
catch clauses of a given
try statement are compiled by simply appending the Java virtual
machine code for each catch clause one after the other and adding
entries to the exception table, as shown:
If during the execution of theMethodvoid catchTwo()0 aload_0 // Begintryblock 1 invokevirtual #5 // MethodExample.tryItOut()V4 return // End oftryblock; normal return 5 astore_1 // Beginning of handler forTestExc1; // Store thrown value in local variable 1 6 aload_0 // Pushthis7 aload_1 // Push thrown value 8 invokevirtual #7 // Invoke handler method: //Example.handleExc(LTestExc1;)V11 return // Return after handlingTestExc112 astore_1 // Beginning of handler forTestExc2; // Store thrown value in local variable 1 13 aload_0 // Pushthis14 aload_1 // Push thrown value 15 invokevirtual #7 // Invoke handler method: //Example.handleExc(LTestExc2;)V18 return // Return after handlingTestExc2Exception table: From To Target Type 0 4 5 ClassTestExc10 4 12 ClassTestExc2
try clause
(between indices 0 and 4) a value is thrown that matches the parameter of one or
more of the catch clauses (the value is an instance of one or more
of the parameters), the first (innermost) such catch clause is
selected. Control is transferred to the Java virtual machine code for the block
of that catch clause. If the value thrown does not match the
parameter of any of the catch clauses of catchTwo, the
Java virtual machine rethrows the value without invoking code in any
catch clause of catchTwo.
Nested try-catch statements are
compiled very much like a try statement with multiple
catch clauses:
becomesvoid nestedCatch() {try {try {tryItOut();} catch (TestExc1 e) {handleExc1(e);}} catch (TestExc2 e) {handleExc2(e);}}
The nesting ofMethodvoid nestedCatch()0 aload_0 // Begintryblock 1 invokevirtual #8 // MethodExample.tryItOut()V4 return // End oftryblock; normal return 5 astore_1 // Beginning of handler forTestExc1; // Store thrown value in local variable 1 6 aload_0 // Pushthis7 aload_1 // Push thrown value 8 invokevirtual #7 // Invoke handler method: //Example.handleExc1(LTestExc1;)V11 return // Return after handlingTestExc112 astore_1 // Beginning of handler forTestExc2; // Store thrown value in local variable 1 13 aload_0 // Pushthis14 aload_1 // Push thrown value 15 invokevirtual #6 // Invoke handler method: //Example.handleExc2(LTestExc2;)V18 return // Return after handlingTestExc2Exception table: From To Target Type 0 4 5 ClassTestExc10 12 12 ClassTestExc2
catch clauses is
represented only in the exception table. When an exception is thrown, the first
(innermost) catch clause that contains the site of the exception and with a
matching parameter is selected to handle it. For instance, if the invocation of
tryItOut (at index 1) threw an instance of TestExc1,
it would be handled by the catch clause that invokes
handleExc1. This is so even though the exception occurs within the
bounds of the outer catch clause (catching TestExc2)
and even though that outer catch clause might otherwise have been
able to handle the thrown value.
As a subtle point, note that the range of a
catch clause is inclusive on the "from" end and exclusive on the
"to" end (Ħħ4.7.3).
Thus, the exception table entry for the catch clause catching
TestExc1 does not cover the return instruction at offset 4.
However, the exception table entry for the catch clause catching
TestExc2 does cover the return instruction at offset 11. Return
instructions within nested catch clauses are included in the range
of instructions covered by nesting catch clauses.
finallytry-finally statement is similar to that of
try-catch. Prior to transferring control outside the
try statement, whether that transfer is normal or abrupt, because
an exception has been thrown, the finally clause must first be
executed. For this simple example
the compiled code isvoid tryFinally() {try {tryItOut();} finally {wrapItUp();}}
There are four ways for control to pass outside of theMethodvoid tryFinally()0 aload_0 // Beginning oftryblock 1 invokevirtual #6 // MethodExample.tryItOut()V4 jsr 14 // Callfinallyblock 7 return // End oftryblock 8 astore_1 // Beginning of handler for any throw 9 jsr 14 // Callfinallyblock 12 aload_1 // Push thrown value 13 athrow // ...and rethrow the value to the invoker 14 astore_2 // Beginning offinallyblock 15 aload_0 // Pushthis16 invokevirtual #5 // MethodExample.wrapItUp()V19 ret 2 // Return fromfinallyblock Exception table: From To Target Type 0 4 8 any
try statement: by falling through the bottom of that block, by
returning, by executing a break or continue statement,
or by raising an exception. If tryItOut returns without raising an
exception, control is transferred to the finally block using a jsr
instruction. The jsr 14 instruction at index 4 makes a "subroutine call" to the
code for the finally block at index 14 (the finally
block is compiled as an embedded subroutine). When the finally
block completes, the ret 2 instruction returns control to the instruction
following the jsr instruction at index 4.
In more detail, the subroutine call works as follows: The
jsr instruction pushes the address of the following instruction (return at index
7) onto the operand stack before jumping. The astore_2 instruction that is the
jump target stores the address on the operand stack into local variable 2. The
code for the finally block (in this case the aload_0 and
invokevirtual instructions) is run. Assuming execution of that code completes
normally, the ret instruction retrieves the address from local variable 2 and
resumes execution at that address. The return instruction is executed, and
tryFinally returns normally.
A try statement with a finally
clause is compiled to have a special exception handler, one that can handle any
exception thrown within the try statement. If tryItOut
throws an exception, the exception table for tryFinally is searched
for an appropriate exception handler. The special handler is found, causing
execution to continue at index 8. The astore_1 instruction at index 8 stores the
thrown value into local variable 1. The following jsr instruction does a
subroutine call to the code for the finally block. Assuming that
code returns normally, the aload_1 instruction at index 12 pushes the thrown
value back onto the operand stack, and the following athrow instruction rethrows
the value.
Compiling a try statement with both a
catch clause and a finally clause is more complex:
becomesvoid tryCatchFinally() {try {tryItOut();} catch (TestExc e) {handleExc(e);} finally {wrapItUp();}}
If theMethodvoidtryCatchFinally()0 aload_0 // Beginning oftryblock 1 invokevirtual #4 // MethodExample.tryItOut()V4 goto 16 // Jump tofinallyblock 7 astore_3 // Beginning of handler forTestExc; // Store thrown value in local variable 3 8 aload_0 // Pushthis9 aload_3 // Push thrown value 10 invokevirtual #6 // Invoke handler method: //Example.handleExc(LTestExc;)V13 goto 16 // Huh???1 16 jsr 26 // Callfinallyblock 19 return // Return after handlingTestExc20 astore_1 // Beginning of handler for exceptions // other thanTestExc, or exceptions // thrown while handlingTestExc21 jsr 26 // Callfinallyblock 24 aload_1 // Push thrown value... 25 athrow // ...and rethrow the value to the invoker 26 astore_2 // Beginning offinallyblock 27 aload_0 // Pushthis28 invokevirtual #5 // MethodExample.wrapItUp()V31 ret 2 // Return fromfinallyblock Exception table: From To Target Type 0 4 7 ClassTestExc0 16 20 any
try statement completes
normally, the goto instruction at index 4 jumps to the subroutine call for the
finally block at index 16. The finally block at index
26 is executed, control returns to the return instruction at index 19, and
tryCatchFinally returns normally.
If tryItOut throws an instance of
TestExc, the first (innermost) applicable exception handler in the
exception table is chosen to handle the exception. The code for that exception
handler, beginning at index 7, passes the thrown value to handleExc
and on its return makes the same subroutine call to the finally
block at index 26 as in the normal case. If an exception is not thrown by
handleExc, tryCatchFinally returns normally.
If tryItOut throws a value that is not an
instance of TestExc or if handleExc itself throws an
exception, the condition is handled by the second entry in the exception table,
which handles any value thrown between indices 0 and 16. That exception handler
transfers control to index 20, where the thrown value is first stored in local
variable 1. The code for the finally block at index 26 is called as
a subroutine. If it returns, the thrown value is retrieved from local variable 1
and rethrown using the athrow instruction. If a new value is thrown during
execution of the finally clause, the finally clause
aborts, and tryCatchFinally returns abruptly, throwing the new
value to its invoker.
synchronized method.
A synchronized method is not normally
implemented using monitorenter and monitorexit. Rather, it is simply
distinguished in the runtime constant pool by the ACC_SYNCHRONIZED
flag, which is checked by the method invocation instructions. When invoking a
method for which ACC_SYNCHRONIZED is set, the current thread
acquires a monitor, invokes the method itself, and releases the monitor whether
the method invocation completes normally or abruptly. During the time the
executing thread owns the monitor, no other thread may acquire it. If an
exception is thrown during invocation of the synchronized method
and the synchronized method does not handle the exception, the
monitor for the method is automatically released before the exception is
rethrown out of the synchronized method.
The monitorenter and monitorexit instructions exist to
support synchronized statements. For example:
is compiled tovoid onlyMe(Foo f) {synchronized(f) {doSomething();}}
Methodvoid onlyMe(Foo)0 aload_1 // Pushf1 astore_2 // Store it in local variable 2 2 aload_2 // Push local variable 2 (f) 3 monitorenter // Enter the monitor associated withf4 aload_0 // Holding the monitor, passthisand... 5 invokevirtual #5 // ...callExample.doSomething()V8 aload_2 // Push local variable 2 (f) 9 monitorexit // Exit the monitor associated withf10 return // Return normally 11 aload_2 // In case of any throw, end up here 12 monitorexit // Be sure to exit monitor... 13 athrow // ...then rethrow the value to the invoker Exception table: From To Target Type 4 8 11 any
A full treatment of the compilation of nested classes and
interfaces is outside the scope of this chapter. However, interested readers can
refer to the Inner Classes Specification at http://java.sun.com/products/jdk/1.1/docs/guide/innerclasses/spec/innerclasses.doc.html.
javac compiler of Sun's JDK release 1.0.2.
Contents | Prev | Next | Index
The JavaTM Virtual
Machine Specification
Copyright
© 1999 Sun Microsystems, Inc. All rights reserved
Please send any
comments or corrections to jvm@java.sun.com