A Deep Dive into the JVM Memory Model: How Heap, Stack, and Metaspace Function and Fail.

Effective troubleshooting, as well as creating memory-friendly system designs, require a good understanding of the Java memory model. Since the most common memory areas involved in runtime problems are the heap, the stack and the metaspace, it’s really important to understand what they’re for, how they work and how and where things can go wrong.

In this article, we’ll look in depth at each of these three memory areas, discuss some of the common issues encountered in each, and include some hints on how to diagnose these problems.

The Java Memory Model: A Brief Overview

 We can visualize the JVM memory model like this:

Fig: The Java Memory Model

Briefly, the main areas are:

  • Heap Memory: It is split into the Young Generation and the Old Generation to maximize garbage collection efficiency. This is a central storage area for all objects created in the application.
  • Metaspace: This stores details of classes (as opposed to objects created from classes.)
  • Thread space: This stores a stack for each running thread.
  • Code cache: Precompiled machine code for frequently-used methods (hot spots) is stored here.
  • Direct Buffers: Applications can use direct buffers in native memory for faster I/O operations.
  • GC: The garbage collector reserves this for its own use.
  • JNI: If the program uses the Java Native Interface, this area is used.
  • Misc: The JVM requires some memory for internal use, for example, to store symbol tables.

To read more about memory errors, see Common Memory Leaks in Java and How to Fix Them.

There are, in fact, 9 types of OutofMemoryError in Java

For more information on Java memory structure, watch JVM Explained in 10 Minutes.

1. The Java Heap

The heap is used by all classes that make up the application. It stores objects and their component variables. It’s split into smaller areas to make garbage collection more efficient. According to the Generational Hypothesis, most objects in memory are short-lived. For example, a customer’s online request to place an order stores several pieces of information, but as soon as the request has been processed, this information is no longer needed.

The JVM therefore splits the heap into two main areas: 

  • The Young Generation, which holds newly-created objects, and is garbage-collected frequently. Since it’s small, this happens very quickly. This cycle is known as a minor GC event.
  • The Old Generation holds objects that have survived several cycles of garbage collection. It’s much bigger, and is cleaned less frequently. This cycle is known as a full (or major) GC event.

What Can Cause Memory Issues in the Heap?

Essentially, memory problems in the heap have one of five main causes:

  • The program has a memory leak, where unnecessary objects are building up in memory over time;
  • Memory is being wasted by poor coding practices;
  • The heap size is not configured correctly;
  • Garbage collection is incorrectly tuned;
  • The device or container has insufficient memory. This can result in the application being killed by the operating system.

What are the Symptoms of Memory Issues in the Heap?

The most obvious symptom is a heap-related OutOfMemoryError. There are two possible error messages:

In fact, these two errors can occur interchangeably, and both signify that the heap has run out of memory.

Less obvious symptoms occur when memory is still available, but the garbage collector is overworking to clear up memory. They include:

  • Poor response times;
  • System hangs, which resolve after a while;
  • Transaction timeouts;
  • Poor throughput.

Lastly, if the program is repeatedly requesting more heap space, there is a chance it may be silently killed by the device or container. On investigation, the kernel log will contain an error along these lines: Kill Process or Sacrifice Child.

Troubleshooting Heap Space Issues

The first thing we need for diagnosing heap issues is a heap dump. This is a snapshot of the heap at a given moment. Since it’s a large binary file, we need a heap dump analyzer such as HeapHero or Eclipse MAT to extract meaningful information from it.

To see a demonstration of solving memory issues with HeapHero, see this video: How to Analyze Heap Dumps Fast.

The next important source of information is the garbage collection logs. Again, the logs are rather long, and we need a GC Log Analyzer such as GCeasy or GCViewer to convert the logs to meaningful information. For a demonstration, see this video: Reading GC Logs.

2. The Stack

We can visualize the JVM Thread Space like this:

Fig: JVM Stack Space

For each running thread, a stack is created, and removed when the thread completes. Each stack consists of a series of frames and a program counter pointing to the next instruction in sequence. When the thread calls a method, a frame is created for it and pushed onto the stack. It’s removed, or ‘popped’, from the stack when the method completes. Each frame includes the following:

  • Primitive local variables;
  • Pointers to object local variables in the heap;
  • A return address;
  • A pointer to the method’s area in the constant pool

