Unlocking Java Bytecode: How to Read, Decompile, and Rebuild Lost Source
This article explains why Java developers should understand bytecode, walks through JVM data types and stack architecture, demonstrates how to inspect compiled classes with javap, and shows practical examples of manual decompilation and reconstruction of source code using low‑level bytecode analysis.
Even experienced Java developers find reading compiled Java bytecode tedious, but learning it can rescue lost source code and deepen understanding of the JVM.
Why Learn Bytecode?
A recent incident where a JAR was deployed without source control highlighted the value of being able to reconstruct code from bytecode when the original source is unavailable.
JVM Data Types
Java’s static typing influences bytecode design. Primitive types include byte, short, int, long, char, float, double, boolean, and returnAddress. Reference types cover classes, arrays, and interfaces. Note that boolean is compiled as int because the JVM lacks direct boolean instructions.
Stack‑Based Architecture
The JVM uses a stack‑based VM rather than a register‑based one. Key components per thread include:
PC register – holds the address of the current instruction.
JVM stack – stores local variables, method parameters, and intermediate results.
Heap – shared memory for objects and arrays, managed by the garbage collector.
Method area – contains method bytecode, symbol tables, and the constant pool.
Bytecode Exploration
Each method in a class file has a code attribute consisting of a sequence of instructions. For example, the simple method:
public static void main(String[] args){
int a = 1;
int b = 2;
int c = a + b;
}produces the following bytecode (shown with javap -v Test.class):
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: returnThe iconst_1 and iconst_2 push constants onto the operand stack, iadd adds them, and istore_3 stores the result.
Method Calls
When invoking another method, arguments are pushed onto the operand stack before the call. Consider adding a static calc method:
static int calc(int a, int b){
return (int)Math.sqrt(Math.pow(a,2) + Math.pow(b,2));
}The compiled bytecode includes an invokestatic instruction that references the method via the constant pool, followed by the usual stack manipulation.
Example Development
A more complex example introduces a Point class and uses it to compute an area:
public class Test {
public static void main(String[] args) {
Point a = new Point(1,1);
Point b = new Point(5,3);
int c = a.area(b);
}
}
class Point {
int x, y;
Point(int x, int y){ this.x = x; this.y = y; }
public int area(Point b){
int length = Math.abs(b.y - this.y);
int width = Math.abs(b.x - this.x);
return length * width;
}
}The generated bytecode shows the use of new, dup, and invokespecial to create and initialize objects, followed by invokevirtual to call area.
Conclusion
Because the bytecode instruction set is simple and compiler optimizations are minimal, inspecting class files can be an effective way to track code changes or recover lost implementations when source code is unavailable.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
21CTO
21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
