Overview

Appendix E. Memory management in Java apps

This appendix introduces how the JVM organizes and manages memory in Java applications and why this matters for troubleshooting. Memory is finite, and poor handling leads to slower startups, higher costs, degraded throughput, and even process termination via OutOfMemoryError. The discussion centers on the three main regions involved in allocation and execution: per-thread stacks for local data, the shared heap for object instances, and the metaspace for type metadata. With just enough detail to support diagnosis elsewhere in the book, it sets expectations about common symptoms and where to look for root causes.

The stack is private to each thread and holds method parameters and locally declared variables in stack frames created when a block or method begins and removed when it ends—last in, first out. This predictable lifecycle makes stack memory self-cleaning under normal execution. Problems arise when the stack’s fixed size is exceeded, most commonly through runaway recursion or excessive call depth, resulting in StackOverflowError that halts only the offending thread and leaves a telltale stack trace. Although stack references typically don’t cause leaks, stalled execution (for example, a deadlock) can keep frames alive longer than intended, temporarily retaining references to heap objects.

The heap is shared by all threads and stores object instances and their fields; it is therefore both the most contested resource and the most frequent source of memory issues. The garbage collector reclaims objects only when they are no longer reachable, so lingering references—such as entries accumulated in long-lived collections or objects held by static fields—can create memory leaks. Because all threads allocate from the same heap, the first thread that cannot allocate may report an OutOfMemoryError even if another thread caused the leak. Finally, the metaspace holds class and type metadata; while less commonly a bottleneck, it can also be exhausted under certain conditions, producing its own OutOfMemoryError.

Figure E.1 An app’s memory is a limited resource. There’s no magical solution that allows us to allocate infinite memory to an app. When building apps, we need to treat memory consumption with consideration and avoid spending it for no reason. Apps may sometimes have memory issues. If a certain capability uses too much memory, it can cause performance problems or even a complete failure. You need to be ready to find the causes of such issues and solve them properly.
Figure E.2 T1, T2, and T3 are all threads of a Java app. All these threads use the same heap. The heap is a memory location where the app stores object instances’ data. However, each thread uses its own memory location, called a stack, to store data locally declared
Figure E.3 The app reserves the locally declared variables in the thread’s stack and the data defining an object instance in the heap. A variable in one thread’s stack may refer to an object in the heap. In this example, variable x, holding value 10, and variable c, holding the reference to the Cat instance, are part of the thread’s stack.
Figure E.4 Adding and removing values from a stack. The stack is an ordered collection working on the LIFO principle. When you add a value to the stack it becomes the top layer—the only one you can remove.
Figure E.5 When the execution reaches the start of a block of code, a new layer is created in the thread’s stack. All the variables the block of code defines are stored in this new layer. The layer is removed when the block of code ends. This way, we know that the values in this part of the memory are released when they’re no longer needed. Besides variable x, the args parameter is also part of the stack in this case. I have simply taken it out to allow you focus on the local declared variables.
Figure E.6 Another block of code can be called from one in execution. In this case, method main() calls method a(). Since main() didn’t finish, its layer is still part of the stack. Method a() creates its own layer where the local values it defines are stored. Method names you see in the stack on the right side of they layers are simply to illustrate which method created which layer.
Figure E.7 When the execution reaches the end of a block of code, the stack layer opened for that block is removed with all the data it contains. In this case, when method a() returns, its stack layer is removed. This way, we make sure the unneeded data is removed from the memory.
Figure E.8 Just like with method a(), when method b() is called and the execution reaches the start of its block of code, a new layer is added to the stack. The method can use this layer to store local data until the method returns and the layer is removed.
Figure E.9 Every new execution of a method creates a new layer in the stack. In case of a recursion, a method may fill the stack if it’s called too many times. When the stack gets full, the app throws a StackOverflowError, and the current thread stops.
Figure E.10 The stack trace caused by a StackOverflowError. Usually, a StackOverflowError is easy to identify. The stack trace shows a method calling itself repeatedly or a group of methods that call each other, as in this example. You can go directly to these methods to figure out how they started infinitely calling each other.
Figure E.11 All threads use the same heap location. If one of the threads has an issue that causes the heap to become full (memory leak), another thread may signal the problem. This scenario happens quite often because the problem will be reported by the first thread unable to store data in the heap. Because any thread can signal the problem and it’s not necessarily the one causing the problem, heap-related issues are more challenging to solve.
Figure E.12 Any object in the heap can keep references to other objects in the heap. The GC can remove an object only when no reference to it exists.
Figure E.13 A variable in the stack can also refer to an instance in the heap, which cannot be removed until all its references are gone (including the ones in the stack).
Figure E.14 The metaspace is a memory location where the app stores the data types’ descriptors. It holds the blueprints used to define the instances stored in the heap.

FAQ

How does the JVM organize a Java app’s memory?The JVM uses three main areas: stacks (one per thread for local variables and parameters), a shared heap (for object instances and their instance fields), and the metaspace (for class/type metadata used to create and manage objects).
What data goes on the stack, and when is it removed?Each time execution enters a block/method, the thread creates a new stack layer (frame) for its local variables and parameters. The stack follows LIFO: the top frame is removed when the block returns, reaches its end, or throws an exception, automatically freeing those locals.
What is the heap and what is stored there?The heap is a memory area shared by all threads where object instances live, including their instance fields. References to these objects can be held by other heap objects or by variables in thread stacks.
How do thread stacks interact with the shared heap?Each thread has a private stack that can hold references to heap objects. While stacks aren’t shared, the heap is; any thread can allocate there. One thread’s behavior (e.g., leaking objects) can impact other threads by consuming the shared heap.
What causes a StackOverflowError, and what are its effects?A StackOverflowError occurs when a thread’s stack runs out of space, typically due to deep or infinite recursion creating too many frames. It stops only the affected thread. The stack trace usually shows a method calling itself (directly or indirectly) many times. Stack size is finite, though adjustable.
What is an OutOfMemoryError in the heap, and why might a different thread report it?An OutOfMemoryError occurs when the heap is full and a new allocation can’t be satisfied. The first thread that attempts an allocation and fails will report it, even if another thread’s leak actually filled the heap.
If Java has garbage collection, how can memory leaks still happen?GC reclaims only objects that are unreachable. If your app keeps references to objects that are no longer needed (for example, by retaining them in collections or static fields), those objects remain reachable and cannot be collected, accumulating until the heap fills.
How can collections and static variables lead to memory leaks?Collections that grow without removing entries keep references to their elements. Static variables live for the lifetime of the process; if they reference long-lived objects (like collections), everything reachable from them will also stay alive unless explicitly cleared.
Can references from the stack prevent garbage collection?Yes. While stack references are usually short-lived (frames are removed when a method ends), a long-lived or blocked stack frame (e.g., due to deadlock or a never-ending call) can keep referenced heap objects reachable, effectively preventing their collection.
What is the metaspace, and can it also run out of memory?The metaspace stores class/type metadata—the “blueprints” for heap instances. In rare cases it can fill up; if there’s no room for new type metadata, the JVM throws an OutOfMemoryError indicating the metaspace is full.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Troubleshooting Java, Second Edition ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Troubleshooting Java, Second Edition ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Troubleshooting Java, Second Edition ebook for free