This chapter reveals the inner workings of Java. Youll find out all about Javas vision, Javas virtual machine, those bytecodes youve heard so much about, that mysterious garbage collector, and why you might worry about security but dont have to.
Lets begin, however, with 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. Theyve 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 marshaled behind a similar goal and given the tools to help achieve it. The Java language, with its four Ss (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 Suns 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. Lets 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 the final release of the API. 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.
One of the reasons this brilliant move on Suns 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. Its 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 its 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 center stage.
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 Javas 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 tastewhenever the need arisesby 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 through 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 dreamJava.
To make visions like this possible, Java must be ubiquitous. It must be able to run on any computer and any operating systemnow, 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 securityand how to maintain itand a precise format for how this stream of bytecodes can be sent from one virtual machine to another. (For more on security issues, see Chapter 40, Java Security.)
Each of these requirements is addressed in this chapter.
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 in this chapter.
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 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 chipbut all these logical, abstract components of the virtual machine must be supplied in some form in every Java system.
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 whats doing it.
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 Suns 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 in this chapter 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.
Bytecodes interpret data in the run-time memory areas as belonging to a fixed set of types: the primitives types youve 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 primitives 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. (Youll see all this in gruesome detail later.)
The registers of the Java virtual machine are just like the registers inside a real computer.
NOTE |
---|
Registers hold the machines state, affect its operation, and are updated after each byte-code is executed. |
The following are the Java registers:
The virtual machine defines these registers to be 32 bits wide.
The Java virtual machine is stack-based. A Java stack frame is similar to the stack frame of a conventional programming languageit holds the state for a single method call. Frames for nested method calls are stacked on top of this frame.
NOTE |
---|
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 through an index N, take up the (32-bit) slots N and N + 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 stacks 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 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. Youll learn more about garbage collection later.
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 might want to associate with each methods 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.)
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 through specially coded bytes, and have a precisely defined format when they appear in the .class file for a class. Later in this chapter, a little of this file format is covered, but everything is fully specified by the virtual machine specifications in your Java release.
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 in this chapter).
These limitations and their implications are
One of the main tasks of the virtual machine is the fast, efficient execution of the Java bytecodes in methods. Unlike in a discussion 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.
A bytecode interpreter examines each opcode byte (bytecode) in a methods 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, theyve 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), but what if a smart implementor wants to do better?
About a decade ago, a really clever trick was discovered by L. 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 youve just writtenin C, for examplealready 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 it92's really only the back end of a traditional compilerthe part that does code generation. By the way, the front end here is javac. |
Heres 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. |
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 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 youre 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. Youd link the Java run-time environment into that code so that your server remains a fully capable Java program, but its 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 Suns virtual machine does not perform just-in-time compilation. Both of these have been promised in a later release (perhaps 1.1). |
Lets 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 operandsvalue1 and value2to 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 fromand the result returned tothe stack.
Bytecodes that dont 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. |
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.
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. 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.
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.
Wide -no change-
Precedes iload, lload, fload, dload, aload, istore, lstore, fstore, dstore, astore, or iinc. byte1 and byte 2 of the following bytecode form an unsigned 16-bit index that replaces the 8-bit index of the bytecode following.
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.
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 postive 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 Its 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 youve 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 youve seen in previous store bytecodes.
nop -no change-
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.
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 youre 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. |
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.
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.
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- ret_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 Javas finally keyword. |
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. Javas 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. |
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-lowth 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.
putfield ..., handle, value => ... putfield ..., handle, value-word1,value-word2 => ...
Set field in object. byte1 and byte2 are used to construct and 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 fields width and offset both in bytes.
The field at that offset from the start of the object referenced 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 fields 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.
invokevirtual ..., handle, [arg1, [arg2 ...]], ... => ...
Invoke instance method based on run-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. A pointer to the objects 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 exactly.
The result of the lookup is an index into the method table of the named class that is used to look in the method table of the objects 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 the method table of the class indicated. The method signature is guaranteed to match one of the method signatures in the classs 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 objects 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.)
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.
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 must 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.
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.)
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 bytecodes operands (if any) are not overwritten, and the original bytecode is restored when the breakpoint bytecode is removed.
The following discussion, straight out of the Java virtual machine documentation, shows you an example of the cleverness mentioned earlier thats 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 arent 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 youd like to go back in this chapter 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 for arrays (to do fast lookups in java.lang.Object).
You wont be given the entire .class file format here, only a taste of what its like. (You can read all about it in the release documentation.) Its 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 bytecodes are expected to travel across arbitrary networksto and from arbitrary computers and operating systemsand 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. (Use java.io.DataInput and java.io.DataOutput to read and write .class files.)
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.
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 arrays 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 classs 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]; }
Heres 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, heres 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 dont need to have a deep understanding of the details, even in that case.
Because methods signatures are used in .class files, now is an appropriate time to explore them in detailbut theyre probably most useful to you when writing the native methods.
NOTE |
---|
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>*
Now, lets try out the new rules: a method called complexMethod() in the class my.package.name.ComplexClass takes three argumentsa long, a boolean, and a two-dimensional array of shortsand 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. 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!)
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 systemit 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.
Imagine that youre 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 youve 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 disciplineand so does everyone who writes every library and method you callyou can, in principle, survive this responsibility without too many mishaps. But arent you human? Arent they? There must be some small slips in this perfect discipline due to error. Whats worse, such errors are virtually undetectable, as anyone whos tried to hunt down a stray pointer problem in C will tell you. What about the thousands of programmers who dont 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 cars 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 thesethe 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.
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 languageas variables and assignments, for examplethe 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 thats 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 its that simple, its probably not fast enough to run at the lowest level of the systemand youd 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 programs life. For each of these tiny, nano-steps in the program, an extra incrementat bestor decrement, test, and deallocationat worstwill 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 dont 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 programa 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, wont 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, wont they create even more objects than usual?
Whats 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 objects 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.
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 runall 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-dont-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. Arent these pointers scattered throughout your program and not just buried in objects? Even if theyre only in objects, dont 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 heaps memoryones 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 isat 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 called 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), its 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 youre doing critical, real-time programs (such as those mentioned in Chapter 37 that legitimately require native methods)but how often will your Java code be flying a commercial airliner in real-time, anyway? |
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 its 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 heaps memory in circular garbage, and want it all taken away quickly. You might also call this whenever youre 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, youll 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 nightand whats wrong with that?
Speaking of sleeping well at night, if you havent 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 this chapter.
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 constructively solving some of the problems that this new world brings with it. One of the benefits of solving security once and for all will be a flowering unseen before in the virtual communities of the Net; whole new economies based on peoples 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.
NOTE |
---|
For more information about security issues with Java, please see Chapter 40, Java Security. |
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 Javas 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 everythingand 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 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 youre 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 this: Dont 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 youve had positive, personal experience. If you dont 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. |
Javas 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 doesnt 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 dont 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.
Lets now examine each of these layers in turn.
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 objects 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 kindobject referencesbut 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 predictablea property that thwarts attacks.
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 lions 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, or 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. Id 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. Also, check out the next chapter for more information. |
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 referenceand that symbol table has even more type information. This extra type information allows the run-time system to guarantee that Java objects and data arent 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 informationall the slots and local variablesis called the type state of the execution environment. An important requirement of the Java type state is that it must be determinable statically by inductionthat is, before any program code is executed. As a result, as the run-time systems read 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 stacks type state changes, the start of the loopthe same point in multiple pathswould have more than one type state, which is not allowed.)
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 Javas 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 its are secure. In the future, however, you should be careful when downloading or buying another companys (or individuals) 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 Javas 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 objects 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 unforgeablecapabilities 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 doesnt 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 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 classesand they give you a whole lot of trustto accomplish this.) As a user, you can tell your Java-capable browser, or Java system, what realm of security (of the three) youd 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 systems 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 name space. This means that applets are protected even from each other! No applet can access anothers 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, its 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.
Youve 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 methods 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.
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 userand even then, only in certain restricted directories. (Of course, users can always be stupid about this, but thats 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 hearts 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 lostany executing method can be dynamically restricted by this information anywhere in the call chain. So why isnt this enough?
Because what youd 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 dont know the company or individual that operates a particular server machine doesnt mean that you want to mistrust every applet stored on that machine. Its 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, youd be golden.
NOTE |
---|
Luckily, Sun is planning to do exactly that for Java, as soon as export restrictions can be resolved. Heres 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 1.0 release adds a special banner to applet-created windows to solve this problem.)
Flexibility and security cant both be maximized. Thus far on the Net, people have chosen maximum flexibility, and have lived with the minimal security the Net now provides. Lets 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.
In this chapter we discussed 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 ownbut luckily, you dont have to. You can simply download the latest release of Javaor 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.