Memory address of variable after reassignment
"Local variable" is a concept in the source language. Some target languages have the concept (e.g. virtual machines with dictionary-based scopes), but many do not. Since you're using Java, let's look at the JVM.

A local variable cannot be accessed by other methods (e.g. you can't pass a variable by reference like you can in C++ or Fortran), and cannot be accessed by other threads. This gives a Java compiler (and a JVM implementation, for that matter) a lot of freedom about what to do with it.

JVM code has two "local" areas where it may store data: the operand stack and the local variable array. If you want to read along, this is covered in section 2.6 of the JVM specification. As the names suggest, the operand stack is organised like a stack (mostly push and pop-type operations), and the local variable array is organised like an array (dereference-type operations).

The JVM is designed so that a compiler can emit code where every local variable has one exactly entry in the local variable array. To use the value, the compiler would emit code to transfer a value from the local variable array to the stack (the instruction is called "load"; there are variants for variables of different types, but we'll ignore this complication for now), and to write a value to the variable, there is a corresponding "store" instruction which transfers a value from the stack to the local variable array. Section 3.2 of the specification covers this.

The reason why the JVM is designed with this in mind is that it allows for very simple compilers. However, this is not the only possible way to generate code for the JVM.

A local variable cannot be accessed by another method (e.g. you can't pass it by reference like you can in C++), or on another thread. So the compiler has a lot of freedom about what it can do with one.

If a variable is extremely short-lived, for example, it may not need an entry in the local variable array. Consider, for example:

for (int i = 0; i &lt; 10; ++i) {
    int x = f(i);
    g(x,x);
}


A simple way of compiling the body of this loop might be something like this.

// x = f(i);
iload i    // push i onto the stack
invoke f   // pop the argument, call f, and then push the return value on the stack
istore x   // pop the stack and store the value in local variable x
// g(x,x);
iload x    // This probably doesn't need to be explained, right?
iload x
invoke g


(Note that this isn't actually JVM bytecode. The JVM uses slot numbers in the bytecode stream, not names, and also has different "invoke" instructions depending on what "f" actually is; it might be a static method, or an interface method, or one of a number of other things. Nonetheless, this should give you the basic idea.)

The variable x doesn't really need to be stored in the local variable array. When this method calls the method f(), the return value is pushed onto the operand stack. Invoking the method g() requires two values to be on the top of the operand stack, representing its arguments. The JVM has an instruction (called "dup") which duplicates the value on the top of the operand stack, so the body of this loop could be compiled as:

iload i
invoke f
dup        // Duplicate the top value on the stack
invoke g


In fact, in this simple example, the compiler may not need to allocate local storage for i, either.

There are lots of other possibilities. The compiler might reuse local storage slots for local variables whose lifetimes don't overlap, or it might assign different locations to the same variable for different parts of the code; this is known as live range splitting. In a sense, the distinction between the operand stack and the local variable array when compiling for the JVM is very like the distinction between registers and the stack when compiling for a physical CPU, and many of the same ideas carry across.

Now, there are a few reasons why a compiler might not want to go to all this trouble.

First off, it can be a lot of work. Optimal register allocation is a formally unsolvable problem, although for some reasonable definitions of "optimal" it's NP-complete. Nonetheless, for stack machines like the JVM, very simple techniques work in practice.

Secondly, chances are good that if this is performance-critical code, it will be JIT-compiled anyway, and so many of these optimisations will be done by that part of the JVM implementation.

Thirdly, it complicates debugging. If a variable can be stored in more than one location, then the debugger needs to know where to find the value any point in the program. If the reason why you're optimising local variable storage is to reduce the size of the class file, chances are good that it won't help, because what you gain in the number of instructions you will probably lose in debugger metadata.

This is one reason why software projects often use "debug builds" during development.

One final thing: I've been talking about basic types like integers here, but one additional complication in optimising the storage of local variables is how it interacts with the garbage collector, since the garbage collector needs to know what heap objects are referred to by local variables.

Garbage collection is a more advanced topic, and you didn't ask about it, so I won't go into the details. It doesn't affect how a Java compiler would compile to JVM bytecode, but it may affect the design of the JVM implementation. Once again, a local variable can't be accessed by other methods or other threads, so this gives the JVM implementation a lot of freedom on how it can let the garbage collector know what is live and what isn't.