Skip to content

The Stack

pulse edited this page Apr 13, 2024 · 4 revisions

The Stack

The stack is a very important part of the JVM. It's very simple to understand how it works just by its name, "stack" refers to stacking values on top of each other.

It centers around the 'push & pop' concept where values are pushed and popped from the stack. pushing refers to putting a new value onto the stack. popping refers to removing the latest value from the stack.

The values on the stack are called 'stack elements'. Imagine a stack being sort of like this:

{
   element_1,
   element_2,
   element_3,
}

when you push a value onto the stack you add a new element and the stack updates to this:

{
   element_1,
   element_2,
   element_3,
   element_4,
}

when you pop a value from the stack you remove the last element that was pushed, meaning element_4 here would be removed.

The JVM has instructions for pushing and popping values on and off the stack, like: bipush, sipush, pop, dup etc.

The instructions on this page will be covered in order to the examples provided above. The most important part about these instructions is that for some reason they are signed meaning the actual value they can push is cut in half to represent sign.

For bonus I will also cover ldc and ldc_w.

bipush

The bipush instruction is again easy to understand by its name 'byte push'. This instructions pushes a value that is 1 byte in length onto the stack, meaning the maximum you can push with this instruction (because it's signed) is 127 and the minimum is -128.

The syntax is very easy to use in most assemblers, here I will be using JASM:

bipush 127
bipush -128

sipush

The sipush instruction is just like bipush the only difference being is that it can push a value that is 2 bytes in length also known as a short, hence the name 'short push'. sipush can push a value that is at maximum (because it's signed) 32767 and the minimum is -32768.

sipush 32767
sipush -32768

pop

The pop instruction really doesn't need to be explained it simply pops the last element on the stack.

sipush 32767
pop

dup

The dup instruction is also really self explanatory as it simply duplicates the last element on the stack.

sipush 32767
dup

The stack after:

{
   32767
   32767
}

ldc

The ldc instruction is the big boy in terms of the amount of data it can hold, in total the ldc instruction can hold 4 signed bytes also known as a word, the reason for this being is because the ldc instruction doesn't actually push a value onto the stack, it's in the name 'load constant' it references a constant pool entry for its value and in reality pushes the index of the entry onto the stack.

In assemblers this is dumbed down so you don't have to manually make constant pool entries and specify their indexes, you simply specify the value and it abstracts everything away, it works the same as bipush and sipush in assemblers.

It can hold a lot of types like integers, longs, shorts, floats, strings and so on. The main problem with the ldc instruction is that it's given 1 signed byte for the index of the pool entry. If your constant pool has more than 127 entries you are in some trouble, which is why ldc_w exists.

ldc_w

The ldc_w instruction is the same as the ldc instruction but there is one key difference being that it has 2 signed bytes for the index of the pool entry meaning a short, this means your pool can be 32767 entries long.

Most assemblers abstract this part away as well meaning it counts the amount of pool entries you have and uses ldc_w if there are more than 127.

The w in ldc_w stands for wide, in the JVM there are a lot of instructions that are wide meaning they have more bytes for a pool entry index.

Consumption & production

The stack is often used to perform operations like invocation for methods. Let's say I want to invoke a method with the descriptor (II)Z, to do so I need to pass arguments to the method, in this case being of type primitive integer.

boolean isEqual(int a, int b) {
   return a == b;
} 

Now I can invoke it:

aload this // load this class to use as object for virtual method
bipush 16 // push first integer argument for method
bipush 91 // push second integer argument for method
invokevirtual sandbox/Entry.isEqual (II)Z // invoke using the loaded object & arguments

Once the method here is invoked and all 3 stack elements that were loaded onto the stack have now been consumed by the method call meaning they don't exist on the stack anymore. I say three because aload also loads an element onto the stack but at the moment that isn't important.

The call consumes our stack and pushes its return value, in this case a boolean, meaning 0 or 1 is now on the stack and in the next instruction we can use it.

Clone this wiki locally