Quick question for you. When you write val user equals User parentheses in Kotlin, where does that object actually go? Like physically, in your phone's memory, where does it end up? Most Android developers write code like this hundreds of times a day and never think about it. And honestly, you don't have to think about it most of the time. But when you're chasing a memory leak, or trying to understand why your app just crashed with a StackOverflowError, or wondering why sharing data between coroutines is tricky, this is exactly the knowledge that separates someone who guesses from someone who knows. So today, let's pull back the curtain. Let's look at where your objects actually live.

To understand where things go in memory, you need to know about two main regions: the stack and the heap. Think of them like two very different storage systems. The stack is like a stack of cafeteria trays. You can only add a tray on top, and you can only remove the tray that's on top. It's extremely fast, super organized, but rigid. The heap is more like a big warehouse. You can put boxes anywhere there's space, in any order, and retrieve them whenever you want. It's flexible, but it takes more work to manage. Every thread in your app gets its own stack, but there's only one heap, and every thread shares it. This distinction is fundamental to how the JVM and Android Runtime work, and it drives a lot of the behavior you see in your apps.

Let's start with the stack because it's simpler. Every time you call a function, the JVM creates something called a stack frame. That frame holds all the local variables for that function call, the parameters that were passed in, and the return address so the system knows where to go back when the function finishes. Imagine you call a function called fetchUser, and inside fetchUser, you call validateToken, and inside validateToken, you call decodeBase64. Each of those function calls pushes a new frame onto the stack. When decodeBase64 finishes, its frame gets popped off. Then validateToken finishes and its frame gets popped. Then fetchUser finishes and its frame gets popped. It's beautifully automatic. You never have to free anything. The stack cleans up after itself. That's what makes it so fast.

Now the heap. The heap is where objects live. When you write val user equals User parentheses, the JVM allocates space on the heap for that User object. All its fields, its name string, its age integer, its email, they all live on the heap as part of that object. The heap is big, it's shared across all threads, and it's managed by the garbage collector. Unlike the stack, memory on the heap doesn't get cleaned up automatically when a function returns. Instead, the garbage collector periodically scans the heap, finds objects that nothing points to anymore, and reclaims that memory. This is incredibly convenient. You don't have to manually free memory like in C or C plus plus. But it comes with a cost. The garbage collector takes CPU time, and if it runs at the wrong moment, you might see a dropped frame or a small stutter in your UI.

So here's the key mental model I want you to build. When you declare a local variable like val user equals User parentheses inside a function, two things happen. First, a reference, basically a pointer, gets created on the stack as part of the current function's stack frame. Second, the actual User object gets allocated on the heap. The stack variable is just an address. It says hey, the real object is over there in the heap at this memory location. When that function returns and the stack frame is popped, the reference on the stack disappears. But the object on the heap? It stays there until the garbage collector decides nothing else points to it. This is why you can return an object from a function and it doesn't vanish. The object lives on the heap, independent of any particular stack frame.

Now let's talk about primitives versus objects because this is where the JVM gets interesting. In Java, and by extension in the JVM that Kotlin targets, primitive types like int, float, double, boolean, these can live directly on the stack. When you write var count equals 42, that number 42 sits right there in the stack frame. No heap allocation, no reference, just the raw value. But Kotlin tries to hide this distinction from you. When you write val count colon Int equals 42, Kotlin compiles that to a JVM primitive int when it can. But the moment you make it nullable, val count colon Int question mark, Kotlin has to box it into a java.lang.Integer object, which lives on the heap. Same value, 42, but now it's wrapped in an object with a reference. This boxing is usually invisible, but in hot loops or performance-critical code, it can add up. The Android Runtime, ART, handles this similarly but has its own optimizations for how objects are laid out in memory.

Here's something that trips up a lot of Kotlin developers. The keywords val and var describe whether the reference can be reassigned. They say nothing about where the object lives. Val user equals User parentheses, that user reference is on the stack, and the User object is on the heap. Var user equals User parentheses, same thing. The user reference is on the stack, the object is on the heap. The only difference is that with var, you can point that reference to a different User object later. With val, you cannot. But in both cases, the actual User object is sitting out there on the heap. Val does not mean the object is immutable. Val does not mean the object is on the stack. It just means this particular reference, this particular arrow pointing from the stack to the heap, cannot be changed to point somewhere else.

