The JMM divides the memory space between thread stacks and the heap. Each application has at least one thread, which is referred to as the main thread. When a new thread starts, a new stack is created. If an application has several threads, the simplified memory model may look like this:
The thread stack is a stack of blocks. Whenever a thread calls a method, a new block is created and added to the stack. This is also referred to as the call stack. This block contains all the local variables that were created in its scope. The local variables cannot be shared between threads, even if threads are executing the same method. A block fully stores all local variables of primitive types and references to objects. One thread can only pass copies of local primitive variables or references to another thread:
Kotlin doesn't have primitive types, in contrast to Java, but Kotlin does compile into the same bytecode as Java. And if you don't manipulate a variable in the same way as an object, then the generated bytecode will contain the variable of a primitive type:
fun main(vars: Array<String>) {
val localVariable = 0
}
The simplified generated bytecode will look like this:
public final static main([Ljava/lang/String;)V
LOCALVARIABLE localVariable I L2 L3 1
But if you specify the type of the localVariable as Nullable, as follows:
val localVariable: Int? = null
Then this variable will be represented as an object in the bytecode:
LOCALVARIABLE localVariable Ljava/lang/Integer; L2 L3 1
All objects are contained in the heap even if they're local variables. In the case of local primitive variables, they'll be destroyed automatically when the execution point of a program leaves the scope of the method. The object can be destroyed only with the GC. So the use of local primitive variables is preferable. Since the Kotlin compiler applies optimizations to variables that can be primitive, in most cases the bytecode will contain variables of primitive types.
This diagram illustrates how two threads can share the same object: