java.lang.ref.Reference$Lock 上数组分配的 JVM 中的大量锁

Massive locks in JVM on arrays allocations on java.lang.ref.Reference$Lock

我们使用 Java Flight Recorder 分析我们的应用程序,发现 java.lang.ref.Reference$Lock 对象上有大量锁。

我调查了堆栈跟踪中的一些地方,发现在所有情况下都有数组分配

代码示例(图像上的位置 3):

    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;
    }

我怀疑这种锁定与GC有关,但找不到任何相关信息。 我在哪里可以阅读更多关于这个主题的信息?

activity 的最终目标:了解在这种情况下发生了什么, 因素会影响这种情况,以及我们如何减少此类操作的锁定时间。

评论中的一些细节:

  1. Java 8
  2. 堆 512Mb
  3. GC - G1
  4. 通过实验我发现,锁定时间随着堆大小的增加而减少。

Java Flight Recorder 的一大缺点是它只显示 Java 堆栈,完全忽略了本机和 VM 部分。

async-profiler 在这个意义上要准确得多。如果您 运行 它处于 lock 分析模式并启用本机堆栈,它将向您显示 JVM 中获取这些锁的确切位置。示例命令:

./profiler.sh -d 60 -e lock --cstack fp -f profile.html -o flamegraph=total PID
  • -d 60 运行 分析 60 秒
  • -e lock 配置文件锁争用
  • --cstack fp 记录 C(原生)堆栈
  • -f profile.html 输出文件名(HTML async-profiler 2.0 格式,或 1.x 中的 SVG)
  • -o flamegraph=total使用总锁等待时间作为计数器构建火焰图
  • PID Java 进程 ID

在此示例中,火焰图突出显示了 Reference$Lock 实例上的锁争用。 Java 部分堆栈跟踪显示为绿色。这与您在 JFR 中看到的堆栈跟踪相匹配。与您的情况一样,顶部 Java 帧是 Arrays.copyOfRange(该图也显示了其他堆栈,但让我们关注第一个)。

黄色部分是原生C++代码。让我解释一下那里发生了什么。

  1. Arrays.copyOfRange 调用 VM 运行time 函数 OptoRuntime::new_array_nozero_C。实际的数组分配发生在 JVM 的 C++ 代码中。

  2. JVM 无法从现有线程本地分配缓冲区 (TLAB) 分配数组,然后回退到新 TLAB 的慢速路径分配。

  3. 慢速路径分配也不成功,因为Java 堆中没有足够的空闲内存。因此,JVM 同步调用垃圾收集器。

  4. 在 GC 序言中,JVM 尝试获取保护挂起引用列表的锁。这是为了确保 ReferenceHandler 线程在 GC 开始之前离开临界区。在持有这个锁的同时,JVM 可以安全地将新发现的弱引用附加到挂起列表。

  5. 但是,该锁已被另一个同时尝试以相同方式调用垃圾收集器的线程获取。当前线程挂起,直到 GC 完成。

综上所述,多个 Java 线程并发尝试从堆中分配一个对象,但堆已满。因此,垃圾收集开始,分配线程被阻塞在 Reference$Lock - 引用挂起列表锁上。

Reference$Lock 上的争用本身不是问题。分配线程无论如何都无法继续,直到 GC 回收足够的内存。实际问题是并发垃圾收集跟不上分配率

要缓解此问题,请尝试以下一种或多种方法:

  • 增加堆大小;
  • 降低分配率;
  • 增加并发 GC 线程数 - ConcGCThreads;
  • 降低 InitiatingHeapOccupancyPercent 以更早地开始并发 GC 周期;

增加堆可能是最有效的。

顺便说一句,async-profiler 还有其他有用的模式来诊断 GC 相关问题:

  • -e cpu 显示花费最多 CPU 时间的内容。 Java 和 VM 线程一起显示在同一个图表上,因此您可以了解 GC activity 与应用程序工作相比是否过高。
  • -e alloc 显示分配最多的代码。这在研究如何降低分配率时特别有用。