1. An overview of the

Ever wonder why Java applications consume much more memory than specified through the well-known -XMS and -xMX tuning flags? The JVM can allocate additional native memory for a variety of reasons and possible optimizations. These additional allocations will eventually push the memory consumption beyond the -XMX limit.

In this tutorial, we’ll take a look at some of the common memory allocation sources in the JVM, along with their sizing flags, and then learn how to monitor them using native memory tracing.

2. Native allocation

The heap is usually the largest memory consumer in a Java application, but there are others. In addition to the heap, the JVM allocates a sizable chunk of native memory to maintain class metadata, application code, JIT-generated code, internal data structures, and so on. We will explore some of these allocations in the following sections.

2.1. Metaspace(Meta-space)

To maintain some metadata about loaded classes, the JVM uses a dedicated non-heap region called Metaspace. Before Java 8, it was called PermGen or Permanent Generation. Metaspace or PermGen contain metadata about loaded classes, not their instances, which are stored in the heap.

What is important here is that the heap size configuration does not affect the Metaspace size because Metaspace is an off-heap data area. To limit the Metaspace size, we use other tuning flags:

  • -xx: MetaspaceSize and -xx: MaxMetaspaceSize Set the minimum and maximum MetaspaceSize sizes
  • Prior to Java 8, -xx: PermSize and -xx: MaxPermSize set the minimum and maximum PermGen sizes

2.2. Threads

One of the most memory-intensive data areas in the JVM is the stack, which is created at the same time as each thread. The stack stores local variables and partial results and plays an important role in method calls.

The default thread stack size depends on the platform, but on most modern 64-bit operating systems it is about 1 MB. This size can be configured with the -xSS adjustment flag.

When there is no limit on the number of threads, the total memory allocated to the stack is effectively unlimited compared to other data areas. It is worth mentioning that the JVM itself requires several threads to perform its internal operations, such as GC or just-in-time compilation.

2.3. Code Cache

In order to run JVM bytecode on different platforms, it needs to be translated into machine instructions. When the program is executed, the JIT compiler is responsible for this compilation.

When the JVM compiles bytecode into assembly instructions, it stores these instructions in a special non-heap data area called the code cache. The code cache can be managed just like any other data area in the JVM. -xx: InitialCodeCacheSize and -xx: ReservedCodeCacheSize Adjust flags to determine the initial and possible maximum values for the code cache.

Garbage Collection

The JVM ships with several GC algorithms, each suitable for a different use case. All of these GC algorithms have one thing in common: they need to use some off-heap data structure to perform their tasks. These internal data structures consume more native memory.

2.5. Symbols

Let’s start with Strings, one of the most commonly used data types in application and library code. Because of their ubiquity, they usually take up a large portion of the heap. If a large number of these strings contain the same content, a large portion of the heap will be wasted.

To save some heap space, we can store one version of each String and have other versions reference the stored version. This procedure is called String Interning. Since the JVM can only compile time string constants internally, we can manually call the string intern method to get the internally compiled string.

The JVM stores the actual stored strings in natively special fixed-size hash tables called string tables, also known as string pools. You can run the -xx: StringTableSize command to adjust the flag configuration table size (that is, the number of buckets).

In addition to the string table, there is another native data area called the runtime constant pool. The JVM uses this pool to store constants, such as numeric literals at compile time or method and field references that must be parsed at run time.

2.6. Native Byte Buffers

JVMS are often suspected of allocating large amounts of native memory, but sometimes developers can also allocate native memory directly. The most common methods are malloc, which is called by JNI, and ByteBuffers, which are directly callable in NIO.

Additional Tuning Flags

In this section, we use a small number of JVM tuning flags for different optimization scenarios. Using the following tips, we can find almost all tuning flags related to a particular concept:

$ java -XX:+PrintFlagsFinal -version | grep <concept>
Copy the code

PrintFlagsFinal prints all -xx options in the JVM. For example, to find all flags associated with Metaspace:

$ java -XX:+PrintFlagsFinal -version | grep Metaspace
      // truncated
      uintx MaxMetaspaceSize                          = 18446744073709547520                    {product}
      uintx MetaspaceSize                             = 21807104                                {pd product}
      // truncated
Copy the code

3. Native Memory Tracing (NMT)

Now that we know the common sources of native memory allocation in the JVM, it’s time to figure out how to monitor them. First of all, we should use another JVM tuning symbol to enable native memory tracking: – XX: NativeMemoryTracking = off | sumary | the detail. By default, NMT is turned off, but we can make it see a summary or detailed view of its observations.

Suppose we want to track native allocation for a typical Spring Boot application:

$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseG1GC -jar app.jar
Copy the code

Here, we enable NMT while allocating 300 MB of heap space, G1 as our GC algorithm.

3.1. Instance Snapshot

With NMT enabled, we can obtain native memory information at any time using the JCMD command:

$ jcmd <pid> VM.native_memory
Copy the code

