Previous Page TOC Index Next Page Home


Day 21

Under the Hood

by Charles L. Perkins

On today, your final day, the inner workings of the Java system will be revealed.

You'll find out all about Java's vision, Java's virtual machine, those bytecodes you've heard so much about, that mysterious garbage collector, and why you might worry about security but don't have to.

Let's begin, however, with the big picture.

The Big Picture

The Java team is very ambitious. Their ultimate goal is nothing less than to revolutionize the way software is written and distributed. They've started with the Internet, where they believe much of the interesting software of the future will live.

To achieve such an ambitious goal, a large fraction of the Internet programming community itself must be marshalled behind a similar goal and given the tools to help achieve it. The Java language, with its four S's (small, simple, safe, secure), and its flexible, Net-oriented environment, hopes to become the focal point for the rallying of this new legion of programmers.

To this end, Sun Microsystems has done something rather gutsy. What was originally a secret, tens-of-millions-of-dollars research and development project, and 100 percent proprietary, has become a free, open, and relatively unencumbered technology standard upon which anyone can build. They are literally giving it away and reserving only the rights they need to maintain and grow the standard.


Note: Actually, as Sun's lawyers have had more and more time to think, the original intentions of the Java team get further obscured by legal details. It is still relatively unencumbered, but its earlier releases were completely unencumbered. Let's hope that this is not a pattern that will continue.

Any truly open standard must be supported by at least one excellent, freely available "demonstration" implementation. Sun has already shipped an alpha, and now a beta, of one to the Internet and plans on a final release soon. In parallel, several universities, companies, and individuals have already expressed their intention to duplicate the Java environment, based on the open API that Sun has created.

Several other languages are even contemplating compiling down to Java bytecodes, to help support them in becoming a more robust and widespread standard for moving executable content around on the Net.

Why It's a Powerful Vision

One of the reasons this brilliant move on Sun's part has a real chance of success is the pent-up frustration of literally a whole generation of programmers who desperately want to share their code with one another. Right now, the computer science world is balkanized into factions at universities and companies all over the world, with hundreds of languages, dozens of them widely used, dividing and separating us all. It's the worst sort of Tower of Babel. Java hopes to build some bridges and help tear down that tower. Because it is so simple, because it's so useful for programming over the Internet, and because the Internet is so "hot" right now—this confluence of forces should help propel Java onto centerstage.