What Can Cause Problems in Thread Space?

Two errors that are sometimes seen relating to the thread space are:

  • StackOverflowError. This occurs when a thread’s stack exceeds the maximum space per thread, which is configured in the JVM using the argument -Xss<size>. The most common reason for the error is that the program is in a loop, recursively calling methods. It can also occasionally occur because the maximum size is under-configured.
  • OutOfMemoryError: Unable to create new native threads. This can happen for several reasons when the program has a large number of threads. It may be due to:
    •  A program bug that creates threads within a loop that fails to terminate;
    • The stack size being over-configured;
    • Lack of memory in the device or container;
    •  The number of threads exceeding the operating system limit.

Poor management of threads, competition for resources and long waits for I/O operations can affect application performance.

What are the Symptoms of Memory Issues in the Heap?

The errors mentioned above may be seen in the application log, and in most cases, they will crash the system.

Issues relating to threads also include high CPU usage, program hangs or slow response times.

Troubleshooting Thread Issues

When a Java application crashes, it usually prints the stack trace of the thread that caused the error. This is always the first step in troubleshooting. The stack trace includes the name of the thread, followed by the method and line number within the source file at which it crashed. It then prints a line for each entry in the stack, making it possible to trace the program path backwards through the execution path.

The stack trace looks like this:

Fig: A Stack Trace

For more complex issues, we need a thread dump. This is a text file, but it can be quite long, so it’s best to analyze it with a tool such as fastThread or JstackReview. See this video for a demonstration of using fastThread to analyze a thread dump.

3. The Metaspace

The metaspace holds class metadata. This includes information about the class, class definitions and method definitions. It does not include objects created from the class, which are always stored in the heap.

In Java versions prior to Java 8, this information was stored in an area called the PermGen (permanent generation).

We can configure the maximum size of the metaspace using the JVM argument – XX:MaxMetaspaceSize. 

What Can Cause Problems in the Metaspace?

If the metaspace expands beyond the maximum size, the JVM will throw the error java.lang.OutOfMemoryError: Metaspace. This can happen for several reasons:

  • The program is creating too many classes:
    • Third-party libraries create a large number of classes;
    • The program is creating a large number of dynamic classes. This is likely to be when using scripting languages such as Groovy, or using  Java Reflection.
  • The program is using a large number of class loaders. Web server environments such as Tomcat are at risk for this. They run each Webapp via a separate loader in order to isolate their class environment. If either the loader, any of its classes, or any objects created from them are not properly dereferenced, it results in a memory leak.

With this type of application, if the maximum size is not set, the metaspace can keep expanding until the device or container runs out of memory. This may result in the application being killed by the operating system, or hanging the device.

What are the Symptoms of Issues in the Metaspace?

The program may crash with the error error java.lang.OutOfMemoryError: Metaspace, or the application may be silently killed by the operating system if the device runs low on memory.

However, before the problem reaches this stage, the application may experience:

  • Slow response time;
  • High CPU usage due to full garbage collection cycles running more frequently;
  • Performance within the device or container degrading.

Troubleshooting Metaspace Issues

There are several ways we can obtain information about what classes are loaded in the metaspace.

One way is to use the JVM argument -verbose class, which displays each class name as it’s loaded. In Java 9 and above, we have the option to write this information to a file using the argument  -Xlog:class+load=info:<file name>. 

This produces a text file which may look like this:

Fig: Text File Produced Using Verbose JVM Option

This information can be very long and tedious to scroll through. An easier way is to use a heap dump analyzer. Most of these tools include a class histogram, which consolidates and arranges the information.

Fig: Histogram Produced by HeapHero

For more information on troubleshooting the metaspace, watch this video: Inspect the Contents of the Metaspace.

Conclusion

For effective troubleshooting, we need a good understanding of the Java memory model, especially the problem-prone areas such as the heap, the stack and the metaspace.

Tools such as HeapHero, GCeasy and fastThread can be invaluable when debugging memory-related problems.

Share your Thoughts!

Up ↑

Index

Discover more from HeapHero – Java & Android Heap Dump Analyzer

Subscribe now to keep reading and get access to the full archive.

Continue reading