Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and such attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.
Yet we should not pass up our opportunities in that critical 3%. Good programmers will not be lulled into complacency by such reasoning, they will be wise to look carefully at the critical code; but only after the critical code has been identified. ... After working with such tools for seven years, I've become convinced that all compilers written from now on should be designed to provide all programmers with feedback indicating what parts of their programs are costing the most; indeed, this feedback should be supplied automatically unless it has been specifically turned off.
-- D. Knuth,Structured Programming with go to statements(1974), reproduced in Literate Programming (1992).
Without read statements, everything in a MiniJava program could be folded away. With read statements, things become more interesting again, but there remain places where there are compile-time constants (e.g. 0-1).
Addition and multiplication are commutative, so you can rewrite sequences like push a; push b; swap; + to get rid of the swap operation.
If a and b have no interdependencies (through side-effects or conflicting assignments), then you can exchange their order. For example, you can sometimes use this fact to get rid of a swap operation during a subtraction.
After an assignment x = y, we can use the value of y everywhere we previously used the value of x (up until the value of x is re-assigned). If you think of leaving a return value on the stack as an assignment to an anonymous temporary, then you can use copy propagation to eliminate some of the intermediate stores that appear because MiniJava return statements have to be at the end of functions.
Suppose you have a loop test like j < size, where size is some class field. Chances are, the loop isn't changing that size field, so why should you bother with a field lookup every time? You can compute the field once and save it on the stack for use during the loop iterations.
If you can do a few iterations of a loop without encountering a jump statement, you can save some instructions. Even more, you can optimize based on the flow of data between loop iterations (e.g. by removing redundant assignments).
You can get rid of a vtable lookup on method dispatch if you (a) can statically compute the object's dynamic type (e.g. because you have a method call immediately following object creation), (b) can tell that the static type has no children, or (c) can tell that no children override this method.
Because MiniJava has provides no way to directly access fields of any object but this, there are lots of getter and setter methods. These are prime targets for inlining -- they're short, they typically have no inconvenient local variables, and the dispatch overhead accounts for a lot of the cost of the call.