The Working Developer's Guide to Java Bytecode

Java Development News:

The Working Developer's Guide to Java Bytecode

By Ted Neward

01 May 2007 | TheServerSide.com

Introduction

In the last year or two, interest in "alternative" languages on top of the Java Virtual Machine has grown exponentially, to the point where three languages were blessed with official acceptance in the Java Community Process: JRuby (a port of the Ruby programming language), Groovy (a Java-like language with more dynamic features), and Bean-Shell (a more scripting-oriented shell- like language). In addition, the Java6 (code-named "Mustang") release includes an API layer designed to encapsulate the details of various scripting engines away in the same manner JDBC does for database access. Plus, we have all the changes that took place in the Java5 version of the language itself. In short, the Java platform programming language landscape has changed pretty significantly. One thing that hasn't changed, however, is the basic fact that all of these languages, despite all their interesting features or capabilities, eventually end up in the lingua franca of the Java Virtual Machine, the JVM bytecode set. In this article, we're going to examine the JVM bytecode set, disassemble some code to see how it works, and play with some tools that allow us to manipulate bytecode directly.

It may seem counterintuitive to learn the JVM bytecode format; after all, it's not like anyone (not even me) is going to suggest that you write code in bytecode directly, right? That's what high-level languages are for, to allow us to think in more general terms and let tools (like compilers) worry about the precise details. But it's always a good thing to know what the compiler produces: for example, you've probably heard the debates surrounding the use of StringBuffer instead of doing straight String concatenation, or the idea that the compiler will generate code on your behalf in certain situations (such as creating a default constructor for a class if you haven't specified a constructor), but when's the last time you verified those assumptions? Or wondered how inner classes work to ensure that the inner class has access to the private variables of the outer class, without breaking encapsulation for any potential accessor? By looking at JVM bytecode (which, by the way, is probably the simplest "assembly language" you'll ever see in your lifetime), you can verify crucial assumptions and/or debunk persistent myths. Not to mention give you a tool for ensuring that classes running in production are the ones that are supposed to be there (because, of course, deployments into production always go the way they're supposed to, right?).

All of this completely ignores the advantages of using bytecode directly in other scenarios. For example, the java.lang.instrumentation package provides APIs for doing load-time instrumentation of classes (what others might call load-time aspect-oriented weaving of code), but to do it effectively, you need to know the bytecode set so you can know where and what to inject. And sometimes you actually need to reverseengineer obfuscated vendor code in order to fix a bug at the 11th hour before you can ship. And so on, and so on, and so on. So hop on the Java bytecode bandwagon with me for a bit and let's run through the core parts of the JVM bytecode set that you'll see. Hopefully, in the process, you'll discover some things you may not have known about Java and the JVM that will be of use in more practical scenarios.

Disassembling Java 101

While it's always possible to begin our discussion of Java bytecode by simply asserting some facts ("Java bytecode is stored in a tree format"), given that most of the audience is already familiar with Java, it's easier to start by going the opposite direction: we'll take working Java-compiled code, and disassemble it. That way, we've got a working framework (that is, your understanding of the Java language) from which to hang various points about the Java bytecode format. We begin with every Java programmer's first program, the ubiquitous traditional "Hello World" app.

ppublic class HelloWorld
{
 public static void main(String[] args)
 {
  System.out.println("Hello, world!");
 }
}

Our guide to Java bytecode comes in two forms. The first is a JDK tool that even long-time Java developers have never seen before, javap. javap is a bytecode disassembler, meaning it takes compiled .class files and dumps the file's structure to the console, including the bytecode making up the methods contained in that file. Use it against a class on the CLASSPATH like so:

$ javap -verbose -c -private HelloWorld
Compiled from "HelloWorld.java"
public class HelloWorld extends java.lang.Object
 SourceFile: "HelloWorld.java"
 minor version: 0
 major version: 50
 Constant pool:
const #1 = Method #6.#15; // java/lang/Object."<init>":()V
const #2 = Field #16.#17; // java/lang/System.out:Ljava/io/PrintStream;
const #3 = String #18; // Hello, world!
const #4 = Method #19.#20; // java/io/PrintStream.println:(Ljava/lang/String;)V
const #5 = class #21; // HelloWorld
const #6 = class #22; // java/lang/Object
const #7 = Asciz <init>;
const #8 = Asciz ()V;
const #9 = Asciz Code;
const #10 = Asciz LineNumberTable;
const #11 = Asciz main;
const #12 = Asciz ([Ljava/lang/String;)V;
const #13 = Asciz SourceFile;
const #14 = Asciz HelloWorld.java;
const #15 = NameAndType #7:#8;// "<init>":()V
const #16 = class #23; // java/lang/System
const #17 = NameAndType #24:#25;// out:Ljava/io/PrintStream;
const #18 = Asciz Hello, world!;
const #19 = class #26; // java/io/PrintStream
const #20 = NameAndType #27:#28;// println:(Ljava/lang/String;)V
const #21 = Asciz HelloWorld;
const #22 = Asciz java/lang/Object;
const #23 = Asciz java/lang/System;
const #24 = Asciz out;
const #25 = Asciz Ljava/io/PrintStream;;
const #26 = Asciz java/io/PrintStream;
const #27 = Asciz println;
const #28 = Asciz (Ljava/lang/String;)V;

{
public HelloWorld();
 Code:
  Stack=1, Locals=1, Args_size=1
  0: aload_0
  1: invokespecial #1; //Method java/lang/Object."<init>":()V
  4: return

 LineNumberTable:
  line 1: 0

public static void main(java.lang.String[]);
 Code:
  Stack=2, Locals=1, Args_size=1
  0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
  3: ldc #3; //String Hello, world!
  5: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
  8: return
 LineNumberTable:
  line 5: 0
  line 6: 8
}

[The use of the term "class file" is a bit incorrect here, since technically the Java Virtual Machine doesn't care where this stream of binary bits came from, but we refer to the binary format of Java classes like this as "class files" for historical reasons, since the original JDK 1.0 release demanded that they be in independent ".class" files in the directory structure(s) pointed to by the CLASSPATH environment variable. Thus, "class file" should be interpreted to mean "the binary format defined by the Java Virtual Machine
Specification, Second Edition".]

The various flags tell javap to provide more information than its default, scanty, output; "-c" performs method bytecode display, "-private" displays all members regardless of accessibility, and "-verbose" shows us the constant pool of the class, among other things. Examining the disassembled contents of HelloWorld is interesting; in particular, we can verify a few assumptions about the Java compiler right off. For example, it's been well-known for years that if a class doesn't explicitly extend a base class, it automatically extends java.lang.Object, which javap verifies in the first line of output of HelloWorld. Secondly, it's also been well-known that a class must have a constructor, and that if a constructor isn't explicitly written, the compiler inserts a default, no-argument constructor, and again, javap verifies this. (Constructors, at the JVM level, are regular methods that are known by the well-known name "<init>".)

The most obvious factor in the javap output (when "-verbose" is passed in, anyway) is the constant pool output. Every class file has a constant pool in which all constants–particularly strings, be they constant strings in code, class names, method names, or even field names–are stored in a central location in the class, and referenced via indeces into this pool. Normally, this particular detail will be handled by tools– this is why javap displays the constant's value in a comment at the end of the line, for example–but it's still important to realize this constant pool is in place, as it eases understanding the disassembled code. So, for example, we can see that the operation in line 5 of the "main" method invokes a method whose name is stored in slot #4 in the constant pool (which in turn is a method constant consisting of class name (slot #19) and method name (slot #20), which ultimately resolves to "java.io.PrintStream.println(String[])"). For full details of all the different types of constants, as well as the formal description of the .class file format, see the Java Virtual Machine Specification. [The JVM Specification can be purchased from Amazon, or is available in both PDF and HTML formats from the Java website, at "http://java.sun.com/docs/books/vmspec".] (In point of fact, the JVM Specification is going to be the canonical reference for this whole article, so simply assume that as a blanket statement.) The meat of what we want to examine is in the method blocks, and to begin, we're going to examine the autogenerated HelloWorld constructor:

public HelloWorld();
 Code:
  Stack=1, Locals=1, Args_size=1
  0: aload_0
  1: invokespecial #1; //Method java/lang/Object."<init>":()V
  4: return
 LineNumberTable:
  line 1: 0

In the JVM, all bytecode operates on a basic principle, that of stack manipulation: each opcode may consume one or more operands off the execution stack, [Each of these "slots" are 32 bits wide, meaning that long and double values pushed on to the stack consume two slots each, a decision that is widely regarded as the biggest mistake in the JVM's implementation today.] and may in turn push an operand on to the execution stack as a result. In addition, each method has a collection of locals, where local variables and arguments live. So, for example, the "aload_0" instruction takes the first argument to the method (the "this" pointer implicit in every instance method call) and pushes it to the execution stack. The "invokespecial" instruction, as its name implies, invokes an instance method, but ignores traditional dynamic binding. (This particular opcode is used in all "super" calls from the language, because we specifically want to invoke the base class version of an overridden method.) Because the Object constructor takes one argument (the "this" pointer on which to invoke the method), it consumes one slot off the execution stack (which is the argument we just pushed, the "this" pointer of our own instance, remember), and because it returns no values (as given by the "V" in the end of the signature above), nothing is pushed on the stack when the method returns. At this point, there's nothing more for the HelloWorld constructor to do, so it simply returns via the "return" opcode.

Still with us? Next, let's tackle one other method, HelloWorld's "main" method, reproduced below:

public static void main(java.lang.String[]);
 Code:
  Stack=2, Locals=1, Args_size=1
  0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
  3: ldc #3; //String Hello, world!
  5: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
  8: return
 LineNumberTable:
  line 5: 0
  line 6: 8

Since this method is static, one particular difference will be the fact that the first argument won't be a "this" pointer, but other than that it'll look similar to what we saw with the HelloWorld constructor. The first opcode, "getstatic", fetches a static field and pushes its value onto the stack, which in this case is the "System.out" reference, described by constant pool entry #2 and displayed in the javap comment after the opcode. Next, we load the constant string "Hello, world!", stored in constant pool entry #3. With both of those references on the stack, we "invokevirtual" the PrintStream.println(String[]) method; since it expects one argument (plus the initial "this" reference on which to invoke the method), both of those items we just pushed onto the stack are consumed, and since println() returns nothing (again, as described by the "V" at the end of the signature), nothing is left on the stack when we're finished. A simple "return" opcode terminates the method, and we're done.

We'll get a bit more complicated as we go on, but for the most part, understanding Java bytecode is an exercise in understanding how each opcode manipulates the execution stack. If you've ever done any assembly in your history, regardless of CPU, Java's bytecode format will seem a highly simpler version.

Java Bytecode Taxonomy

Fundamentally, the JVM bytecode set is broken into several distinct categories. Rather than go over each bytecode opcode individually, we'll instead discuss the categories, highlight a few more common opcodes in particular, and refer you to the JVM Specification for details on the rest.

Stack manipulation. The easiest set of opcodes to understand are those that simply manipulate the stack directly:

  • pop, pop2: Pop the top value of the stack. (pop2 is used if the value is 64 bits wide instead of the usual 32.)
  • dup, dup2: Duplicate the top value of the stack, effectively forming a pop/push/push combination. (Again, dup2 is the 64-bit version of dup.)
  • const_null pushes the null reference onto the stack.
  • bipush pushes a (byte) constant value from -127 to 128 onto the stack.
  • sipush pushes a (short) constant value from -32k to +32k onto the stack.
  • ldc pushes a constant value from the constant pool onto the stack.
  • Xload, where X is one of a, d, f, l or i, pushes a "local" (argument or variable) of the indicated type onto the stack. [Where a means reference, b means boolean, c means char, d means double, f means float, i means integer, l means long and s means short; this encoding scheme shows up repeatedly in the opcode names.]
  • Xstore, where X is one of a,d,f,l or i, pops the top value off the stack and puts it into a "local" slot.
  • Xconst_Y are a series of optimizations in the opcode set, designed to push a constant value Y of type X onto the stack; for example iconst_0 pushes the integer constant 0 onto the stack, effectively a hard-coded variation of bipush (value).

Branching and control flow. No opcode set would be useful without some kind of constructs to allow the code to execute in some kind of nonlinear form. Accordingly, the JVM opcode set has all the basics you would expect:

  • nop, which (not surprisingly) does nothing.
  • if(condition), which tests a condition and, if true, jumps to the indicated opcode offset, where (condition) is one of "null", "notnull", "eq", "ne", "gt", "lt", "_icmpeq", "_icmpne", and so on.
  • goto. Yes, even if Java doesn't support goto, the JVM does.
  • return and Xreturn, where X is one of a, d, f, l, or i, returns from the current to the caller, returning the top of the stack as a value of type X.
  • lookupswitch provides an implementation for switch/case tables.

Arithmetic instructions. Again, the JVM opcode set is similar to almost all other CPU instruction sets in that it provides basic mathematic operations, such as add, substract, multiply and divide, as well as basic conversion operators to do widening and narrowing conversions:

  • The data conversion opcodes take the form XtoY, where X and Y are one of b, c, d, f, l, s or i; take the top of the stack, which must be of the form delineated by X, and push it back onto the stack in the form of Y.
  • The mathematics opcodes take the form XOP, where X is one of d, f, i and l, and where OP is one of add, sub, mul, div, and rem (remainder).
  • The bitwise opcodes take the form iOP, where OP is one one of and, or, xor, shl (shift-left), and shr (shift-right).
  • The comparison opcodes take the form of XcmpY, where X is d (meaning a double-based comparison), f (meaning a float-based comparison), or l (meaning a long-based comparison) and Y is either g or l, where the difference between the two is simply the treatment of NaN. The first value is compared against the second value, and if the first is greater than the second, 1 is pushed on the stack; if the two values are equal, 0 is pushed, and if the second is greater than the first, -1 is pushed.

Object model instructions. The JVM has a built-in awareness of objects, and as a result, has several opcodes designed specifically to work against objects: creating them, calling methods, accessing fields, and so on:

  • new, newarray, anewarray: Create an object (new), an array (newarray), or an array of object references (anewarray). The resulting object or array is pushed onto the top of the stack. Note that in the case of the new opcode, constructors are not called; that is the responsibility of the code to follow.
  • getfield, setfield, getstatic, setstatic Get or set the value of the object field referenced by the reference at the top of the stack, be it an instance or static field. When setting the value, the value is at the top of the stack, and the object reference is right beneath it. When working with static fields, obviously no object reference is necessary.
  • invokevirtual, invokestatic, invokespecial, invokeinterface Each of these calls a method described by the constant pool entry specified as an operand, using the values pushed on the stack as parameters to the call in left- to-right order (meaning the first parameters to the call are the lowest on the stack, the "this" reference being the first, which is also to say, the lowest reference on the stack). The invokevirtual opcode indicates a call invoked in"normal" method calls on objects, invokeinterface when calling a method through an interface reference, invokestatic when calling static methods, and invokespecial is used to call methods without concern for dynamic binding, in order to invoke the particular class' version of a method regardless of derived type overrides.
  • castclass, instanceof As implied, these two deal with casting the object reference at the top of the stack to the type implied in the operand; if it succeeds, either the new reference or a boolean "true" is pushed to the top of the stack, and if it fails, either an exception of type CastClassException is thrown or a boolean "false" is pushed, respsectively.

Block synchronization (that is, synchronized blocks or methods) are handled by two opcodes, monitorenter and monitorexit, each of which takes an object reference on the stack as the object whose monitor the code wishes to attempt to acquire. Note that it is the compiler's job to ensure balanced entry/exit calls, so synchronized methods or blocks generally turn into try/finally blocks to ensure that the monitorexit opcode is always called. (Failure to do so will leave the monitor owned by the thread and eventually deadlock the system.)

Exception handling. Exception handling isn't handled via a particular set of opcodes at all, but by creating a table marking blocks of instructions as guarded and establishing a series of entries indicating what to do, namely, the opcode offset to goto when a particular type of exception is thrown. For example, a fairly simple try/catch/finally block catching several types of exceptions, such as:

public class ExFun
{
 public static void main(String[] args)
 {
  try
  {
   System.out.println("In try block");
   throw new Exception();
  }
  catch (java.io.IOException ioEx)
  {
   System.out.println("In catch IOException block");
   System.out.println(ioEx);
  }
  catch (Exception ex)
  {
   System.out.println("In catch Exception block");
   System.out.println(ex);
  }
  finally
  {
   System.out.println("In finally block");
  }
 }
}
  

... turns into the following method implementation:

Compiled from "ExFun.java"
public class ExFun extends java.lang.Object
 SourceFile: "ExFun.java"
 minor version: 0
 major version: 50
 Constant pool:
  (snipped for simplicity)
{
public ExFun();
 (snipped for simplicity)

public static void main(java.lang.String[]);
 Code:
  Stack=2, Locals=3, Args_size=1
  0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
  3: ldc #3; //String In try block
  5: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
  8: new #5; //class java/lang/Exception
  11: dup
  12: invokespecial #6; //Method java/lang/Exception."<init>":()V
  15: athrow
  16: astore_1
  17: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
  20: ldc #8; //String In catch IOException block
  22: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
  25: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
  28: aload_1
  29: invokevirtual #9; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  32: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
  35: ldc #10; //String In finally block
  37: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
  40: goto 81
  43: astore_1
  44: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
  47: ldc #11; //String In catch Exception block
  49: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
  52: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
  55: aload_1
  56: invokevirtual #9; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  59: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
  62: ldc #10; //String In finally block
  64: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
  67: goto 81
  70: astore_2
  71: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
  74: ldc #10; //String In finally block
  76: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
  79: aload_2
  80: athrow
  81: return
 Exception table:
  from to target type
  0 16 16 Class java/io/IOException
  0 16 43 Class java/lang/Exception
  0 32 70 any
  43 59 70 any
  70 71 70 any
 LineNumberTable: (snipped)
 StackMapTable: (snipped)
}

The contents of the "try" block are the opcode range 0 - 16, so the "catch" blocks from the Java source are encoded in the first two lines of the Exception table (from 0/to 16/target 16/type java.io.IOException and from 0/to 16/target 43/type java.lang.Exception), and the bodies of each begin at the opcode offsets 16 and 43, respectively. Notice, too, that the last three lines of the Exception table indicate that any exception thrown anywhere else in the method–even within the exception blocks–must jump to opcode 70 for processing the body of the finally block. Notice, however, that this is not the only place the finally block appears in the bytecode–it's repeated twice more, once in each catch block. This is just one example of how the javac compiler produces bytecode output that may be surprising and counterintuitive to the Java developer. Also, it's interesting to note that goto opcodes are used to help control flow, so as to create a single point of return when exiting the method from one of the two catch blocks.

The JVM bytecode set has 212 opcodes, with 46 more reserved for future use/expansion. The full set, as mentioned previously, are fully documented in the JVM Specification, and should be kept handy when doing disassembly of Java bytecode (at least until most of the opcodes are recognizable on sight). More importantly, beginners to Java bytecode should recognize that certain Java constructs turn into particular bytecode patterns, and learn to recognize those patterns so as to recognize what the bytecode is doing, without having to work out the stack manipulations on an opcode-by-opcode basis.

Java Bytecode In Practice

Armed with a decent awareness of the JVM bytecode set, we can now examine some common and familiar Java language constructs and see how they map to bytecode, and gain some interesting insight into Java's implementation details while we're at it.

Java 5: Autoboxing

One of the new features of the Java 5 language is that of "autoboxing", where a primitive type instance can be transformed into an object instance because object semantics are required, such as when trying to pass a primitive into a Collection class, as in the following:

public class Autoboxing
{
 public static void main(String[] args)
 {
  int x = 5;
  java.util.ArrayList al = new java.util.ArrayList();
  al.add(x);
 }
}

Under pre-Java5 rules, the add call would fail, since x is not of a type that extends Object. Under Java5, the code compiles into:

0: iconst_5
1: istore_1
2: new #2; //class java/util/ArrayList
5: dup
6: invokespecial #3; //Method java/util/ArrayList."<init>":()V
9: astore_2
10: aload_2
11: iload_1
12: invokestatic #4; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
15: invokevirtual #5; //Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
18: pop
19: return

Line 0 pushes the integer constant 5 onto the stack; line 1 stores the top of the stack (5) into the first locals slot (this is our "x" local). Next, we see four opcode instructions, new/dup/invokespecial/astore, which are the common pattern when an object is new'ed and stored into a local variable. (As an exercise, prove to yourself why the dup opcode is
necessary.) Next, on line 10, we push the ArrayList reference onto the stack, then push the value of the "x" local onto the stack after it. Line 12 we call the static Integer.valueOf method, which requires a single stack slot, and consumes the integer value "5", and pushes in its place the newly-constructed Integer object containing the value 5. This object then, in turn, becomes the argument to the add() call, which consumes both the Integer reference and the ArrayList reference off the stack, and pushes the return value from add() back onto the stack after the call.

In other words, bytecode has revealed to us that autoboxing does pretty much what we would have been required to do in pre-Java5 environments: create a wrapper instance around the value (Integer for int, Long for long, and so on), and pass that resulting object in to the ArrayList instead.

Inner Classes

In the JDK 1.1 release, Sun introduced inner classes, a new feature of the Java language that permitted Java developers to create nested classes that had a special, private-visible relationship to the enclosing class. When introduced, however, no changes to the Java Virtual Machine granting some kind of C++-like "friend" capability were added, leading several Java developers at the time to question how this was possible: how could Java grant private access to classes, when the JVM itself enforced private accessibility, and treated these inner classes no differently than any other kind of class?

In the following example, class Inner obviously has access to the private member named "data" in class Outer:

class Outer
{
 private int data = 12;
 public Inner getInner()
 {
  return new Inner();
 }
 public class Inner
 {
  public int getData()
  {
   return data;
  }
 }
}

public class NestedFun
{
 public static void main(String[] args)
 {
  Outer o = new Outer();
  Outer.Inner i = o.getInner();
  System.out.println(i.getData());
  // prints 12; how?
 }
}

So how, exactly, does the compiler make this little bit of syntactic magic work? Discovering this takes a bit of digging, but all is revealed through bytecode if you follow the call chain. To begin, we start with Nested-Fun.main():

public static void main(java.lang.String[]);
 Code:
  Stack=2, Locals=3, Args_size=1
  0: new #2; //class Outer
  3: dup
  4: invokespecial #3; //Method Outer."<init>":()V
  7: astore_1
  8: aload_1
  9: invokevirtual #4; //Method Outer.getInner:()LOuter$Inner;
  12: astore_2
  13: getstatic #5; //Field java/lang/System.out:Ljava/io/PrintStream;
  16: aload_2
  17: invokevirtual #6; //Method Outer$Inner.getData:()I
  20: invokevirtual #7; //Method java/io/PrintStream.println:(I)V
  23: return

Not surprisingly, this code is fairly straightforward: we see the usual new/dup/invokespecial/astore set to create the instance of Outer, the call to Outer.getInner(), the call to getData(), whose return value is immediately passed to the println() method. (Notice how the compiler chooses to fetch "System.out" prior to the call to getData(), so that the object reference is in the right place on the execution stack for the call to println().) Nothing too exciting here, so let's examine the Outer.Inner.getData() method call:

public class Outer$Inner extends java.lang.Object
 SourceFile: "NestedFun.java"
 InnerClass:
  public #21= #4 of #18; //Inner=class Outer$Inner of class Outer
 minor version: 0
 major version: 50
 Constant pool: (snipped)

{
final Outer this$0;

public Outer$Inner(Outer);
 Code:
  Stack=2, Locals=2, Args_size=2
  0: aload_0
  1: aload_1
  2: putfield #1; //Field this$0:LOuter;
  5: aload_0
  6: invokespecial #2; //Method java/lang/Object."<init>":()V
  9: return

public int getData();
 Code:
  Stack=1, Locals=1, Args_size=1
  0: aload_0
  1: getfield #1; //Field this$0:LOuter;
  4: invokestatic #3; //Method Outer.access$000:(LOuter;)I
  7: ireturn
}

(Note that I've removed parts of the dump to keep things easier to read; any future full-length listings will be similarly edited.) Several interesting things about the generated bytecode appear in this listing. First, we can see that the "outer this" reference mentioned in the Java Language Specification is explicitly added to the Inner class as a field named "this$0" and marked it final, since an inner class can't change its outerclass reference. Second, we can see that the compiler also generated a constructor for Inner that takes an Outer as a reference to fill in as the "outer this", and we can assume that the call to "new Inner()" inside the Outer.getInner() call is going to make use of this constructor. (Verifying this is left as an exercise to the reader.) Last, but ceratinly not least, we can see that something very interesting is going on inside of the Inner.getData() method, in that it's accessing a static method called "access$000" on the Outer class to get the data returned by the get- Data() call. Sure enough, if we look on Outer...

class Outer extends java.lang.Object{
private int data;

Outer();
 Code:
  0: aload_0
  1: invokespecial #2; //Method java/lang/Object."<init>":()V
  4: aload_0
  5: bipush 12
  7: putfield #1; //Field data:I
  10: return

public Outer$Inner getInner();
 Code:
  0: new #3; //class Outer$Inner
  3: dup
  4: aload_0
  5: invokespecial #4; //Method Outer$Inner."<init>":(LOuter;)V
  8: areturn

static int access$000(Outer);
 Code:
  0: aload_0
  1: getfield #1; //Field data:I
  4: ireturn
}

... we can see that the compiler generated a static method that specifically opens a hole in the encapsulation wall to grant access to "data" to callers. Unfortunately, because Java has no explicit sense of granting access to specific classes, the "access$000" method has to be marked as package-private, meaning any class in the same package has access to this method. Whether this particular implementation is a hack or an elegant way to add functionality to the language after its initial definition is left up to the reader to decide. What is true, however, is that any language can make use of this hole once they know about it. So a Groovy client could, conceivably, call this method to bypass the "private" declaration on data on any Outer instance, which clearly wasn't a concern back in 1997 when this feature was introduced, but could be of concern now.

Readers are invited to explore other Java language constructs and see how they map into Java bytecode, such as the enhanced for loop, variable argument lists, enumerations, and so on. For a real challenge, take one of the new dynamic languages for the Java platform, such as Rhino (JavaScript) or JRuby, and examine the bytecode generated for particular language constructs there, such as Groovy's closures or JRuby's mixins.

Using Java Bytecode

A variety of tools are available that work on Java bytecode, and while it's well beyond the scope of this article to describe them all, certainly
it does no harm to list them and offer a one-sentence description and potential purpose of each:

  • • Javassist is a bytecode-manipulation tool that allows for either static- or dynamic-time manipulation of class bytecode; when combined with a structured syntax to describe what code to manipulate and where, it can form the basis of an Aspect-Oriented Programming platform or language, and in fact has been put to such purpose in JBoss.
  • BCEL and ASM are each lower-level bytecode-manipulation libraries (the first from Apache, the second from ObjectWeb) that can either tweak existing class bytecode or even create new classes on the fly; the Sun JDK includes a version of BCEL in a sun.* package specifically for this purpose, in fact, in order to support the creation of dynamic proxies.
  • Jasmin is an open-source project that provides a "Java assembler", allowing developers to explore bytecode constructs directly, thus helping to see the line between the Java language and the Java Virtual Machine itself. As an interesting exercise, the reader is encouraged to determine if it's possible for one class to access the private fields of another through bytecode. (In other words, is it the Java compiler that enforces private accessibility, or the JVM itself?) Jasmin was originally a project created by the authors of the book "Java Virtual Machine" by Jon Meyer and Troy Downing, and although the book is out-of-print, the assembler lives on.
  • Bill Venners' Inside the Java Virtual Machine is probably one of the better Java Virtual Machine books written, and contains an excellent overview of Java bytecode to go alongside the Java Virtual Machine Specification, along with good descriptions of other facets of JVM internals. A bit dated at this point, but given that the JVM itself hasn't changed significantly (except in the details of the garbage collector and JIT compiler), it's still highly relevant.

Probably the most important tool to puzzling through bytecode, however, remains the javap disassembler, and readers are encouraged to play with it, to get a better feel for what language constructs turn into what bytecode patterns. Then it becomes a relatively trivial exercise to pull it out in production, examine the raw code inside of a jar file, determine where the problem is, and amaze your friends and co-workers with your incredibly deep knowledge of the Java platform. Fame and fortune are sure to follow... and if not, at least the well-deserved satisfaction that you have a much deeper idea of what's going on under the hood.

Biography

Ted Neward is an independent consultant specializing in high-scale enterprise systems, working with clients ranging in size from Fortune 500 corporations to small 20-person shops. He speaks on the conference circuit, including the No Fluff Just Stuff Symposium tour, discussing Java, .NET and XML service technologies, focusing on Java-.NET interoperability. He has written several widely-recognized books in both the Java and .NET space, including the recently-released "Effective Enterprise Java". He lives in the Pacific Northwest with his wife, two sons, two cats, and eight PCs.

Related Topics: JSRs and APIs, VIEW ALL TOPICS