JVM Memory Management
In this article, we will explore some the less known JVM memory regions. Most of us are familiar with heap, which is where all the objects and string pool lives. There are many tools and resources which can help in profiling the heap. Whereas profiling non-heap regions, could be bit more involved.
By enabling native memory tracking, we could get a sense of how different regions contribute to the full memory used by the JVM. Total JVM memory is a combination of the following
Total memory = Heap + GC + Metaspace + Code Cache + Symbol tables
+ Thread stacks + Compiler + Other JVM structures +
+ Direct buffers + Mapped files +
+ Native libraries allocations + Malloc overhead
Metaspace
Metaspace contains a runtime representation of the classes, which are loaded into the JVM. This includes Klass structure, method metadata, static primitives, static object references, etc. The default max metaspace config value per JVM is unlimited. This can be controlled through -XX:MaxMetaspaceSize argument.
The allocated metaspace for a class is owned by its class loader, so it is released only when corresponding class loader itself is unloaded. When metaspace reaches a certain max threshold, GC would be attempted to remedy the situation.
Code Cache
This is mostly used by the JIT. For the frequently executing code blocks, when JIT generates native code from the bytecode, the native code is cached in the code cache. The default ReservedCodeCacheSize is 240MB. When it is full, JIT is disabled. This behavior can be controlled through the following property.
UseCodeCacheFlushing— default false
When this property is enabled, JVM will keep track of the hotness counter for the precompiled code blocks and clears the non-hot cache caches when certain memory thresholds are met.
Symbol tables
Also referred to as constant pool. This area contains static constants, class names, method names, method descriptions, field names, and interned strings. Typically, this is small compared to other regions.
Excessive symbol memory usage can be an indicator that Strings have been interned too aggressively.
Threads
The default behavior of the JVM is to allocate 1MB for each thread stack. So, the memory impact should be a consideration while adding more threads to a JVM. The stack size for a thread can be tuned with the -Xss argument. All local variables(primitives) of a method are stored in the stack. Stack will also have references/pointers to the objects in heap.
GC Area
Objects in heap, which are not reachable through a reference from stack or metaspace(statics) are eligible for garbage collection. GC algorithms manage this information through card tables, references, etc. The amount of native memory used by GC itself could vary based on the GC algorithm.
DirectByteBuffers
DirecByteBuffers are appealing for IO use cases that require faster throughput. For the DirectByteBuffers, there are no underlying arrays, so it could be accessed directly through a pointer in native code. When a DirectByteBuffer object is created, it allocates native memory equal to the buffer capacity.
Since OS’es can only work with native memory, even for the HeapByteBuffer use cases, JVM will create a temporary DirectByteBuffer. So, use cases, that create many threads which perform I/O contribute to a large native memory pressure.
These DirectByteBuffers are cached per thread. Cache size of the temporary per-thread buffer could be controlled through maxCachedBufferSize property. Its default value is unlimited.
Others
There are also few other contributors to the memory, Such as the compiler itself, native allocations, and overhead from mmap and malloc, etc. These are relatively fixed in size per JVM.