4

We profiling our application with Java Flight Recorder and found massive locks on java.lang.ref.Reference$Lock object.

I investigate some places in stacktraces and findout that in all cases - there is array allocation

enter image description here

example of code ( position 3 on image):

    public static char[] copyOfRange(char[] original, int from, int to) {
        int newLength = to - from;
        if (newLength < 0)
            throw new IllegalArgumentException(from + " > " + to);

        // stacktrace points on next line 
        char[] copy = new char[newLength];

        System.arraycopy(original, from, copy, 0,
                         Math.min(original.length - from, newLength));
        return copy;
    }

I'm suspect that such locking has something with GC, but can't found any relevant information. Where I can read more on this topic?

End goal of activity: understand what is pappenning in such case, witch factors afect this and how we can decrease lock time of such operations.

Some details from comments:

  1. Java 8
  2. Heap 512Mb
  3. GC - G1
  4. With experiments I find out, that locktime is decreasing with heap size increase.
cgi
  • 190
  • 1
  • 8
  • Which GC and JVM version? – apangin Jan 28 '21 at 12:57
  • Another question. While I certainly appreciate that you want to understand what's causing this, do you have an actual performance problem? – Erik Jan 28 '21 at 12:59
  • Java 8, GC - G1, heap size - 512Mb – cgi Jan 28 '21 at 14:30
  • Yes, I think I have problem - Top 1 locks time in this lock – cgi Jan 28 '21 at 14:32
  • 1
    The garbage collector acquires this lock, so when you find that increasing heap size reduces that time, you may simply have a problem of excessive garbage collection, in other words, your heap size is too small for what you are doing. Instead of looking at the methods and locks, look at the ratio of garbage collection vs application code CPU time. – Holger Jan 28 '21 at 15:25
  • `java.lang.Reference$Lock` is a taken when GC works on some kind of weak references, soft, phantom or finalizers. It's either your code or some libraries that you use that make use of that – Eugene Jan 28 '21 at 16:24
  • 1
    @Eugene as far as I know, the garbage collector will always acquire this lock when starting its work. – Holger Jan 28 '21 at 16:52
  • @Holger I thought it is only taken when there are references to process – Eugene Jan 28 '21 at 18:06

1 Answers1

5

A big shortcoming of Java Flight Recorder is that it shows only Java stacks, completely ignoring the native and the VM part.

async-profiler is much more accurate in this sense. If you run it in lock profiling mode with native stacks on, it will show you exact place in the JVM where these locks are acquired. Example command:

./profiler.sh -d 60 -e lock --cstack fp -f profile.html -o flamegraph=total PID
  • -d 60 run profiling for 60 seconds
  • -e lock profile lock contention
  • --cstack fp record C (native) stacks
  • -f profile.html output file name (HTML format in async-profiler 2.0, or SVG in 1.x)
  • -o flamegraph=total build the flame graph using the total lock wait time as a counter
  • PID Java process ID

Lock profile

In this example, the Flame Graph highlighted lock contention on a Reference$Lock instance. Java part of the stack traces is displayed in green. This matches the stack traces you've seen in JFR. Like in your case, the top Java frame is Arrays.copyOfRange (the graph shows other stacks as well, but let's focus on the first one).

The yellow part is the native C++ code. Let me explain what happens there.

  1. Arrays.copyOfRange calls the VM runtime function OptoRuntime::new_array_nozero_C. The actual array allocation happens in C++ code of the JVM.

  2. The JVM fails to allocate an array from existing Thread Local Allocation Buffer (TLAB) and then falls back to a slow path allocation of a new TLAB.

  3. The slow path allocation doesn't succeed either, because there is no enough free memory in Java Heap. So, the JVM synchronously invokes Garbage Collector.

  4. In the GC prologue, the JVM tries to acquire the lock that protects the list of pending references. This is to ensure that ReferenceHandler thread leaves the critical section before the GC starts. While holding this lock, the JVM can safely append newly discovered weak references to the pending list.

  5. However, the lock is already acquired by another thread that has concurrently attempted to invoke Garbage Collector the same way. The current thread is suspended until GC finishes.

To sum it up, multiple Java threads concurrently attempted to allocate an object from the Heap, but the Heap was full. So, the Garbage Collection started, and the allocating threads were blocked on Reference$Lock - the reference pending list lock.

The contention on Reference$Lock is not a problem per se. The allocating threads cannot proceed anyway, until GC reclaims enough memory. The actual issue is that concurrent garbage collection does not keep up with the allocation rate.

To mitigate the problem, try one or more of the following approaches:

  • increase the heap size;
  • reduce the allocation rate;
  • increase the number of concurrent GC threads - ConcGCThreads;
  • lower InitiatingHeapOccupancyPercent to start concurrent GC cycle earlier;

Increasing the heap will be probably the most efficient.

BTW, async-profiler has other useful modes to diagnose GC related issues:

  • -e cpu shows what takes the most CPU time. Java and VM threads are displayed together on the same graph, so you can get an idea whether GC activity is too high comparing to the application work.
  • -e alloc shows the code which allocates most. It's especially useful when investigating how to reduce the allocation rate.
apangin
  • 92,924
  • 10
  • 193
  • 247
  • 1
    pretty darn nice. we only recently started using async-profiler (just a tiny bit) and it's just insane the amount of insights we got within a couple of hours from its usage. We sincerely thank you for it. – Eugene Jan 29 '21 at 03:17
  • 1
    I wish I could use it under Windows… – Holger Jan 29 '21 at 12:54
  • @Holger Not yet, sorry. I wish Windows was POSIX :) – apangin Jan 29 '21 at 14:44