To find the PID of the JVM application, we can use the JPS command:

$ jps -l                    
7858 app.jar // This is our app
7899 sun.tools.jps.Jps
Copy the code

Now, if we use JCMD with an appropriate PID, vm. native_memory causes the JVM to print out information about native allocation:

$ jcmd 7858 VM.native_memory
Copy the code

Let’s analyze the NMT output section by section.

3.2. The total distribution

NMT reports full reserved and committed memory as follows:

Native Memory Tracking:
Total: reserved=1731124KB, committed=448152KB
Copy the code

Reserved memory represents the total amount of memory that our application might use. Instead, committed memory represents the amount of memory our application is currently using.

Despite the allocation of 300MB of heap, our application’s total reserved memory was almost 1.7GB, far more than that. Similarly, the committed memory was about 440 MB, which again is well over 300 MB.

After understanding the whole, NMT reports the memory allocation for each allocation source. So, let’s dig into each source.

3.3. Heap (Heap)

NMT reports heap allocation as we expect:

Java Heap (reserved=307200KB, committed=307200KB)
          (mmap: reserved=307200KB, committed=307200KB)
Copy the code

300 MB of reserved and committed memory, matching our heap size Settings.

3.4. Metaspace (Meta-space)

Here is the NMT report on the metadata of the loaded class:

Class (reserved=1091407KB, committed=45815KB)
      (classes # 6566)
      (malloc=10063KB # 8519)
      (mmap: reserved=1081344KB, committed=35752KB)
Copy the code

Almost 1 GB is reserved, and 45 MB is reserved to load 6566 classes.

3.5. Threads

Here is the NMT report on thread allocation:

Thread (reserved=37018KB, committed=37018KB)
       (thread # 37)
       (stack: reserved=36864KB, committed=36864KB)
       (malloc=112KB # 190)
       (arena=42KB # 72)
Copy the code

A total of 36 MB of memory was allocated to the stack of 37 threads – about 1 MB per stack. The JVM allocates memory to threads at creation time, so reserved and committed allocations are equal.

3.6. Code Cache

Let’s look at NMT’s report on JIT-generated and cached assembly instructions:

Code (reserved=251549KB, committed=14169KB)
     (malloc=1949KB # 3424)
     (mmap: reserved=249600KB, committed=12220KB)
Copy the code

Currently, approximately 13 MB of code is being cached, and the amount could reach 245 MB.

3.7. The GC

Here is the NMT report on G1 GC memory usage:

GC (reserved=61771KB, committed=61771KB)
   (malloc=17603KB # 4501)
   (mmap: reserved=44168KB, committed=44168KB)
Copy the code

As we can see, both reserved and committed are close to 60 MB dedicated to helping G1.

Let’s look at the memory usage of simpler GC, such as the Serial GC:

$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseSerialGC -jar app.jar
Copy the code

Serial GC uses almost less than 1 MB:

GC (reserved=1034KB, committed=1034KB)
   (malloc=26KB # 158)
   (mmap: reserved=1008KB, committed=1008KB)
Copy the code

Obviously, we can’t choose a GC algorithm just because of its memory usage, as the paused collection nature of serial GC can lead to performance degradation. However, there are several GCS to choose from, each balancing memory and performance.

3.8. Symbol

Here are NMT reports on symbol assignment, such as string tables and constant pools:

Symbol (reserved=10148KB, committed=10148KB)
       (malloc=7295KB # 66194)
       (arena=2853KB # 1)
Copy the code

Nearly 10 MB is allocated to symbols.

3.9. NMT over time

NMT allows us to track how memory allocation changes over time. First, we should mark the current state of the application as a baseline:

$ jcmd <pid> VM.native_memory baseline
Baseline succeeded
Copy the code

Then, after a while, we can compare our current memory usage to that baseline:

$ jcmd <pid> VM.native_memory summary.diff
Copy the code

NMT uses the + and – symbols to tell us how memory usage has changed in the meantime:

Total: reserved=1771487KB +3373KB, committed=491491KB +6873KB
-             Java Heap (reserved=307200KB, committed=307200KB)
                        (mmap: reserved=307200KB, committed=307200KB)

-             Class (reserved=1084300KB +2103KB, committed=39356KB +2871KB)
// Truncated
Copy the code

Total reserved and committed memory increased by 3 MB and 6 MB, respectively. Other fluctuations in memory allocation can easily be detected.

3.10. Detailed NMT

NMT can provide very detailed information about the mapping of the entire storage space. To enable this detailed report, we should use the -xx: NativeMemoryTracking =detail message to adjust the flag.

4. The conclusion

In this article, we have listed the different users of native memory allocation in the JVM. Then, we learned how to check running applications to monitor their native allocation. With this in mind, we can more effectively resize our applications and runtime environments.

Original: www.baeldung.com/native-memo…

By Ali Dehghani

Translator: Emma