Let's make this even more concrete with a real scenario. Say you have a data class called User with a name and an age. You write a function called createUser that takes a name string and an age int, creates a User, and returns it. When createUser is called, a new stack frame is pushed. The parameters name and age are in that frame. Name is a reference to a String object on the heap. Age, being a primitive int, is stored directly in the stack frame. Inside the function, val user equals User name comma age allocates a new User object on the heap. The user reference sits on the stack. When the function returns, the stack frame is gone, poof. But the User object survives on the heap because the calling code now holds a reference to it. Follow the arrows: stack frame holds references, references point to heap objects, and the garbage collector follows those arrows to figure out what's still alive.

Now here's something that matters a lot on Android. Each thread gets its own stack, and on Android, the default stack size is just 512 kilobytes. That sounds tiny, and it is. 512 KB has to hold every stack frame for every nested function call in that thread. Most of the time this is fine because each stack frame is pretty small, just a few references and primitives. But if you have deeply recursive code, or you're nesting function calls dozens of levels deep, you can exhaust that 512 KB and get a StackOverflowError. This is not the website, this is a real error telling you your stack literally overflowed. On the main thread, Android actually gives you a larger stack, around 8 MB, but background threads and threads from thread pools often get the smaller default. Coroutines help here because they use suspension instead of deep call stacks, but it's still good to know the limit exists.

Let's visualize this all together. Picture your app running with two threads, the main thread and a background thread. Each thread has its own stack growing downward. The main thread's stack has frames for onCreate, then loadData, then parseJson, stacked on top of each other. The background thread's stack has frames for its own function calls. But over to the side, there's one big heap shared by both threads. The parseJson frame on the main thread has a reference pointing to a JsonObject on the heap. The background thread's frame also has a reference pointing to maybe the same object, or maybe a different one. This shared heap is exactly why thread safety matters. If two threads hold references to the same heap object and both try to modify it, you get a data race. The stack is private per thread, so local variables are always thread-safe. But the heap is shared territory.

So why does all of this matter for your day-to-day Android work? Let's connect the dots. Memory leaks happen when an object on the heap is kept alive by a reference that should have been cleared. Classic example: an inner class holding a reference to an Activity. The Activity's stack frames are long gone, but the reference from that inner class keeps the whole Activity alive on the heap, preventing garbage collection. That can be hundreds of kilobytes or even megabytes of leaked memory. Understanding that references are on the stack but objects are on the heap helps you reason about these leaks. When you see a LeakCanary report, you're literally looking at a chain of references, arrows from one heap object to another, that's keeping something alive that should be dead.

And there are performance implications too. Every time you create an object, the heap has to find space for it. Every time an object becomes unreachable, the garbage collector has to find it and reclaim that space. On Android, GC pauses used to be a real problem. They're much better now with ART's concurrent garbage collector, but they still exist. If you're creating thousands of short-lived objects in a tight loop, say inside an onDraw method or during a RecyclerView bind, you're putting pressure on the garbage collector. Knowing that primitives can avoid the heap and that object creation has a cost lets you make smarter choices. Use primitive arrays instead of lists of boxed integers in hot paths. Reuse objects where it makes sense. These aren't premature optimizations when you understand the underlying mechanics. They're informed decisions.

So let's recap what you now know. The stack is fast, automatic, per-thread, and limited in size. It holds function frames with local references and primitive values. The heap is large, shared across threads, and managed by the garbage collector. It holds your actual objects. When you write val user equals User, the reference is on the stack, the object is on the heap. Val versus var controls the reference, not the object. Primitives can live on the stack, but boxed or nullable versions live on the heap. Android's default thread stack is just 512 KB, so deep recursion is risky. And most importantly, understanding this stack and heap relationship is the foundation for reasoning about memory leaks, garbage collection performance, and thread safety. Next time you write User parentheses, you'll know exactly where that object ends up and why it matters.