I have a Java application running on Liberica JDK 8 (HotSpot VM, G1 GC) on an Oracle Linux machine with 24 GB RAM. The application has -Xmx15g
max heap size, utilizes it heavily (due to its load profile) and is the only process with such demands on the server.
From time to time (usually after dozens of uptime hours), the application gets killed by Linux oom-killer. To find the root cause, I enabled JVM Native Memory Tracking (NMT) in detailed mode, established the baseline soon after warming up, and gathered the following stats for 34 uptime hours (right before the process was killed once more):
According to the Oracle's NMT Memory Categories documentation, I expected the heaviest Internal category to be filled with something like Direct ByteBuffers. However, the NMT details shown that almost 70% of those 3 GB of Internals are composed of allocations like this:
[0x00007faeb7f9b5b5] BitMap::BitMap(unsigned long, bool)+0x1d5
[0x00007faeb82ca08f] OtherRegionsTable::add_reference(void*, int)+0x57f
[0x00007faeb82e0f40] InstanceKlass::oop_oop_iterate_nv(oopDesc*, FilterOutOfRegionClosure*)+0xc0
[0x00007faeb82c3373] HeapRegion::oops_on_card_seq_iterate_careful(MemRegion, FilterOutOfRegionClosure*, signed char*)+0x163
(malloc=1154790KB type=Internal +846638KB #577395 +423319)
In total, there were 8 such blocks with oops_on_card_seq_iterate_careful(…)
method invocations having various classes on the next stack frame, e.g.:
InstanceRefKlass
InstanceKlass
ObjArrayKlass
Basing on these identifiers, I found out that these routines are a part of G1 GC. However, I couldn't see a way to influence their behavior (memory consumption) from G1 parameters available among corresponding JVM options.
Looking at this related SO answer, I've tried to increase the -XX:G1HeapRegionSize
from its ergonomically computed 4 MB to manually set 8 MB, but no significant changes were observed.
So the questions are:
Why purely G1-related activities are recorded by NMT into Internal category? (not GC)
NMT does not know it's G1-related activity: it does not walk the stack to find out the allocation type, it just uses the type passed to the allocation function.
As you can see in the stack trace, the allocation happens in the BitMap
constructor. This is a general purpose class used in many places, not just GC. BitMap
class has an allocator associated with mtInternal
type:
ArrayAllocator<bm_word_t, mtInternal> _map_allocator;
In newer JDK versions, BitMap has a variable allocation type passed from outside.
Is there a way to make G1 consume less native memory?
Migrate to JDK 17 or newer. G1GC got tons of improvements that will never be backported to JDK 8. Tuning GC in JDK 8 is a goldmine for performance consultants, but if you care about time and resources - upgrading JDK is the best investment.