It deserves to be there. It is the natural outgrowth of ideas that, since the early 1970s inside the Smalltalk group at Xerox PARC, have lain relatively dormant in the mainstream. Smalltalk, in fact, invented the first object-oriented bytecode interpreter and pioneered many of the deep ideas that Java builds on today. Those efforts were not embraced over the intervening decades as a solution to the general problems of software, however. Today, with those problems becoming so much more obvious, and with the Net crying out for a new kind of programming, the soil is fertile to grow something stronger from those old roots, something that just might spread like wildfire. (Is it a coincidence that Java's previous internal names were Green and OAK?)

This new vision of software is one in which the Net becomes an ocean of objects, classes, and the open APIs between them. Traditional applications have vanished, replaced by skeletal frameworks like the Eiffel tower, into which can be fitted any parts from this ocean, on demand, to suit any purpose. User interfaces will be mixed and matched, built in pieces and constructed to taste, whenever the need arises, by their own users. Menus of choices will be filled by dynamic lists of all the choices available for that function, at that exact moment, across the entire ocean (of the Net).

In such a world, software distribution is no longer an issue. Software will be everywhere and will be paid for via a plethora of new micro-accounting models, which charge tiny fractions of cents for the parts as they are assembled and used. Frameworks will come into existence to support entertainment, business, and the social (cyber-)spaces of the near future.

This is a dream that many of us have waited all our lives to be a part of. There are tremendous challenges to making it all come true, but the powerful winds of change we all feel must stir us into action, because, at last, there is a base on which to build that dream—Java.

The Java Virtual Machine

To make visions like this possible, Java must be ubiquitous. It must be able to run on any computer and any operating system—now, and in the future. In order to achieve this level of portability, Java must be very precise not only about the language itself, but about the environment in which the language lives. You can see, from earlier in the book and Appendix B, that the Java environment includes a generally useful set of packages of classes and a freely available implementation of them. This takes care of a part of what is needed, but it is crucial also to specify exactly how the run-time environment of Java behaves.

This final requirement is what has stymied many attempts at ubiquity in the past. If you base your system on any assumptions about what is "beneath" the run-time system, you lose. If you depend in any way on the computer or operating system below, you lose. Java solves this problem by inventing an abstract computer of its own and running on that.

This "virtual" machine runs a special set of "instructions" called bytecodes that are simply a stream of formatted bytes, each of which has a precise specification of exactly what each bytecode does to this virtual machine. The virtual machine is also responsible for certain fundamental capabilities of Java, such as object creation and garbage collection.

Finally, in order to be able to move bytecodes safely across the Internet, you need a bulletproof model of security—and how to maintain it—and a precise format for how this stream of bytecodes can be sent from one virtual machine to another.

Each of these requirements is addressed in today's lesson.


Note: This discussion blurs the distinction between the run-time and the virtual machine of Java. This is intentional but a little unconventional. Think of the virtual machine as providing all the capabilities, even those that are conventionally assigned to the run-time. This book uses the words "run-time" and "virtual machine" interchangeably. Equating the two highlights the single environment that must be created to support Java.

Much of the following description is based closely on the latest "Virtual Machine Specifications" documents (and the 1.0 bytecodes), so if you delve more deeply into the details online, you should cover some familiar ground.

An Overview

It is worth quoting the introduction to the Java virtual machine documentation here, because it is so relevant to the vision outlined earlier:

The Java virtual machine specification has a purpose that is both like and unlike equivalent documents for other languages and abstract machines. It is intended to present an abstract, logical machine design free from the distraction of inconsequential details of any implementation. It does not anticipate an implementation technology, or an implementation host. At the same time it gives a reader sufficient information to allow implementation of the abstract design in a range of technologies.

However, the intent of the [...] Java project is to create a language [...] that will allow the interchange over the Internet of "executable content," which will be embodied by compiled Java code. The project specifically does not want Java to be a proprietary language and does not want to be the sole purveyor of Java language implementations. Rather, we hope to make documents like this one, and source code for our implementation, freely available for people to use as they choose.

This vision [...] can be achieved only if the executable content can be reliably shared between different Java implementations. These intentions prohibit the definition of the Java virtual machine from being fully abstract. Rather, relevant logical elements of the design have to be made sufficiently concrete to allow the interchange of compiled Java code. This does not collapse the Java virtual machine specification to a description of a Java implementation; elements of the design that do not play a part in the interchange of executable content remain abstract. But it does force us to specify, in addition to the abstract machine design, a concrete interchange format for compiled Java code.

The Java virtual machine specification consists of the following:

Each of these is covered today.

Despite this degree of specificity, there are still several elements of the design that remain (purposely) abstract, including the following:

These places are where the creativity of a virtual machine implementor has full rein.

The Fundamental Parts

The Java virtual machine can be divided into five fundamental pieces:

Some of these might be implemented by using an interpreter, a native binary code compiler, or even a hardware chip—but all these logical, abstract components of the virtual machine must be supplied in some form in every Java system.


Note: The memory areas used by the Java virtual machine are not required to be at any particular place in memory, to be in any particular order, or even to use contiguous memory. However, all but the method area must be able to represent align 32-bit values (for example, the Java stack is 32 bits wide).

The virtual machine, and its supporting code, is often referred to as the run-time environment, and when this book refers to something being done at run-time, the virtual machine is what's doing it.

Java Bytecodes

The Java virtual machine instruction set is optimized to be small and compact. It is designed to travel across the Net, and so has traded off speed-of-interpretation for space. (Given that both Net bandwidth and mass storage speeds increase less rapidly than CPU speed, this seems like an appropriate trade-off.)

As mentioned, Java source code is "compiled" into bytecodes and stored in a .class file. On Sun's Java system, this is performed using the javac tool. It is not exactly a traditional "compiler," because javac translates source code into bytecodes, a lower-level format that cannot be run directly, but must be further interpreted by each computer. Of course, it is exactly this level of "indirection" that buys you the power, flexibility, and extreme portability of Java code.


Note: Quotation marks are used around the word "compiler" when talking about javac because later today you will also learn about the "just-in-time" compiler, which acts more like the back end of a traditional compiler. The use of the same word "compiler" for these two different pieces of Java technology is unfortunate, but somewhat reasonable, because each is really one-half (either the front or the back end) of a more traditional compiler.

A bytecode instruction consists of a one-byte opcode that serves to identify the instruction involved and zero or more operands, each of which may be more than one byte long, that encode the parameters the opcode requires.


Note: When operands are more than one byte long, they are stored in big-endian order, high-order byte first. These operands must be assembled from the byte stream at run-time. For example, a 16-bit parameter appears in the stream as two bytes so that its value is first_byte * 256 + second_byte. The bytecode instruction stream is only byte-aligned, and alignment of any larger quantities is not guaranteed (except for "within" the special bytecodes lookupswitch and tableswitch, which have special alignment rules of their own).

Bytecodes interpret data in the run-time memory areas as belonging to a fixed set of types: the primitive types you've seen several times before, consisting of several signed integer types (8-bit byte, 16-bit short, 32-bit int, 64-bit long), one unsigned integer type (16-bit char), and two signed floating-point types (32-bit float, 64-bit double), plus the type "reference to an object" (a 32-bit pointer-like type). Some special bytecodes (for example, the dup instructions), treat run-time memory areas as raw data, without regard to type. This is the exception, however, not the rule.

These primitive types are distinguished and managed by the compiler, javac, not by the Java run-time environment. These types are not "tagged" in memory, and thus cannot be distinguished at run-time. Different bytecodes are designed to handle each of the various primitive types uniquely, and the compiler carefully chooses from this palette based on its knowledge of the actual types stored in the various memory areas. For example, when adding two integers, the compiler generates an iadd bytecode; for adding two floats, fadd is generated. (You'll see all this in gruesome detail later.)

Registers

The registers of the Java virtual machine are just like the registers inside a "real" computer.

Registers hold the machine's state, affect its operation, and are updated after each bytecode is executed.

The following are the Java registers:

The virtual machine defines these registers to be 32 bits wide.


Note: Because the virtual machine is primarily stack-based, it does not use any registers for passing or receiving arguments. This is a conscious choice skewed toward bytecode simplicity and compactness. It also aids efficient implementation on register-poor architectures, which most of today's computers, unfortunately, are. Perhaps when the majority of CPUs out there are a little more sophisticated, this choice will be reexamined, though simplicity and compactness may still be reason enough!

By the way, the pc register is also used when the run-time handles exceptions; catch clauses are (ultimately) associated with ranges of the pc within a method's bytecodes.

The Stack

The Java virtual machine is stack-based. A Java stack frame is similar to the stack frame of a conventional programming language—it holds the state for a single method call. Frames for nested method calls are stacked on top of this frame.


The stack is used to supply parameters to bytecodes and methods, and to receive results back from them.

Each stack frame contains three (possibly empty) sets of data: the local variables for the method call, its execution environment, and its operand stack. The sizes of these first two are fixed at the start of a method call, but the operand stack varies in size as bytecodes are executed in the method.

Local variables are stored in an array of 32-bit slots, indexed by the register vars. Most types take up one slot in the array, but the long and double types each take up two slots.


Note: long and double values, stored or referenced via an index N, take up the (32-bit) slots[]and[]+ 1. These 64-bit values are thus not guaranteed to be 64-bit-aligned. Implementors are free to decide the appropriate way to divide these values among the two slots.

The execution environment in a stack frame helps to maintain the stack itself. It contains a pointer to the previous stack frame, a pointer to the local variables of the method call, and pointers to the stack's current "base" and "top." Additional debugging information can also be placed into the execution environment.

The operand stack, a 32-bit first-in-first-out (FIFO) stack, is used to store the parameters and return values of most bytecode instructions. For example, the iadd bytecode expects two integers to be stored on the top of the stack. It pops them, adds them together, and pushes the resulting sum back onto the stack.

Each primitive data type has unique instructions that know how to extract, operate, and push back operands of that type. For example, long and double operands take two "slots" on the stack, and the special bytecodes that handle these operands take this into account. It is illegal for the types on the stack and the instruction operating on them to be incompatible (javac outputs bytecodes that always obey this rule).


Note: The top of the operand stack and the top of the overall Java stack are almost always the same. Thus, "the stack," refers to both stacks, collectively.

The Heap

The heap is that part of memory from which newly created instances (objects) are allocated.

The heap is often assigned a large, fixed size when the Java run-time system is started, but on systems that support virtual memory, it can grow as needed, in a nearly unbounded fashion.

Because objects are automatically garbage-collected in Java, programmers do not have to (and, in fact, cannot) manually free the memory allocated to an object when they are finished using it.

Java objects are referenced indirectly in the run-time, via handles, which are a kind of pointer into the heap.

Because objects are never referenced directly, parallel garbage collectors can be written that operate independently of your program, moving around objects in the heap at will. You'll learn more about garbage collection later.

The Method Area

Like the compiled code areas of conventional programming language environments, or the TEXT segment in a UNIX process, the method area stores the Java bytecodes that implement almost every method in the Java system. (Remember that some methods might be native, and thus implemented, for example, in C.) The method area also stores the symbol tables needed for dynamic linking, and any other additional information debuggers or development environments which might want to associate with each method's implementation.

Because bytecodes are stored as byte streams, the method area is aligned on byte boundaries. (The other areas are all aligned on 32-bit word boundaries.)

The Constant Pool

In the heap, each class has a constant pool "attached" to it. Usually created by javac, these constants encode all the names (of variables, methods, and so forth) used by any method in a class. The class contains a count of how many constants there are and an offset that specifies how far into the class description itself the array of constants begins. These constants are typed via specially coded bytes and have a precisely defined format when they appear in the .class file for a class. Later today, a little of this file format is covered, but everything is fully specified by the virtual machine specifications in your Java release.

Limitations

The virtual machine, as currently defined, places some restrictions on legal Java programs by virtue of the choices it has made (some were previously described, and more will be detailed later today).

These limitations and their implications are

In addition, Sun's implementation of the virtual machine uses so-called _quick bytecodes, which further limit the system. Unsigned 8-bit offsets into objects may limit the number of methods in a class to 256 (this limit may not exist in the final release), and unsigned 8-bit argument counts limit the size of the argument list to 255 32-bit words. (Although this means that you can have up to 255 arguments of most types, you can have only 127 of them if they're all long or double.)

Bytecodes in More Detail

One of the main tasks of the virtual machine is the fast, efficient execution of the Java bytecodes in methods. Unlike in the discussion yesterday about generality versus efficiency, this is a case where speed is of the utmost importance. Every Java program suffers from a slow implementation here, so the run-time must use as many "tricks" as possible to make bytecodes run fast. The only other goal (or limitation) is that Java programmers must not be able to see these tricks in the behavior of their programs.

A Java run-time implementer must be extremely clever to satisfy both these goals.

The Bytecode Interpreter

A bytecode interpreter examines each opcode byte (bytecode) in a method's bytecode stream, in turn, and executes a unique action for that bytecode. This might consume further bytes for the operands of the bytecode and might affect which bytecode will be examined next. It operates like the hardware CPU in a computer, which examines memory for instructions to carry out in exactly the same manner. It is the software CPU of the Java virtual machine.

Your first, naive attempt to write such a bytecode interpreter will almost certainly be disastrously slow. The inner loop, which dispatches one bytecode each time through the loop, is notoriously difficult to optimize. In fact, smart people have been thinking about this problem, in one form or another, for more than 20 years. Luckily, they've gotten results, all of which can be applied to Java.

The final result is that the interpreter shipped in the current release of Java has an extremely fast inner loop. In fact, on even a relatively slow computer, this interpreter can perform more than 590,000 bytecodes per second! This is really quite good, because the CPU in that computer does only about 30 times better using hardware.

This interpreter is fast enough for most Java programs (and for those requiring more speed, they can always use native methods—see yesterday's discussion)—but what if a smart implementor wants to do better?

The Just-in-Time Compiler

About a decade ago, a really clever trick was discovered by Peter Deutsch while trying to make Smalltalk run faster. He called it "dynamic translation" during interpretation. Sun calls it "just-in-time" compiling.

The trick is to notice that the really fast interpreter you've just written—in C, for example—already has a useful sequence of native binary code for each bytecode that it interprets: the binary code that the interpreter itself is executing. Because the interpreter has already been compiled from C into native binary code, for each bytecode that it interprets, it passes through a sequence of native code instructions for the hardware CPU on which it is running. By saving a copy of each binary instruction as it "goes by," the interpreter can keep a running log of the binary code it itself has run to interpret a bytecode. It can just as easily keep a log of the set of bytecodes
that it ran to interpret an entire method.

You take that log of instructions and "peephole-optimize" it, just as a smart compiler does. This eliminates redundant or unnecessary instructions from the log, and makes it look just like the optimized binary code that a good compiler might have produced.


Note: This is where the name compiler comes from, in "just-in-time" compiler, but it's really only the back end of a traditional compiler—the part that does code generation. By the way, the front end here is javac.

Here's where the trick comes in. The next time that method is run (in exactly the same way), the interpreter can now simply execute directly the stored log of binary native code. Because this optimizes out the inner-loop overhead of each bytecode, as well as any other redundancies between the bytecodes in a method, it can gain a factor of 10 or more in speed. In fact, an experimental version of this technology at Sun has shown that Java programs using it can run as fast as compiled C programs.


Note: The parenthetical in the last paragraph is needed because if anything is different about the input to the method, it takes a different path through the interpreter and must be relogged. (There are sophisticated versions of this technology that solve this, and other, difficulties.) The cache of native code for a method must be invalidated whenever the method has changed, and the interpreter must pay a small cost up front each time a method is run for the first time. However, these small bookkeeping costs are far outweighed by the amazing gains in speed possible.

The java2c Translator

Another, simpler, trick, which works well whenever you have a good, portable C compiler on each system that runs your program, is to translate the bytecodes into C and then compile the C into binary native code. If you wait until the first use of a method or class, and then perform this as an "invisible" optimization, it gains you an additional speedup over the approach outlined previously, without the Java programmer needing to know about it.

Of course, this does limit you to systems with a C compiler, but as you learned yesterday, there are extremely good, freely available C compilers. In theory, your Java code might be able to travel with its own C compiler, or know where to pull one from the Net as needed, for each new computer and operating system it faced. (Because this violates some of the rules of normal Java code movement over the Net, though, it should be used sparingly.)

If you're using Java, for example, to write a server that lives only on your computer, it might be appropriate to use Java for its flexibility in writing and maintaining the server (and for its capability of dynamically linking new Java code on the fly), and then to run java2c by hand to translate the basic server itself entirely into native code. You'd link the Java run-time environment into that code so that your server remains a fully capable Java program, but it's now an extremely fast one.

In fact, an experimental version of the java2c translator inside Sun shows that it can reach the speed of compiled and optimized C code. This is the best that you can hope to do!


Note: Unfortunately, as of the 1.0 release, there is still no publicly available java2c tool, and Sun's virtual machine does not perform "just-in-time" compilation. Both of these have been promised in a later release (perhaps 1.1).

The Bytecodes Themselves

Let's look at a (progressively less and less) detailed description of each class of bytecodes.

For each bytecode, some brief text describes its function, and a textual "picture" of the stack, both before and after the bytecode has been executed, is shown. This text picture will look like the following:

..., value1, value2 => ..., value3

This means that the bytecode expects two operands—value1 and value2—to be on the top of the stack, pops them both off the stack, operates on them to produce value3, and pushes value3 back onto the top of the stack. You should read each stack from right to left, with the rightmost value being the top of the stack. The ... is read as "the rest of the stack below," which is irrelevant to the current bytecode. All operands on the stack are 32-bits wide.

Because most bytecodes take their arguments from the stack and place their results back there, the brief text descriptions that follow only say something about the source or destination of values if they are not on the stack. For example, the description "Load integer from local variable." means that the integer is loaded onto the stack, and "Integer add." intends its integers to be taken from—and the result returned to—the stack.

Bytecodes that don't affect control flow simply move the pc onto the next bytecode that follows in sequence. Those that do affect the pc say so explicitly. Whenever you see byte1, byte2, and so forth, it refers to the first byte, second byte, and so on, that follow the opcode byte itself. After such a bytecode is executed, the pc automatically advances over these operand bytes to start the next bytecode in sequence.


Note: The next few sections are in "reference manual style," presenting each bytecode separately in all its (often redundant) detail. Later sections begin to collapse and coalesce this verbose style into something shorter, and more readable. The verbose form is shown at first because the online reference manuals will look more like it, and because it drives home the point that each bytecode "function" comes in many, nearly identical bytecodes, one for each primitive type in Java.

Pushing Constants onto the Stack
bipush        ... => ..., value

Push one-byte signed integer. byte1 is interpreted as a signed 8-bit value. This value is expanded to an int and pushed onto the operand stack.

sipush        ... => ..., value

Push two-byte signed integer. byte1 and byte2 are assembled into a signed 16-bit value. This value is expanded to an int and pushed onto the operand stack.

ldc1          ... => ..., item

Push item from constant pool. byte1 is used as an unsigned 8-bit index into the constant pool of the current class. The item at that index is resolved and pushed onto the stack.

ldc2          ... => ..., item

Push item from constant pool. byte1 and byte2 are used to construct an unsigned 16-bit index into the constant pool of the current class. The item at that index is resolved and pushed onto the stack.

ldc2w         ... => ..., constant-word1, constant-word2

Push long or double from constant pool. byte1 and byte2 are used to construct an unsigned 16-bit index into the constant pool of the current class. The two-word constant at that index is resolved and pushed onto the stack.

aconst_null   ... => ..., null

Push the null object reference onto the stack.

iconst_m1     ... => ..., -1

Push the int -1 onto the stack.

iconst_<I>    ... => ..., <I>

Push the int <I> onto the stack. There are six of these bytecodes, one for each of the integers 0-5: iconst_0, iconst_1, iconst_2, iconst_3, iconst_4, and iconst_5.

lconst_<L>    ... => ..., <L>-word1, <L>-word2

Push the long <L> onto the stack. There are two of these bytecodes, one for each of the integers 0 and 1: lconst_0, and lconst_1.

fconst_<F>    ... => ..., <F>

Push the float <F> onto the stack. There are three of these bytecodes, one for each of the integers 0-2: fconst_0, fconst_1, and fconst_2.

dconst_<D>    ... => ..., <D>-word1, <D>-word2

Push the double <D> onto the stack. There are two of these bytecodes, one for each of the integers 0 and 1: dconst_0, and dconst_1.

Loading Local Variables onto the Stack
iload         ... => ..., value

Load int from local variable. Local variable byte1 in the current Java frame must contain an int. The value of that variable is pushed onto the operand stack.

iload_<I>     ... => ..., value

Load int from local variable. Local variable <I> in the current Java frame must contain an int. The value of that variable is pushed onto the operand stack. There are four of these bytecodes, one for each of the integers 0-3: iload_0, iload_1, iload_2, and iload_3.

lload         ... => ..., value-word1, value-word2

Load long from local variable. Local variables byte1 and byte1 + 1 in the current Java frame must together contain a long integer. The values contained in those variables are pushed onto the operand stack.

lload_<L>     ... => ..., value-word1, value-word2

Load long from local variable. Local variables <L> and <L> + 1 in the current Java frame must together contain a long integer. The value contained in those variables is pushed onto the operand stack. There are four of these bytecodes, one for each of the integers 0-3: lload_0, lload_1, lload_2, and lload_3.

fload         ... => ..., value

Load float from local variable. Local variable byte1 in the current Java frame must contain a single precision floating-point number. The value of that variable is pushed onto the operand stack.

fload_<F>     ... => ..., value

Load float from local variable. Local variable <F> in the current Java frame must contain a single precision floating-point number. The value of that variable is pushed onto the operand stack. There are four of these bytecodes, one for each of the integers 0-3: fload_0, fload_1, fload_2, and fload_3.

dload         ... => ..., value-word1, value-word2

Load double from local variable. Local variables byte1 and byte1 + 1 in the current Java frame must together contain a double precision floating-point number. The value contained in those variables is pushed onto the operand stack.

dload_<D>     ... => ..., value-word1, value-word2

Load double from local variable. Local variables <D> and <D> + 1 in the current Java frame must together contain a double precision floating-point number. The value contained in those variables is pushed onto the operand stack There are four of these bytecodes, one for each of the integers 0-3: dload_0, dload1, dload_2, and dload_3.

aload         ... => ..., value

Load object reference from local variable. Local variable byte1 in the current Java frame must contain a return address or reference to an object or array. The value of that variable is pushed onto the operand stack.

aload_<A>     ... => ..., value

Load object reference from local variable. Local variable <A> in the current Java frame must contain a return address or reference to an object. The value of that variable is pushed onto the operand stack. There are four of these bytecodes, one for each of the integers 0-3: aload_0, aload_1, aload_2, and aload_3.

Storing Stack Values into Local Variables
istore        ..., value => ...

Store int into local variable. value must be an int. Local variable byte1 in the current Java frame is set to value.

istore_<I>    ..., value => ...

Store int into local variable. value must be an int. Local variable <I> in the current Java frame is set to value. There are four of these bytecodes, one for each of the integers 0-3: istore_0, istore_1, istore_2, and istore_3.

lstore        ..., value-word1, value-word2 => ...

Store long into local variable. value must be a long integer. Local variables byte1 and byte1 + 1 in the current Java frame are set to value.

lstore_<L>    ..., value-word1, value-word2 => ...

Store long into local variable. value must be a long integer. Local variables <L> and <L> + 1 in the current Java frame are set to value. There are four of these bytecodes, one for each of the integers 0-3: lstore_0, lstore_1, lstore_2, and lstore_3.

fstore        ..., value => ...

Store float into local variable. value must be a single precision floating-point number. Local variable byte1 in the current Java frame is set to value.

fstore_<F>    ..., value => ...

Store float into local variable. value must be a single precision floating-point number. Local variable <F> in the current Java frame is set to value. There are four of these bytecodes, one for each of the integers 0-3: fstore_0, fstore_1, fstore_2, and fstore_3.

dstore        ..., value-word1, value-word2 => ...

Store double into local variable. value must be a double precision floating-point number. Local variables byte1 and byte1 + 1 in the current Java frame are set to value.

dstore_<D>    ..., value-word1, value-word2 => ...

Store double into local variable. value must be a double precision floating-point number. Local variables <D> and <D> + 1 in the current Java frame are set to value. There are four of these bytecodes, one for each of the integers 0-3: dstore_0, dstore_1, dstore_2, and dstore_3.

astore        ..., handle => ...

Store object reference into local variable. handle must be a return address or a reference to an object. Local variable byte1 in the current Java frame is set to value.

astore_<A>    ..., handle => ...

Store object reference into local variable. handle must be a return address or a reference to an object. Local variable <A> in the current Java frame is set to value. There are four of these bytecodes, one for each of the integers 0-3: astore_0, astore_1, astore_2, and astore_3.

iinc          -no change-

Increment local variable by constant. Local variable byte1 in the current Java frame must contain an int. Its value is incremented by the value byte2, where byte2 is treated as a signed 8-bit quantity.

Managing Arrays
newarray        ..., size => result

Allocate new array. size must be an int. It represents the number of elements in the new array. byte1 is an internal code that indicates the type of array to allocate. Possible values for byte1 are as follows: T_BOOLEAN (4), T_CHAR (5), T_FLOAT (6), T_DOUBLE (7), T_BYTE (8), T_SHORT (9), T_INT (10), and T_LONG (11).

An attempt is made to allocate a new array of the indicated type, capable of holding size elements. This will be the result. If size is less than zero, a NegativeArraySizeException is thrown. If there is not enough memory to allocate the array, an OutOfMemoryError is thrown. All elements of the array are initialized to their default values.

anewarray       ..., size => result

Allocate new array of objects. size must be an int. It represents the number of elements in the new array. byte1 and byte2 are used to construct an index into the constant pool of the current class. The item at that index is resolved. The resulting entry must be a class.

An attempt is made to allocate a new array of the indicated class type, capable of holding size elements. This will be the result. If size is less than zero, a NegativeArraySizeException is thrown. If there is not enough memory to allocate the array, an OutOfMemoryError is thrown. All elements of the array are initialized to null.


Note: anewarray is used to create a single dimension of an array of objects. For example, the request new Thread[7] generates the following bytecodes:

bipush 7
anewarray <Class "java.lang.Thread">

anewarray can also be used to create the outermost dimension of a multidimensional array. For example, the array declaration new int[6][] generates this:

bipush 6
anewarray <Class "[I">

(See the section "Method Signatures" for more information on strings such as [I.)

multianewarray  ..., size1 size2...sizeN => result

Allocate new multidimensional array. Each size<I> must be an int. Each represents the number of elements in a dimension of the array. byte1 and byte2 are used to construct an index into the constant pool of the current class. The item at that index is resolved. The resulting entry must be an array class of one or more dimensions.

byte3 is a positive integer representing the number of dimensions being created. It must be less than or equal to the number of dimensions of the array class. byte3 is also the number of elements that are popped off the stack. All must be ints greater than or equal to zero. These are used as the sizes of the dimensions. An attempt is made to allocate a new array of the indicated class type, capable of holding size<1> * size<2> * ... * <sizeN> elements. This will be the result. If any of the size<I> arguments on the stack is less than zero, a NegativeArraySizeException is thrown. If there is not enough memory to allocate the array, an OutOfMemoryError is thrown.


Note: new int[6][3][] generates these bytecodes:

bipush 6
bipush 3
multianewarray <Class "[[[I"> 2

It's more efficient to use newarray or anewarray when creating arrays of single dimension.

arraylength     ..., array => ..., length

Get length of array. array must be a reference to an array object. The length of the array is determined and replaces array on the top of the stack. If array is null, a NullPointerException is thrown.

iaload          ..., array, index => ..., value
laload          ..., array, index => ..., value-word1, value-word2
faload          ..., array, index => ..., value
daload          ..., array, index => ..., value-word1, value-word2
aaload          ..., array, index => ..., value
baload          ..., array, index => ..., value
caload          ..., array, index => ..., value
saload          ..., array, index => ..., value

Load <type> from array. array must be an array of <type>s. index must be an int. The <type> value at position number index in array is retrieved and pushed onto the top of the stack. If array is null, a NullPointerException is thrown. If index is not within the bounds of array, an ArrayIndexOutOfBoundsException is thrown. <type> is, in turn, int, long, float, double, object reference, byte, char, and short. <type>s long and double have two word values, as you've seen in previous load bytecodes.

iastore         ..., array, index, value => ...
lastore         ..., array, index, value-word1, value-word2 => ...
fastore         ..., array, index, value => ...
dastore         ..., array, index, value-word1, value-word2 => ...
aastore         ..., array, index, value => ...
bastore         ..., array, index, value => ...
castore         ..., array, index, value => ...
sastore         ..., array, index, value => ...

Store into <type> array. array must be an array of <type>s, index must be an int, and value a <type>. The <type> value is stored at position index in array. If array is null, a NullPointerException is thrown. If index is not within the bounds of array, an ArrayIndexOutOfBoundsException is thrown. <type> is, in turn, int, long, float, double, object reference, byte, char, and short. <type>s long and double have two word values, as you've seen in previous store bytecodes.

Stack Operations
nop        -no change-

Do nothing.

pop        ..., any => ...

Pop the top word from the stack.

pop2       ..., any2, any1 => ...

Pop the top two words from the stack.

dup        ..., any => ..., any, any

Duplicate the top word on the stack.

dup2       ..., any2, any1 => ..., any2, any1, any2,any1

Duplicate the top two words on the stack.

dup_x1     ..., any2, any1 => ..., any1, any2,any1

Duplicate the top word on the stack and insert the copy two words down in the stack.

dup2_x1    ..., any3, any2, any1 => ..., any2, any1, any3,any2,any1

Duplicate the top two words on the stack and insert the copies two words down in the stack.

dup_x2     ..., any3, any2, any1 => ..., any1, any3,any2,any1

Duplicate the top word on the stack and insert the copy three words down in the stack.

dup2_x2    ..., any4, any3, any2, any1 => ..., any2, any1, any4,any3,any2,any1

Duplicate the top two words on the stack and insert the copies three words down in the stack.

swap       ..., any2, any1 => ..., any1, any2

Swap the top two elements on the stack.

Arithmetic Operations
iadd       ..., v1, v2 => ..., result
ladd       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2
fadd       ..., v1, v2 => ..., result
dadd       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2

v1 and v2 must be <type>s. The vs are added and are replaced on the stack by their <type> sum. <type> is, in turn, int, long, float, and double.

isub       ..., v1, v2 => ..., result
lsub       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2
fsub       ..., v1, v2 => ..., result
dsub       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2

v1 and v2 must be <type>s. v2 is subtracted from v1, and both vs are replaced on the stack by their <type> difference. <type> is, in turn, int, long, float, and double.

imul       ..., v1, v2 => ..., result
lmul       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2
fmul       ..., v1, v2 => ..., result
dmul       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2

v1 and v2 must be <type>s. Both vs are replaced on the stack by their <type> product. <type> is, in turn, int, long, float, and double.

idiv       ..., v1, v2 => ..., result
ldiv       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2
fdiv       ..., v1, v2 => ..., result
ddiv       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2

v1 and v2 must be <type>s. v2 is divided by v1, and both vs are replaced on the stack by their <type> quotient. An attempt to divide by zero results in an ArithmeticException being thrown. <type> is, in turn, int, long, float, and double.

irem       ..., v1, v2 => ..., result
lrem       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2
frem       ..., v1, v2 => ..., result
drem       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2

v1 and v2 must be <type>s. v2 is divided by v1, and both vs are replaced on the stack by their <type> remainder. An attempt to divide by zero results in an ArithmeticException being thrown. <type> is, in turn, int, long, float, and double.

ineg       ..., value => ..., result
lneg       ..., value-word1, value-word2 => ..., result-word1, result-word2
fneg       ..., value => ..., result
dneg       ..., value-word1, value-word2 => ..., result-word1, result-word2

value must be a <type>. It is replaced on the stack by its arithmetic negation. <type> is, in turn, int, long, float, and double.


Note: Now that you're familiar with the look of the bytecodes, the summaries that follow will become shorter and shorter (for space reasons). You can always get any desired level of detail from the full virtual machine specification in the latest Java release.

Logical Operations
ishl       ..., v1, v2 => ..., result
lshl       ..., v1-word1, v1-word2, v2 => ..., r-word1, r-word2
ishr       ..., v1, v2 => ..., result
lshr       ..., v1-word1, v1-word2, v2 => ..., r-word1, r-word2
iushr      ..., v1, v2 => ..., result
lushr      ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2

For types int and long: arithmetic shift-left, shift-right, and logical shift-right.

iand       ..., v1, v2 => ..., result
land       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2
ior        ..., v1, v2 => ..., result
lor        ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2
ixor       ..., v1, v2 => ..., result
lxor       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., r-word1, r-word2

For types int and long: bitwise AND, OR, and XOR.

Conversion Operations
i2l         ..., value => ..., result-word1, result-word2
i2f         ..., value => ..., result
i2d         ..., value => ..., result-word1, result-word2
l2i         ..., value-word1, value-word2 => ..., result
l2f         ..., value-word1, value-word2 => ..., result
l2d         ..., value-word1, value-word2 => ..., result-word1, result-word2
f2i         ..., value => ..., result
f2l         ..., value => ..., result-word1, result-word2
f2d         ..., value => ..., result-word1, result-word2
d2i         ..., value-word1, value-word2 => ..., result
d2l         ..., value-word1, value-word2 => ..., result-word1, result-word2
d2f         ..., value-word1, value-word2 => ..., result
int2byte    ..., value => ..., result
int2char    ..., value => ..., result
int2short   ..., value => ..., result

These bytecodes convert from a value of type <lhs> to a result of type <rhs>. <lhs> and <rhs> can be any of i, l, f,and d, which represent int, long, float, and double, respectively. The final three bytecodes convert types that are self-explanatory.

Transfer of Control
ifeq        ..., value => ...
ifne        ..., value => ...
iflt        ..., value => ...
ifgt        ..., value => ...
ifle        ..., value => ...
ifge        ..., value => ...
if_icmpeq   ..., value1, value2 => ...
if_icmpne   ..., value1, value2 => ...
if_icmplt   ..., value1, value2 => ...
if_icmpgt   ..., value1, value2 => ...
if_icmple   ..., value1, value2 => ...
if_icmpge   ..., value1, value2 => ...
ifnull      ..., value => ...
ifnonnull   ..., value => ...

When value <rel> 0 is true in the first set of bytecodes, value1 <rel> value2 is true in the second set, or value is null (or not null) in the third, byte1 and byte2 are used to construct a signed 16-bit offset. Execution proceeds at that offset from the pc. Otherwise, execution proceeds at the bytecode following. <rel> is one of eq, ne, lt, gt, le, and ge, which represent equal, not equal, less than, greater than, less than or equal, and greater than or equal, respectively.

lcmp        ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., result
fcmpl       ..., v1, v2 => ..., result
dcmpl       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., result
fcmpg       ..., v1, v2 => ..., result
dcmpg       ..., v1-word1, v1-word2, v2-word1, v2-word2 => ..., result

v1 and v2 must be long, float, or double. They are both popped from the stack and compared. If v1 is greater than v2, the int value 1 is pushed onto the stack. If v1 is equal to v2, 0 is pushed onto the stack. If v1 is less than v2, -1 is pushed onto the stack. For floating-point, if either v1 or v2 is NaN, -1 is pushed onto the stack for the first pair of bytecodes, +1 for the second pair.

if_acmpeq   ..., value1, value2 => ...
if_acmpne   ..., value1, value2 => ...

Branch if object references are equal/not equal. value1 and value2 must be references to objects. They are both popped from the stack. If value1 is equal/not equal to value2, byte1 and byte2 are used to construct a signed 16-bit offset. Execution proceeds at that offset from the pc. Otherwise, execution proceeds at the bytecode following.

goto        -no change-
goto_w      -no change-

Branch always. byte1 and byte2 (plus byte3 and byte4 for goto_w) are used to construct a signed 16-bit (32-bit) offset. Execution proceeds at that offset from the pc.

jsr         ... => ..., return-address
jsr—w       ... => ..., return-address

Jump subroutine. The address of the bytecode immediately following the jsr is pushed onto the stack. byte1 and byte2 (plus byte3 and byte4 for goto_w) are used to construct a signed 16-bit (32-bit) offset. Execution proceeds at that offset from the pc.

ret         -no change-
ret2_w      -no change-

Return from subroutine. Local variable byte1 (plus byte2 for ret_w are assembled into a 16-bit index) in the current Java frame must contain a return address. The contents of that local variable are written into the pc.


Note: jsr pushes the address onto the stack, and ret gets it out of a local variable. This asymmetry is intentional. The jsr and ret bytecodes are used in the implementation of Java's finally keyword.

Method Return
return      ... => [empty]

Return (void) from method. All values on the operand stack are discarded. The interpreter then returns control to its caller.

ireturn     ..., value => [empty]
lreturn     ..., value-word1, value-word2 => [empty]
freturn     ..., value => [empty]
dreturn     ..., value-word1, value-word2 => [empty]
areturn     ..., value => [empty]

Return <type> from method. value must be a <type>. The value is pushed onto the stack of the previous execution environment. Any other values on the operand stack are discarded. The interpreter then returns control to its caller. <type> is, in turn, int, long, float, double, and object reference.


Note: The stack behavior of the "return" bytecodes may be confusing to anyone expecting the Java operand stack to be just like the C stack. Java's operand stack actually consists of a number of discontiguous segments, each corresponding to a method call. A return bytecode empties the Java operand stack segment corresponding to the frame of the returning call, but does not affect the segment of any parent calls.

Table Jumping
tableswitch   ..., index => ...

tableswitch is a variable-length bytecode. Immediately after the tableswitch opcode, zero to three 0 bytes are inserted as padding so that the next byte begins at an address that is a multiple of four. After the padding are a series of signed 4-byte quantities: default-offset, low, high, and then (high - low + 1) further signed 4-byte offsets. These offsets are treated as a 0-based jump table.

The index must be an int. If index is less than low or index is greater than high, default-offset is added to the pc. Otherwise, the (index - low)'th element of the jump table is extracted and added to the pc.

lookupswitch  ..., key => ...

lookupswitch is a variable-length bytecode. Immediately after the lookupswitch opcode, zero to three 0 bytes are inserted as padding so that the next byte begins at an address that is a multiple of four. Immediately after the padding is a series of pairs of signed 4-byte quantities. The first pair is special; it contains the default-offset and the number of pairs that follow. Each subsequent pair consists of a match and an offset.

The key on the stack must be an int. This key is compared to each of the matches. If it is equal to one of them, the corresponding offset is added to the pc. If the key does not match any of the matches, the default-offset is added to the pc.

Manipulating Object Fields
putfield      ..., handle, value => ...
putfield      ..., handle, value-word1, value-word2 => ...

Set field in object. byte1 and byte2 are used to construct an index into the constant pool of the current class. The constant pool item is a field reference to a class name and a field name. The item is resolved to a field block pointer containing the field's width and offset (both in bytes).

The field at that offset from the start of the instance pointed to by handle will be set to the value on the top of the stack. The first stack picture is for 32-bit, and the second for 64-bit wide fields. This bytecode handles both. If handle is null, a NullPointerException is thrown. If the specified field is a static field, an IncompatibleClassChangeError is thrown.

getfield      ..., handle => ..., value
getfield      ..., handle => ..., value-word1, value-word2

Fetch field from object. byte1 and byte2 are used to construct an index into the constant pool of the current class. The constant pool item will be a field reference to a class name and a field name. The item is resolved to a field block pointer containing the field's width and offset (both in bytes).

handle must be a reference to an object. The value at offset into the object referenced by handle replaces handle on the top of the stack. The first stack picture is for 32-bit, and the second for 64-bit wide fields. This bytecode handles both. If the specified field is a static field, an IncompatibleClassChangeError is thrown.

putstatic     ..., value => ...
putstatic     ..., value-word1, value-word2 => ...

Set static field in class. byte1 and byte2 are used to construct an index into the constant pool of the current class. The constant pool item will be a field reference to a static field of a class. That field will be set to have the value on the top of the stack. The first stack picture is for 32-bit, and the second for 64-bit wide fields. This bytecode handles both. If the specified field is not a static field, an IncompatibleClassChangeError is thrown.

getstatic     ..., => ..., value_
getstatic     ..., => ..., value-word1, value-word2

Get static field from class. byte1 and byte2 are used to construct an index into the constant pool of the current class. The constant pool item will be a field reference to a static field of a class. The value of that field is placed on the top of the stack. The first stack picture is for 32-bit, and the second for 64-bit wide fields. This bytecode handles both. If the specified field is not a static field, an IncompatibleClassChangeError is thrown.

Method Invocation
invokevirtual     ..., handle, [arg1, [arg2, ...]], ... => ...

Invoke instance method based on run-time type. The operand stack must contain a reference to an object and some number of arguments. byte1 and byte2 are used to construct an index into the constant pool of the current class. The item at that index in the constant pool contains the complete method signature. A pointer to the object's method table is retrieved from the object reference. The method signature is looked up in the method table. The method signature is guaranteed to exactly match one of the method signatures in the table.

The result of the lookup is an index into the method table of the named class that's used to look in the method table of the object's run-time type, where a pointer to the method block for the matched method is found. The method block indicates the type of method (native, synchronized, and so on) and the number of arguments (nargs) expected on the operand stack.

If the method is marked synchronized, the monitor associated with handle is entered.

The base of the local variables array for the new Java stack frame is set to point to handle on the stack, making handle and the supplied arguments (arg1, arg2, ...) the first nargs local variables of the new frame. The total number of local variables used by the method is determined, and the execution environment of the new frame is pushed after leaving sufficient room for the locals. The base of the operand stack for this method invocation is set to the first word after the execution environment. Finally, execution continues with the first bytecode of the matched method.

If handle is null, a NullPointerException is thrown. If during the method invocation a stack overflow is detected, a StackOverflowError is thrown.

invokenonvirtual  ..., handle, [arg1, [arg2, ...]], ... => ...

Invoke instance method based on compile-time type. The operand stack must contain a reference (handle) to an object and some number of arguments. byte1 and byte2 are used to construct an index into the constant pool of the current class. The item at that index in the constant pool contains the complete method signature and class. The method signature is looked up in the method table of the class indicated. The method signature is guaranteed to exactly match one of the method signatures in the table.

The result of the lookup is a method block. The method block indicates the type of method (native, synchronized, and so on) and the number of arguments (nargs) expected on the operand stack. (The last three paragraphs are identical to the previous bytecode.)

invokestatic      ..., , [arg1, [arg2, ...]], ... => ...

Invoke class (static) method. The operand stack must contain some number of arguments. byte1 and byte2 are used to construct an index into the constant pool of the current class. The item at that index in the constant pool contains the complete method signature and class. The method signature is looked up in the method table of the class indicated. The method signature is guaranteed to match one of the method signatures in the class's method table exactly.

The result of the lookup is a method block. The method block indicates the type of method (native, synchronized, and so on) and the number of arguments (nargs) expected on the operand stack.

If the method is marked synchronized, the monitor associated with the class is entered. (The last two paragraphs are identical to those in invokevirtual, except that no NullPointerException can be thrown.)

invokeinterface   ..., handle, [arg1, [arg2, ...]], ...=> ...

Invoke interface method. The operand stack must contain a reference (handle) to an object and some number of arguments. byte1 and byte2 are used to construct an index into the constant pool of the current class. The item at that index in the constant pool contains the complete method signature. A pointer to the object's method table is retrieved from the object reference. The method signature is looked up in the method table. The method signature is guaranteed to exactly match one of the method signatures in the table.

The result of the lookup is a method block. The method block indicates the type of method (native, synchronized, and so on) but, unlike the other "invoke" bytecodes, the number of available arguments (nargs) is taken from byte3; byte4 is reserved for future use. (The last three paragraphs are identical to those in invokevirtual.)

Exception Handling
athrow            ..., handle => [undefined]

Throw exception. handle must be a handle to an exception object. That exception, which must be an instance of Throwable (or a subclass), is thrown. The current Java stack frame is searched for the most recent catch clause that handles the exception. If a matching "catch-list" entry is found, the pc is reset to the address indicated by the catch-list pointer, and execution continues there.

If no appropriate catch clause is found in the current stack frame, that frame is popped and the exception is rethrown, starting the process all over again in the parent frame. If handle is null, then a NullPointerException is thrown instead.

Miscellaneous Object Operations
new               ... => ..., handle

Create new object. byte1 and byte2 are used to construct an index into the constant pool of the current class. The item at that index should be a class name that can be resolved to a class pointer, class. A new instance of that class is then created and a reference (handle) for the instance is placed on the top of the stack.

checkcast         ..., handle => ..., [handle | ...]

Make sure object is of given type. handle must be a reference to an object. byte1 and byte2 are used to construct an index into the constant pool of the current class. The string at that index of the constant pool is presumed to be a class name that can be resolved to a class pointer, class.

checkcast determines whether handle can be cast to a reference to an object of that class. (A null handle can be cast to any class.) If handle can be legally cast, execution proceeds at the next bytecode, and the handle remains on the stack. If not, a ClassCastException is thrown and the stack is emptied.

instanceof        ..., handle => ..., result

Determine whether object is of given type. handle must be a reference to an object. byte1 and byte2 are used to construct an index into the constant pool of the current class. The string at that index of the constant pool is presumed to be a class name that can be resolved to a class pointer, class.

If handle is null, the result is 0 (false). Otherwise, instanceof determines whether handle can be cast to a reference to an object of that class. The result is 1 (true) if it can, and 0 (false) otherwise.

Monitors
monitorenter      ..., handle => ...

Enter monitored region of code. handle must be a reference to an object. The interpreter attempts to obtain exclusive access via a lock mechanism to handle. If another thread already has handle locked, the current thread waits until the handle is unlocked. If the current thread already has handle locked, execution continues normally. If handle has no lock on it, this bytecode obtains an exclusive lock. (A null in either bytecode throws NullPointerException.)

monitorexit       ..., handle => ...

Exit monitored region of code. handle must be a reference to an object. The lock on handle is released. If this is the last lock that this thread has on that handle (one thread is allowed to have multiple locks on a single handle), other threads that are waiting for handle are allowed to proceed. (A null in either bytecode throws NullPointerException.)

Debugging
breakpoint        -no change-

Call breakpoint handler. The breakpoint bytecode is used to overwrite a bytecode to force control temporarily back to the debugger prior to the effect of the overwritten bytecode. The original bytecode's operands (if any) are not overwritten, and the original bytecode is restored when the breakpoint bytecode is removed.

The _quick Bytecodes

The following discussion, straight out of the Java virtual machine documentation, shows you an example of the cleverness mentioned earlier that's needed to make a bytecode interpreter fast:

The following set of pseudo-bytecodes, suffixed by _quick, are all variants of standard Java bytecodes. They are used by the run-time to improve the execution speed of the bytecode interpreter. They aren't officially part of the virtual machine specification and are invisible outside a Java virtual machine implementation. However, inside that implementation they have proven to be an effective optimization.

First, you should know that javac still generates only non-_quick bytecodes. Second, all bytecodes that have a _quick variant reference the constant pool. When _quick optimization is turned on, each non-_quick bytecode (that has a _quick variant) resolves the specified item in the constant pool, signals an error if the item in the constant pool could not be resolved for some reason, turns itself into the _quick variant of itself, and then performs its intended operation.

This is identical to the actions of the non-_quick bytecode, except for the step of overwriting itself with its _quick variant. The _quick variant of a bytecode assumes that the item in the constant pool has already been resolved, and that this resolution did not produce any errors. It simply performs the intended operation on the resolved item.

Thus, as your bytecodes are being interpreted, they are automatically getting faster and faster! Here are all the _quick variants in the current Java run-time:

ldc1_quick
ldc2_quick
ldc2w_quick
anewarray_quick
multinewarray_quick
putfield_quick
putfield2_quick
getfield_quick
getfield2_quick
putstatic_quick
putstatic2_quick
getstatic_quick
getstatic2_quick
invokevirtual_quick
invokevirtualobject_quick
invokenonvirtual_quick
invokestatic_quick
invokeinterface_quick
new_quick
checkcast_quick
instanceof_quick

If you'd like to go back in today's lesson and look at what each of these does, you can find the name of the original bytecode on which a _quick variant is based by simply removing the _quick from its name. The bytecodes putstatic, getstatic, putfield, and getfield have two _quick variants each, one for each stack picture in their original descriptions. invokevirtual has two variants: one for objects and one for arrays (to do fast lookups in java.lang.Object).


Note: One last note on the _quick optimization, regarding the unusual handling of the constant pool (for detail fanatics only):

When a class is read in, an array constant_pool[] of size nconstants is created and assigned to a field in the class. constant_pool[0] is set to point to a dynamically allocated array that indicates which fields in the constant_pool have already been resolved. constant_pool[1] through constant_pool[nconstants - 1] are set to point at the "type" field that corresponds to this constant item.

When a bytecode is executed that references the constant pool, an index is generated, and constant_pool[0] is checked to see whether the index has already been resolved. If so, the value of constant_pool[index] is returned. If not, the value of constant_pool[index] is resolved to be the actual pointer or data, and overwrites whatever value was already in constant_pool[index].

The .class File Format

You won't be given the entire .class file format here, only a taste of what it's like. (You can read all about it in the release documentation.) It's mentioned here because it is one of the parts of Java that needs to be specified carefully if all Java implementations are to be compatible with one another, and if Java byte codes are expected to travel across arbitrary networks—to and from arbitrary computers and operating systems—and yet arrive safely.

The rest of this section paraphrases, and extensively condenses, the latest release of the .class documentation.

class files are used to hold the compiled versions of both Java classes and Java interfaces. Compliant Java interpreters must be capable of dealing with all .class files that conform to the following specification.

A Java .class file consists of a stream of 8-bit bytes. All 16-bit and 32-bit quantities are constructed by reading in two or four 8-bit bytes, respectively. The bytes are joined together in big-endian order. (Use java.io.DataInput and java.io.DataOutput to read and write class files.)

The class file format is presented below as a series of C-struct-like structures. However, unlike a C struct, there is no padding or alignment between pieces of the structure, each field of the structure may be of variable size, and an array may be of variable size (in this case, some field prior to the array gives the array's dimension). The types u1, u2, and u4 represent an unsigned
one-, two-, or four-byte quantity, respectively.

Attributes are used at several different places in the .class format. All attributes have the following format:

GenericAttribute_info {
    u2 attribute_name;
    u4 attribute_length;
    u1 info[attribute_length];
}

The attribute_name is a 16-bit index into the class's constant pool; the value of constant_pool[attribute_name] is a string giving the name of the attribute. The field attribute_length gives the length of the subsequent information in bytes. This length does not include the four bytes needed to store attribute_name and attribute_length. In the following text, whenever an attribute is required, names of all the attributes that are currently understood are listed. In the future, more attributes will be added. Class file readers are expected to skip over and ignore the information in any attributes that they do not understand.

The following pseudo-structure gives a top-level description of the format of a class file:

ClassFile {
    u4  magic;
    u2  minor_version
    u2  major_version
    u2  constant_pool_count;
    cp_info         constant_pool[constant_pool_count - 1];
    u2  access_flags;
    u2  this_class;
    u2  super_class;
    u2  interfaces_count;
    u2  interfaces[interfaces_count];
    u2  fields_count;
    field_info      fields[fields_count];
    u2  methods_count;
    method_info     methods[methods_count];
    u2  attributes_count;
    attribute_info  attributes[attribute_count];
}

Here's one of the smaller structures used:

method_info {
    u2  access_flags;
    u2  name_index;
    u2  signature_index;
    u2  attributes_count;
    attribute_info  attributes[attribute_count];
}

Finally, here's a sample of one of the later structures in the .class file description:

Code_attribute {
    u2  attribute_name_index;
    u2  attribute_length;
    u1  max_stack;
    u1  max_locals;
    u2  code_length;
    u1  code[code_length];
    u2  exception_table_length;
    {  u2   start_pc;
       u2   end_pc;
       u2   handler_pc;
       u2   catch_type;
    }  exception_table[exception_table_length];
    u2  attributes_count;
    attribute_info  attributes[attribute_count];
}

None of this is meant to be completely comprehensible (though you might be able to guess at what a lot of the structure members are for), but just suggestive of the sort of structures that live inside .class files. Because the compiler and run-time sources are available, you can always begin with them if you actually have to read or write .class files yourself. Thus, you don't need to have a deep understanding of the details, even in that case.

Method Signatures

Because method signatures are used in .class files, now is an appropriate time to explore them in the detail promised on earlier days—but they're probably most useful to you when writing the native methods of yesterday's lesson.


A signature is a string representing the type of a method, field, or array.

A field signature represents the value of an argument to a method or the value of a variable and is a series of bytes in the following grammar:

    <field signature> := <field_type>
    <field type>      := <base_type> | <object_type> | <array_type>
    <base_type>       := B | C | D | F | I | J | S | Z
    <object_type>     := L <full.ClassName> ;
    <array_type>      := [ <optional_size> <field_type>
    <optional_size>   := [0-9]*

Here are the meanings of the base types: B (byte), C (char), D (double), F (float), I (int), J (long), S (short), and Z (boolean).

A return-type signature represents the return value from a method and is a series of bytes in the following grammar:

    <return signature>    := <field type> | V

The character V (void) indicates that the method returns no value. Otherwise, the signature indicates the type of the return value. An argument signature represents an argument passed to a method:

    <argument signature>  := <field type>

Finally, a method signature represents the arguments that the method expects, and the value that it returns:

    <method_signature>    := (<arguments signature>) <return signature>
    <arguments signature> := <argument signature>*

Let's try out the new rules: a method called complexMethod() in the class my.package.name.ComplexClass takes three arguments—a long, a boolean, and a two-
dimensional array of shorts—and returns this. Then, (JZ[[S)Lmy.package.name.ComplexClass; is its method signature.

A method signature is often prefixed by the name of the method, or by its full package (using an underscore in the place of dots) and its class name followed by a slash / and the name of the method, to form a complete method signature. (You saw several of these generated in stub comments yesterday.) Now, at last, you have the full story! Thus, the following:

my_package_name_ComplexClass/complexMethod(JZ[[S)Lmy.package.name.ComplexClass;

is the full, complete method signature of complexMethod(). (Phew!)

The Garbage Collector

Decades ago, programmers in both the Lisp and the Smalltalk community realized how extremely valuable it is to be able to ignore memory deallocation. They realized that, although allocation is fundamental, deallocation is forced on the programmer by the laziness of the system—it should be able to figure out what is no longer useful, and get rid of it. In relative obscurity, these pioneering programmers developed a whole series of garbage collectors to perform this job, each getting more sophisticated and efficient as the years went by. Finally, now that the mainstream programming community has begun to recognize the value of this automated technique, Java can become the first really widespread application of the technology those pioneers developed.

The Problem

Imagine that you're a programmer in a C-like language (probably not too difficult for you, because these languages are the dominant ones right now). Each time you create something, anything, dynamically in such a language, you are completely responsible for tracking the life of this object throughout your program and mentally deciding when it will be safe to deallocate it. This can be quite a difficult (sometimes impossible) task, because any of the other libraries or methods you've called might have "squirreled away" a pointer to the object, unbeknownst to you. When it becomes impossible to know, you simply choose never to deallocate the object, or at least to wait until every library and method call involved has completed, which could be nearly as long.

The uneasy feeling you get when writing such code is a natural, healthy response to what is inherently an unsafe and unreliable style of programming. If you have tremendous discipline—and so does everyone who writes every library and method you call—you can, in principle, survive this responsibility without too many mishaps. But aren't you human? Aren't they? There must be some small slips in this perfect discipline due to error. What's worse, such errors are virtually undetectable, as anyone who's tried to hunt down a stray pointer problem in C will tell you. What about the thousands of programmers who don't have that sort of discipline?

Another way to ask this question is: Why should any programmers be forced to have this discipline, when it is entirely possible for the system to remove this heavy burden from their shoulders?

Software engineering estimates have recently shown that for every 55 lines of production C-like code in the world, there is one bug. This means that your electric razor has about 80 bugs, and your TV, 400. Soon they will have even more, because the size of this kind of embedded computer software is growing exponentially. When you begin to think of how much C-like code is in your car's engine, it should give you pause.

Many of these errors are due to the misuse of pointers, by misunderstanding or by accident, and to the early, incorrect freeing of allocated objects in memory. Java addresses both of these—the former, by eliminating explicit pointers from the Java language altogether and the latter, by including, in every Java system, a garbage collector that solves the problem.

The Solution

Imagine a run-time system that tracks each object you create, notices when the last reference to it has vanished, and frees the object for you. How could such a thing actually work?

One brute-force approach, tried early in the days of garbage collecting, is to attach a reference counter to every object. When the object is created, the counter is set to 1. Each time a new reference to the object is made, the counter is incremented, and each time such a reference disappears, the counter is decremented. Because all such references are controlled by the language—as variables and assignments, for example—the compiler can tell whenever an object reference might be created or destroyed, just as it does in handling the scoping of local variables, and thus it can assist with this task. The system itself "holds onto" a set of root objects that are considered too important to be freed. The class Object is one example of such a V.I.P. object. (V.I.O.?) Finally, all that's needed is to test, after each decrement, whether the counter has hit 0. If it has, the object is freed.

If you think carefully about this approach, you will soon convince yourself that it is definitely correct when it decides to free anything. It is so simple that you can immediately tell that it will work. The low-level hacker in you might also feel that if it's that simple, it's probably not fast enough to run at the lowest level of the system—and you'd be right.

Think about all the stack frames, local variables, method arguments, return values, and local variables created in the course of even a few hundred milliseconds of a program's life. For each of these tiny, nano-steps in the program, an extra increment—at best—or decrement, test, and deallocation—at worst—will be added to the running time of the program. In fact, the first garbage collectors were slow enough that many predicted they could never be used at all!

Luckily, a whole generation of smart programmers has invented a big bag of tricks to solve these overhead problems. One trick is to introduce special "transient object" areas that don't need to be reference counted. The best of these generational scavenging garbage collectors today can take less than 3 percent of the total time of your program—a remarkable feat if you realize that many other language features, such as loop overheads, can be as large or larger!

There are other problems with garbage collection. If you are constantly freeing and reclaiming space in a program, won't the heap of objects soon become fragmented, with small holes everywhere and no room to create new, large objects? Because the programmer is now free from the chains of manual deallocation, won't they create even more objects than usual?

What's worse, there is another way that this simple reference counting scheme is inefficient, in space rather than time. If a long chain of object references eventually comes full circle, back to the starting object, each object's reference count remains at least 1 forever. None of these objects will ever be freed!

Together, these problems imply that a good garbage collector must, every once in a while, step back to compact or to clean up wasted memory.


Compaction occurs when a garbage collector steps back and reorganizes memory, eliminating the holes created by fragmentation. Compacting memory is simply a matter of repositioning objects one-by-one into a new, compact grouping that places them all in a row, leaving all the free memory in the heap in one big piece.

Cleaning up the circular garbage still lying around after reference counting is called marking and sweeping. A mark-and-sweep of memory involves first marking every root object in the system and then following all the object references inside those objects to new objects to mark, and so on, recursively. Then, when you have no more references to follow, you "sweep away" all the unmarked objects, and compact memory as before.

The good news is that this solves the space problems you were having. The bad news is that when the garbage collector "steps back" and does these operations, a nontrivial amount of time passes during which your program is unable to run—all its objects are being marked, swept, rearranged, and so forth, in what seems like an uninterruptible procedure. Your first hint to a solution is the word "seems."

Garbage collecting can actually be done a little at a time, between or in parallel with normal program execution, thus dividing up the large time needed to "step back" into numerous so-small-you-don't-notice-them chunks of time that happen between the cracks. (Of course, years of smart thinking went into the abstruse algorithms that make all this possible!)

One final problem that might worry you a little has to do with these object references. Aren't these "pointers" scattered throughout your program and not just buried in objects? Even if they're only in objects, don't they have to be changed whenever the object they point to is moved by these procedures? The answer to both of these questions is a resounding yes, and overcoming them is the final hurdle to making an efficient garbage collector.

There are really only two choices. The first, brute force, assumes that all the memory containing object references needs to be searched on a regular basis, and whenever the object references found by this search match objects that have moved, the old reference is changed. This assumes that there are "hard" pointers in the heap's memory—ones that point directly to other objects. By introducing various kinds of "soft" pointers, including pointers that are like forwarding addresses, the algorithm improves greatly. Although these brute-force approaches sound slow, it turns out that modern computers can do them fast enough to be useful.


Note: You might wonder how the brute-force techniques identify object references. In early systems, references were specially tagged with a "pointer bit," so they could be unambiguously located. Now, so-called conservative garbage collectors simply assume that if it looks like an object reference, it is—at least for the purposes of the mark and sweep. Later, when actually trying to update it, they can find out whether it really is an object reference or not.

The final approach to handling object references, and the one Java currently uses, is also one of the very first ones tried. It involves using 100 percent "soft" pointers. An object reference is actually a handle, sometimes call an "OOP," to the real pointer, and a large object table exists to map these handles into the actual object reference. Although this does introduce extra overhead on almost every object reference (some of which can be eliminated by clever tricks, as you might guess), it's not too high a price to pay for this incredibly valuable level of indirection.

This indirection allows the garbage collector, for example, to mark, sweep, move, or examine one object at a time. Each object can be independently moved "out from under" a running Java program by changing only the object table entries. This not only allows the "step back" phase to happen in the tiniest steps, but it makes a garbage collector that runs literally in parallel with your program much easier to write. This is what the Java garbage collector does.


Warning: You need to be very careful about garbage collection when you're doing critical, real-time programs (such as those mentioned yesterday that legitimately require native methods)—but how often will your Java code be flying a commercial airliner in real-time, anyway?

Java's Parallel Garbage Collector

Java applies almost all these advanced techniques to give you a fast, efficient, parallel garbage collector. Running in a separate thread, it cleans up the Java environment of almost all trash (it is conservative), silently and in the background, is efficient in both space and time, and never steps back for more than a small amount of time. You should never need to know it's there.

By the way, if you want to force a full mark-and-sweep garbage collection to happen soon, you can do so simply by calling the System.gc() method. You might want to do this if you just freed up a majority of the heap's memory in circular garbage, and want it all taken away quickly. You might also call this whenever you're idle, as a hint to the system about when it would be best to come and collect the garbage. This "meta knowledge" is rarely needed by the system, however.

Ideally, you'll never notice the garbage collector, and all those decades of programmers beating their brains out on your behalf will simply let you sleep better at night—and what's wrong with that?

The Security Story

Speaking of sleeping well at night, if you haven't stepped back yet and said, "My Goodness! You mean Java programs will be running rampant on the Internet!?!" you better do so now, for it is a legitimate concern. In fact, it is one of the major technical stumbling blocks (the others being mostly social and economic) to achieving the dream of ubiquity and code sharing mentioned earlier in today's lesson.

Why You Should Worry

Any powerful, flexible technology can be abused. As the Net becomes mainstream and widespread, it, too, will be abused. Already, there have been many blips on the security radar screens of those of us who worry about such things, warning that (at least until today), not enough attention has been paid by the computer industry (or the media) to solving some of the problems that this new world brings with it. One of the benefits of constructively solving security once and for all will be a flowering unseen before in the virtual communities of the Net; whole new economies based on people's attention and creativity will spring to life, rapidly transforming our world in new and positive ways.

The downside to all this new technology, is that we (or someone!) must worry long and hard about how to make the playgrounds of the future safe for our children, and for us. Fortunately, Java is a big part of the answer.

Why You Might Not Have To

What gives me any confidence that the Java language and environment will be safe, that it will solve the technically daunting and extremely thorny problems inherent in any good form of security, especially for networks?

One simple reason is the history of the people, and the company, that created Java. Many of them are the very smart programmers referred to throughout the book, who helped pioneer many of the ideas that make Java great and who have worked hard over the decades to make techniques such as garbage collection a mainstream reality. They are technically capable of tackling and solving the hard problems that need to be solved. In particular, from discussions with Chuck McManis, one of Java's security gurus, I have confidence that he has thought through these hard problems deeply, and that he knows what needs to be done.

Sun Microsystems, the company, has been pushing networks as the central theme of all its software for more than a decade. Sun has the engineers and the commitment needed to solve these hard problems, because these same problems are at the very center of both its future business and its vision of the future, in which networking is the center of everything—and global networks are nearly useless without good security. Just this year, Sun has advanced the state of the art in easy-to-use Internet security with its new SunScreen products, and it has assigned Whitfield Diffie to oversee them, who is the man who discovered the underlying ideas on which essentially all interesting forms of modern encryption are based.

Enough on "deep background." What does the Java environment provide right now that helps me feel secure?

Java's Security Model

Java protects you against potential "nasty" Java code via a series of interlocking defenses that, together, form an imposing barrier to any and all such attacks.


Caution: Of course, no one can protect you from your own ignorance or carelessness. If you're the kind of person who blindly downloads binary executables from your Internet browser and runs them, you need read no further! You are already in more danger than Java will ever pose.

As a user of this powerful new medium, the Internet, you should educate yourself to the possible threats this new and exciting world entails. In particular, downloading "auto running macros" or reading e-mail with "executable attachments" is just as much a threat as downloading binaries from the Net and running them.

Java does not introduce any new dangers here, but by being the first mainstream use of executable and mobile code on the Net, it is responsible for making people suddenly aware of the dangers that have always been there. Java is already, as you will soon see, much less dangerous than any of these common activities on the Net, and can be made safer still over time. Most of these other (dangerous) activities can never be made safe. So please, do not do them!

A good rule of thumb on the Net is: Don't download anything that you plan to execute (or that will be automatically executed for you) except from someone (or some company) you know well and with whom you've had positive, personal experience. If you don't care about losing all the data on your hard drive, or about your privacy, you can do anything you like, but for most of us, this rule should be law.

Fortunately, Java allows you to relax that law. You can run Java applets from anyone, anywhere, in relative safety.

Java's powerful security mechanisms act at four different levels of the system architecture. First, the Java language itself was designed to be safe, and the Java compiler ensures that source code doesn't violate these safety rules. Second, all bytecodes executed by the run-time are screened to be sure that they also obey these rules. (This layer guards against having an altered compiler produce code that violates the safety rules.) Third, the class loader ensures that classes don't violate name space or access restrictions when they are loaded into the system. Finally, API-specific security prevents applets from doing destructive things. This final layer depends on the security and integrity guarantees from the other three layers.

Let's now examine each of these layers in turn.

The Language and the Compiler

The Java language and its compiler are the first line of defense. Java was designed to be a safe language.

Most other C-like languages have facilities to control access to "objects," but also have ways to "forge" access to objects (or to parts of objects), usually by (mis-)using pointers. This introduces two fatal security flaws to any system built on these languages. One is that no object can protect itself from outside modification, duplication, or "spoofing" (others pretending to be that object). Another is that a language with powerful pointers is more likely to have serious bugs that compromise security. These pointer bugs, where a "runaway pointer" starts modifying some other object's memory, were responsible for most of the public (and not-so-public) security problems on the Internet this past decade.

Java eliminates these threats in one stroke by eliminating pointers from the language altogether. There are still pointers of a kind—object references—but these are carefully controlled to be safe: they are unforgeable, and all casts are checked for legality before being allowed. In addition, powerful new array facilities in Java not only help to offset the loss of pointers, but add additional safety by strictly enforcing array bounds, catching more bugs for the programmer (bugs that, in other languages, might lead to unexpected and, thus, bad-guy-exploitable problems).

The language definition, and the compilers that enforce it, create a powerful barrier to any "nasty" Java programmer.

Because an overwhelming majority of the "net-savvy" software on the Internet may soon be Java, its safe language definition and compilers help to guarantee that most of this software has a solid, secure base. With fewer bugs, Net software will be more predictable—a property that thwarts attacks.

Verifying the Bytecodes

What if that "nasty" programmer gets a little more determined, and rewrites the Java compiler to suit his nefarious purposes? The Java run-time, getting the lion's share of its bytecodes from the Net, can never tell whether those bytecodes were generated by a "trustworthy" compiler. Therefore, it must verify that they meet all the safety requirements.

Before running any bytecodes, the run-time subjects them to a rigorous series of tests that vary in complexity from simple format checks all the way to running a theorem prover, to make certain that they are playing by the rules. These tests verify that the bytecodes do not forge pointers, violate access restrictions, access objects as other than what they are (InputStreams are always used as InputStreams, and never as anything else), call methods with inappropriate argument values or types, nor overflow the stack.

Consider the following Java code sample:

public class VectorTest {
    public int  array[];
    public int  sum() {
        int[]  localArray = array;
        int    sum        = 0;
        for (int  i = localArray.length;  —i >= 0;  )
            sum += localArray[i];
        return sum;
    }
}

The bytecodes generated when this code is compiled look something like the following:

aload_0

Load this

getfield #10

Load this.array

astore_1

Store in localArray

iconst_0

Load 0

istore_2

Store in sum

aload_1

Load localArray

arraylength

Gets its length

istore_3

Store in i

A: iinc 3 -1

Subtract 1 from i

iload_3

Load i

iflt B

Exit loop if < 0

iload_2

Load sum

aload_1

Load localArray

iload_3

Load i

iaload

Load localArray[i]

iadd

Add sum

istore_2

Store in sum

goto A

Do it again

B: iload_2

Load sum

ireturn

Return it


Note: The excellent examples and descriptions in this section of the book are paraphrased from the tremendously informative security paper in the (alpha) Java release. I'd encourage you to read whatever the latest version of this document is in newer releases, if you want to follow the ongoing Java security story.

Extra Type Information and Requirements

Java bytecodes encode more type information than is strictly necessary for the interpreter. Even though, for example, the aload and iload opcodes do exactly the same thing, aload is always used to load an object reference and iload used to load an integer. Some bytecodes (such as getfield) include a symbol table reference—and that symbol table has even more type information. This extra type information allows the run-time system to guarantee that Java objects and data aren't illegally manipulated.

Conceptually, before and after each bytecode is executed, every slot in the stack and every local variable has some type. This collection of type information—all the slots and local variables—is called the type state of the execution environment. An important requirement of the Java type state is that it must be determinable statically by induction—that is, before any program code is executed. As a result, as the run-time systems reads bytecodes, each is required to have the following inductive property: given only the type state before the execution of the bytecode, the type state afterward must be fully determined.

Given "straight-line" bytecodes (no branches), and starting with a known stack state, the state of each slot in the stack is therefore always known. For example, starting with an empty stack:

iload_1

Load integer variable. Stack type state is I.

iconst 5

Load integer constant. Stack type state is II.

iadd

Add two integers, producing an integer. Stack type state is I.


Note: Smalltalk and PostScript bytecodes do not have this restriction. Their more dynamic type behavior does create additional flexibility in those systems, but Java needs to provide a secure execution environment. It must therefore know all types at all times, in order to guarantee a certain level of security.

Another requirement made by the Java run-time is that when a set of bytecodes can take more than one path to arrive at the same point, all such paths must arrive there with exactly the same type state. This is a strict requirement, and implies, for example, that compilers cannot generate bytecodes that load all the elements of an array onto the stack. (Because each time through such a loop the stack's type state changes, the start of the loop—"the same point" in multiple paths—would have more than one type state, which is not allowed).

The Verifier

Bytecodes are checked for compliance with all these requirements, using the extra type information in a .class file, by a part of the run-time called the verifier. It examines each bytecode in turn, constructing the full type state as it goes, and verifies that all the types of parameters, arguments, and results are correct. Thus, the verifier acts as a gatekeeper to your run-time environment, letting in only those bytecodes that pass muster.


Warning: The verifier is the crucial piece of Java's security, and it depends on your having a correctly implemented (no bugs, intentional or otherwise) run-time system. As of this writing, only Sun is producing Java run-times, and theirs are secure. In the future, however, you should be careful when downloading or buying another company's (or individual's) version of the Java run-time environment. Eventually, Sun will implement validation suites for run-times, compilers, and so forth to be sure that they are safe and correct. In the meantime, caveat emptor! Your run-time is the base on which all the rest of Java's security is built, so make sure it is a good, solid, secure base.

When bytecodes have passed the verifier, they are guaranteed not to: cause any operand stack under- or overflows; use parameter, argument, or return types incorrectly; illegally convert data from one type to another (from an integer to a pointer, for example); nor access any object's fields illegally (that is, the verifier checks that the rules for public, private, package, and protected are obeyed).

As an added bonus, because the interpreter can now count on all these facts being true, it can run much faster than before. All the required checks for safety have been done up front, so it can run at full throttle. In addition, object references can now be treated as capabilities, because they are unforgeable—capabilities allow, for example, advanced security models for file I/O and authentication to be safely built on top of Java.


Note: Because you can now trust that a private variable really is private, and that no bytecode can perform some magic with casts to extract information from it (such as your credit card number), many of the security problems that might arise in other, less safe environments simply vanish! These guarantees also make erecting barriers against destructive applets possible, and easier. Because the Java system doesn't have to worry about "nasty" bytecodes, it can get on with creating the other levels of security it wants to provide to you.

The Class Loader

The class loader is another kind of gatekeeper, albeit a higher-level one. The verifier was the security of last resort. The class loader is the security of first resort.

When a new class is loaded into the system, it is placed into (lives in) one of several different "realms." In the current release, there are three possible realms: your local computer, the firewall-guarded local network on which your computer is located, and the Internet (the global Net). Each of these realms is treated differently by the class loader.


Note: Actually, there can be as many realms as your desired level of security (or paranoia) requires. This is because the class loader is under your control. As a programmer, you can make your own class loader that implements your own peculiar brand of security. (This is a radical step: you may have to give the users of your program a whole bunch of classes—and they give you a whole lot of trust—to accomplish this.)

As a user, you can tell your Java-capable browser, or Java system, what realm of security (of the three) you'd like it to implement for you right now, or from now on.

As a system administrator, Java has global security policies that you can set up to help guide your users to not "give away the store" (that is, set all their preferences to be unrestricted, promiscuous, "hurt me please!").

In particular, the class loader never allows a class from a "less protected" realm to replace a class from a more protected realm. The file system's I/O primitives, about which you should be very worried (and rightly so), are all defined in a local Java class, which means that they all live in the local-computer realm. Thus, no class from outside your computer (from either the supposedly trustworthy local network or from the Internet) can take the place of these classes and "spoof" Java code into using "nasty" versions of these primitives. In addition, classes in one realm cannot call upon the methods of classes in other realms, unless those classes have explicitly declared those methods public. This implies that classes from other than your local computer cannot even see the file system I/O methods, much less call them, unless you or the system wants them to.

In addition, every new applet loaded from the network is placed into a separate package-like namespace. This means that applets are protected even from each other! No applet can access another's methods (or variables) without its cooperation. Applets from inside the firewall can even be treated differently from those outside the firewall, if you like.


Note: Actually, it's all a little more complex than this. In the current release, an applet is in a package "namespace" along with any other applets from that source. This source, or origin, is most often a host (domain name) on the Internet. This special "subrealm" is used extensively in the next section. Depending on where the source is located, outside the firewall (or inside), further restrictions may apply (or be removed entirely). This model is likely to be extended in future releases of Java, providing an even finer degree of control over which classes get to do what.

The class loader essentially partitions the world of Java classes into small, protected little groups, about which you can safely make assumptions that will always be true. This type of predictability is the key to well-behaved and secure programs.

You've now seen the full lifetime of a method. It starts as source code on some computer, is compiled into bytecodes on some (possibly different) computer, and can then travel (as a .class file) into any file system or network anywhere in the world. When you run an applet in a Java-capable browser (or download a class and run it by hand using java), the method's bytecodes are extracted from its .class file and carefully looked over by the verifier. Once they are declared safe, the interpreter can execute them for you (or a code generator can generate native binary code for them using either the "just-in-time" compiler or java2c, and then run that native code directly).

At each stage, more and more security is added. The final level of that security is the Java class library itself, which has several carefully designed classes and APIs that add the final touches to the security of the system.

The Security Manager

SecurityManager is an abstract class that was recently added to the Java system to collect, in one place, all the security policy decisions that the system has to make as bytecodes run. You learned before that you can create your own class loader. In fact, you may not have to, because you can subclass SecurityManager to perform most of the same customizations.

An instance of some subclass of SecurityManager is always installed as the current security manager. It has complete control over which of a well-defined set of "dangerous" methods are allowed to be called by any given class. It takes the realms from the last section into account, the source (origin) of the class, and the type of the class (stand-alone, or loaded by an applet). Each of these can be separately configured to have the effect you (the programmer) like on your Java system. For nonprogrammers, the system provides several levels of default security policies from which you can choose.

What is this "well-defined set" of methods that are protected?

File I/O is a part of the set, for obvious reasons. Applets, by default, can open, read, or write files only with the express permission of the user—and even then, only in certain restricted directories. (Of course, users can always be stupid about this, but that's what system administrators are for!)

Also in this protected set are the methods that create and use network connections, both incoming and outgoing.

The final members of the set are those methods that allow one thread to access, control, and manipulate other threads. (Of course, additional methods can be protected as well, by creating a new subclass of SecurityManager that handles them.)

For both file and network access, the user of a Java-capable browser can choose between three realms (and one subrealm) of protection:

For file access, the source subrealm is not meaningful, so it really has only three realms of protection. (As a programmer, of course, you have full access to the security manager and can set up your own peculiar criteria for granting and revoking privileges to your heart's content.)

For network access, you can imagine wanting many more realms. For example, you might specify different groups of trusted domains (companies), each of which is allowed added privileges when applets from that group are loaded. Some groups can be more trusted than others, and you might even allow groups to grow automatically by allowing existing members to recommend new members for admission. (The Java seal of approval?)

In any case, the possibilities are endless, as long as there is a secure way of recognizing the original creator of an applet.

You might think this problem has already been solved, because classes are tagged with their origin. In fact, the Java run-time goes far out of its way to be sure that that origin information is never lost—any executing method can be dynamically restricted by this information anywhere in the call chain. So why isn't this enough?

Because what you'd really like to be able to do is permanently "tag" an applet with its original creator (its true origin), and no matter where it has traveled, a browser could verify the integrity and authenticate the creator of that applet. Just because you don't know the company or individual that operates a particular server machine doesn't mean that you want to mistrust every applet stored on that machine. It's just that, currently, to be really safe, you should mistrust those applets.

If somehow those applets were irrevocably tagged with a digital signature by their creator, and that signature could also guarantee that the applet had not been tampered with, you'd be golden.


Note: Luckily, Sun is planning to do exactly that for Java, as soon as export restrictions can be resolved.

Here's a helpful hint of where the team would like to go, from the security documentation: "...a mechanism exists whereby public keys and cryptographic message digests can be securely attached to code fragments that not only identify who originated the code, but guarantee its integrity as well. This latter mechanism will be implemented in future releases."

Look for these sorts of features in every release of Java; they will be a key part of the future of the Internet!

One final note about security. Despite the best efforts of the Java team, there is always a trade-off between useful functionality and absolute security. For example, Java applets can create windows, an extremely useful capability, but a "nasty" applet could use this to spoof the user into typing private password information, by showing a familiar program (or operating system) window and then asking an expected, legitimate-looking question in it. (The beta release adds a special banner to applet-created windows to solve this problem.)

Flexibility and security can't both be maximized. Thus far on the Net, people have chosen maximum flexibility, and have lived with the minimal security the Net now provides. Let's hope that Java can help tip the scales a bit, enabling much better security, while sacrificing only a minimal amount of the flexibility that has drawn so many to the Net.

Summary

Today, you learned about the grand vision that some of us have for Java, and about the exciting future it promises.

Under the hood, the inner workings of the virtual machine, the bytecode interpreter (and all its bytecodes), the garbage collector, the class loader, the verifier, the security manager, and the powerful security features of Java were all revealed.

You now know almost enough to write a Java run-time environment of your own—but luckily, you don't have to. You can simply download the latest release of Java—or use a Java-capable browser to enjoy most of the benefits of Java right away.

I hope that Java ends up opening new roads in your mind, as it has in mine.

Q&A

Q: I'm still a little unclear about why the Java language and compiler make the Net safer. Can't they just be "side-stepped" by nasty bytecodes?

A: Yes, they can—but don't forget that the whole point of using a safe language and compiler was to make the Net as a whole safer as more Java code is written. An overwhelming majority of this Java code will be written by "honest" Java programmers, who will produce safe bytecodes. This makes the Net more predictable over time, and thus more secure.

Q: I know you said that garbage collection is something I don't have to worry about, but what if I want (or need) to?

A: So, you are planning to fly a plane with Java. Cool! For just such cases, there is a way to ask the Java run-time, during startup (java -noasyncgc), not to run garbage collection unless forced to, either by an explicit call (System.gc()) or by running out of memory. (This can be quite useful if you have multiple threads that are messing each other up and want to "get the gc thread out of the way" while testing them.) Don't forget that turning garbage collection off means that any object you create will live a long, long time. If you're real-time, you never want to "step back" for a full gc—so be sure to reuse objects often, and don't create too many of them!

Q: I like the control above; is there anything else I can do to the garbage collector?

A: You can also force the finalize() methods of any recently freed objects to be called immediately via System.runFinalization(). You might want to do this if you're about to ask for some resources that you suspect might still be tied up by objects that are "gone but not forgotten" (waiting for finalize()). This is even rarer than starting a gc by hand, but it's mentioned here for completeness.

Q: What's the last word on Java?

A: Java adds much more than it can ever take away. It has always done so for me, and now, I hope it will for you, as well.

The future of the Net is filled with as-yet-undreamt horizons, and the road is long and hard, but Java is a great traveling companion.

Previous Page TOC Index Next Page Home