如何找出哪个线程持有监视器?

31

我的应用程序正在使用 Gson 2.2 来将 POJOs 转换为 JSON。当我进行负载测试时,我发现有很多线程在 Gson 构造函数中被阻塞:

"http-apr-28201-exec-28" #370 daemon prio=5 os_prio=0 tid=0x0000000001ee7800 nid=0x62cb waiting for monitor entry [0x00007fe64df9a000]
    java.lang.Thread.State: BLOCKED (on object monitor)
    at com.google.gson.Gson.<init>(Gson.java:200)
    at com.google.gson.Gson.<init>(Gson.java:179)

线程转储未显示任何持有 [0x00007fe64df9a000] monitor 的线程。 我该如何找出谁拥有它?

Gson 代码在第200行看起来相当无害:

// built-in type adapters that cannot be overridden
factories.add(TypeAdapters.STRING_FACTORY);
factories.add(TypeAdapters.INTEGER_FACTORY);

我在Linux上使用JRE 1.8.0_91


1
你尝试过Eclipse MAT吗? - fhofmann
你能发布完整的线程转储吗? - Matt McHenry
@MitjaGustin 通过 Servlets - bedrin
@MattMcHenry 我不确定我能够这样做 - 无论如何,由于我的雇主政策,我都必须将自己的类从中删除。 - bedrin
很可能factories.add是同步的或者持有一个监视器,或者它调用了其他持有监视器的代码。这可能会给你一个开始查找的地方。我需要知道factories的类型并且可能需要它的代码才能提供更详细的建议。 - Warren Dew
显示剩余7条评论
2个回答

16
我认为你遇到了与GC相关的行为,其中线程被置于等待状态以允许垃圾收集。简而言之。
I do not have the whole truth but I hope to provide some pieces of insight. 首先需要认识到的是,方括号中的数字“[0x00007fe64df9a000]”并不是显示器的地址。在转储中,所有线程都可以看到方括号中的数字,即使是运行状态的线程也一样。这个数字也不会改变。以下是我测试转储的示例:
main" #1 prio=5 os_prio=0 tid=0x00007fe27c009000 nid=0x27e5c runnable [0x00007fe283bc2000]
   java.lang.Thread.State: RUNNABLE
        at Foo.main(Foo.java:12)

我不确定这个数字的含义,但this page表明它是Java VM内部线程结构的指针。除非你正在调试实时Java VM或核心文件,否则通常没有兴趣。尽管所解释的跟踪格式有点不同,所以我不确定我是否正确。当显示实际监视器的地址时,转储的方式如下:
"qtp48612937-70" #70 prio=5 os_prio=0 tid=0x00007fbb845b4800 nid=0x133c waiting for monitor entry [0x00007fbad69e8000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:233)
        - waiting to lock <0x00000005b8d68e90> (a java.lang.Object)

请注意跟踪中的waiting to lock行,以及监视器的地址与括号中的数字不同。
我们无法看到涉及的监视器地址表明该监视器仅存在于本地代码中。
其次,所涉及的Gson代码根本没有包含任何同步。 该代码只是向一个ArrayList添加一个元素(假设没有进行字节码操作并且在低级别上没有做任何可疑的事情)。 也就是说,在此调用时看到线程等待标准同步监视器是没有意义的。
我发现someindications,当有大量GC正在进行时,线程可能会显示为等待监视器条目。
我编写了一个简单的测试程序,尝试通过向数组列表中添加大量元素来重现它:
List<String> l = new ArrayList<>();
while (true) {
    for (int i = 0; i < 100_100; i++) {
            l.add("" + i);
    }
    l = new ArrayList<>();
}

然后我对这个程序进行了线程转储。偶尔我遇到了以下跟踪:

"main" #1 prio=5 os_prio=0 tid=0x00007f35a8009000 nid=0x12448 waiting on condition [0x00007f35ac335000]
   java.lang.Thread.State: RUNNABLE
      at Foo.main(Foo.java:10)   <--- Line of l.add()

虽然与OP的跟踪不完全相同,但有趣的是,在没有涉及同步的情况下,有一个线程“等待条件”。我发现在堆较小的情况下更频繁出现,这表明它可能与GC有关。另一个可能性是包含同步的代码已被JIT编译,这会阻止您看到监视器的实际地址。但是,我认为这种可能性较小,因为您在ArrayList.add上遇到了这种情况。如果是这种情况,我不知道如何找到监视器的实际持有者。

有趣!我同意 - 看起来这种行为是由GC引起的。特别是因为我正在进行负载测试。我会尝试从VM获取更多遥测数据来证明它。 - bedrin
你是对的!我已经开始了针对带有分析器的服务器的负载测试,并看到了大量由大量请求引起的主要GC事件(这就是为什么我们需要负载测试!)。我认为,如果我增加了负载,最终会出现“GC超过限制”的异常,但显然我的负载还不够大。 - bedrin

-1
如果您没有GC(垃圾回收)问题,那么可能实际上有一些线程已经获取了对象锁并且被卡住了,等待获取同一个对象的锁。找出方法是查找...
- waiting to lock <some_hex_address> (a <java_class>)

例子可以是这样的

- waiting to lock <0x00000000f139bb98> (a java.util.concurrent.ConcurrentHashMap)

在线程转储中,有一个条目说:“等待监视器条目”。一旦找到它,您可以搜索已经锁定具有地址<some_hex_address>的对象的线程,它可能看起来像这样(例如):

- locked <0x00000000f139bb98> (a java.util.concurrent.ConcurrentHashMap)

现在,您可以查看该线程的堆栈跟踪以确定哪行代码已获取它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接