How many objects do you think your app creates in a single second? Ten? A hundred? Try tens of thousands. Every time you write something like val user equals User, or listOf, or even a simple string template, you are asking the runtime to find a chunk of memory, set it up, and hand you back a reference. And yet your app feels fast. It does not stutter. So what is actually going on under the hood? How can creating thousands of objects per second be essentially free? That is what we are going to unpack today. By the end of this video, you will understand exactly what happens when you allocate an object, why it is almost always fast, and the rare situations where it actually does become a problem.
Let us start with the basics. When you write val user equals User with some name and age, Kotlin compiles that into bytecode that says "allocate a new object of type User." On the JVM or on Android's ART runtime, this triggers memory allocation on the heap. The heap is a big region of memory managed by the runtime. Think of it like a giant warehouse. Every object you create gets placed somewhere in that warehouse. But here is the thing. The runtime does not go searching through the warehouse looking for an empty spot every time. That would be incredibly slow. Instead, it uses a brilliant trick that makes allocation nearly as fast as incrementing a number.
That trick is called the TLAB, which stands for Thread-Local Allocation Buffer. Here is the analogy. Imagine the warehouse manager gives each worker their own personal shelf. Instead of everyone going to the main warehouse every time they need to store something, each worker just puts things on their own shelf. No waiting, no conflicts, no coordination needed. A TLAB is exactly that. Each thread in your app gets its own small chunk of memory, typically around 256 kilobytes to a few megabytes. Inside that buffer, allocation works with what we call a bump pointer. There is a pointer that says "this is where the next free spot is." When you allocate an object, the runtime simply moves that pointer forward by the size of the object. That is it. One addition operation. No searching, no locking, no negotiation with other threads. This is why people say object allocation on the JVM is almost free. And they are right. A TLAB allocation is literally just bumping a pointer forward, which is roughly the same cost as incrementing an integer.
So what happens when a thread's TLAB runs out of space? The pointer has reached the end of the buffer. Now the runtime has a decision to make. Usually it just asks the heap for a new TLAB. The thread gets a fresh chunk of memory and continues with bump pointer allocation as before. But if the heap itself is running low on space, the runtime triggers a minor garbage collection. This cleans up short-lived objects in the young generation, which frees up memory to carve out new TLABs. On Android's ART runtime, the mechanism is similar. ART uses what it calls thread-local allocation regions, which serve the same purpose. Each thread gets a private buffer, bump pointer allocation happens inside it, and when the region fills up, the runtime allocates a new one or triggers a GC if needed. The key takeaway is this: the common case, the fast path, is incredibly cheap. It is only when the system runs low on memory that things get interesting.
Now let us look at what actually gets written into memory when an object is allocated. When you create a simple Kotlin data class like data class Point with val x Int and val y Int, you might think the object in memory is just those two integers. Eight bytes, right? Four bytes for x, four for y. But the reality is very different. Every object in memory has a header that comes before your actual fields. On a 64-bit JVM with compressed oops enabled, which is the default for heaps under 32 gigabytes, the object header is typically 12 bytes. It contains three things. First, a mark word, which is 8 bytes. This stores the object's identity hash code, garbage collection metadata like the age of the object, and lock state for synchronization. Second, a class pointer, which is 4 bytes with compressed oops. This points to the class metadata so the runtime knows what type this object is. So before we even get to your data, there are already 12 bytes of overhead.
Let us do the full math for our Point class. We have the 12-byte object header, then 4 bytes for x, and 4 bytes for y. That gives us 20 bytes. But the actual size in memory is 24 bytes, not 20. Why? Because of alignment padding. The JVM aligns objects to 8-byte boundaries. Since 20 is not a multiple of 8, it pads up to 24. This might seem like a small detail, but it adds up. If you have a data class with a single Boolean field, the object still takes up 16 bytes in memory. Twelve for the header, one for the Boolean, and three bytes of padding to reach the next 8-byte boundary. That is a 16-to-1 ratio between the memory used and the actual data stored. For arrays, there is an additional 4 bytes in the header to store the array length, making array headers 16 bytes total. So an IntArray of 10 elements is 16 bytes for the header plus 40 bytes for the 10 integers, equaling 56 bytes. Understanding this overhead helps you reason about memory when it actually matters.
Let us talk about how Android's ART runtime differs from the standard JVM HotSpot. ART has a few important differences that affect allocation and memory layout. First, object headers in ART are typically 8 bytes, not 12. ART uses a more compact representation, which saves memory on devices where every kilobyte counts. Second, ART uses a different garbage collector. Modern versions of ART use a concurrent copying collector, which can compact memory while the app is running. HotSpot's default G1 collector also does compaction, but the strategies differ. Third, and this is important for Android developers, ART has a tighter memory budget. A phone might give your app 256 megabytes of heap, whereas a server JVM might get 8 gigabytes or more. This means the GC fires more often on Android, and allocation pressure has a more noticeable effect. Fourth, ART performs ahead-of-time compilation. Your Kotlin code gets compiled to native machine code at install time, which can optimize some allocation patterns away entirely. The bottom line is that ART is designed for a constrained environment, so it is leaner but also less tolerant of wasteful allocation.
Now here is the part that might surprise you. Despite all this overhead, creating lots of objects is usually fine. Seriously. The bump pointer allocation in a TLAB is so fast that you can create millions of small objects per second without breaking a sweat. And most of those objects die young. They are created, used briefly, and then become unreachable. The garbage collector's young generation is specifically designed for this pattern. Cleaning up short-lived objects in the young generation is extremely cheap because the GC only needs to copy the few survivors, not clean up the many dead objects. So if you are writing idiomatic Kotlin, using data classes, creating lists with map and filter, passing lambdas around, you are generating tons of temporary objects, and that is perfectly okay. Do not let anyone tell you to rewrite your beautiful functional-style Kotlin code to avoid allocations unless they have profiler data to back it up.
But there are genuine cases where allocation does become a problem, and you should know what they look like. The first and most common is allocation in tight loops. If you are processing every pixel of a bitmap, or running a physics simulation every frame, or doing heavy computation inside a hot loop, creating objects on every iteration can trigger frequent garbage collections. Those GC pauses, even short ones, cause visible jank if they happen during your frame rendering window. The second case is large allocations. When you load a bitmap into memory, you might be allocating several megabytes at once. This bypasses the TLAB entirely and goes straight to the main heap, which is slower and can trigger GC. On Android, BitmapFactory decoding is one of the most common sources of large allocations. The third case is allocation rate. If your app is allocating so fast that the garbage collector cannot keep up, you get into a state where GC runs are back to back, consuming CPU time and causing frame drops.
So what do you do in those specific cases where allocation matters? For tight loops, you can reuse objects. Create an object before the loop and update its fields inside the loop instead of creating a new one each time. For bitmap handling, use BitmapFactory.Options with inBitmap to reuse existing bitmap memory instead of allocating fresh bitmaps. For collection-heavy code in hot paths, consider using primitive arrays instead of Lists of boxed types. An IntArray uses dramatically less memory than a List of Int because you avoid the per-element object overhead. Kotlin inline classes can help too. If you have a value class wrapping a single primitive, the compiler can often avoid allocating a wrapper object at all. And for really performance-critical code, you can use object pools, though be careful because pools add their own complexity and can introduce bugs if you accidentally use a returned object.
Let us put this all together with a practical example. Say you have a RecyclerView that shows a list of users, and each user has a profile image. When you scroll, the adapter's onBindViewHolder method fires for each newly visible item. Inside that method, you might create a User object, a Glide request, maybe some layout params. That is fine. Those allocations happen at most a few dozen times per scroll gesture, and the TLAB handles them instantly. But say inside onBindViewHolder you are also running some image processing, creating a new Bitmap and a Canvas and a Paint object for every single item. Now you are allocating large objects at high frequency during a scroll, which is exactly when you need every millisecond of your 16-millisecond frame budget. This is where allocation becomes visible. The fix? Pre-allocate the Bitmap, reuse the Canvas, cache the Paint. Move the heavy allocations out of the hot path.
Here is what I want you to take away from this video. Object allocation on modern runtimes is brilliantly fast. The TLAB bump pointer mechanism makes creating objects nearly free in the common case. Every object carries overhead in its header, and alignment padding adds more, but for most code this does not matter. Where allocation does matter is in tight loops, large object creation, and sustained high allocation rates, especially on Android where the memory budget is tight and GC pauses can cause jank. The right approach is not to avoid allocation everywhere. That leads to ugly, unreadable code. The right approach is to write clean, idiomatic Kotlin, profile when you see problems, and optimize the specific hot paths where allocation actually shows up as a bottleneck. Know how it works, but do not fear it.