妖精陷阱:线程转储分析模式


这个世界充满了模式。

从一粒盐到复杂的宇宙,到处都有模式。真正的模式经得起时间的考验。在不断变化的世界里,它们是不变的。它需要大量的艰苦劳动、丰富的经验(包括好的和坏的)、激光聚焦和毅力来从噪音中结晶和创造图案。幸运的是,我们的计算世界有幸创造了如此美妙的模式。

一个经典的例子是软件设计模式:单例、工厂、访问者、观察者、纪念品等等。由埃里希·伽马、理查德·赫尔姆、拉尔夫·约翰逊和约翰·弗利西德斯创作。这四位工程师总结了他们多年来的经验教训,完善了它们,并将它们作为易于理解的模式传递给了我们。这些模式是普遍适用的,不管你使用什么编程语言(Java、C、C++、PHP、Ruby等等)。),不管你运行什么样的技术堆栈(JEE,)。NET、LAMP等。),无论您构建什么类型的应用程序(移动、网络、SOA、微服务、批处理等)。)。

线程转储分析模式

在这四位伟大工程师的启发下,在我有限的能力范围内,我以一种谦卑的方式,通过我多年的生产斗争,结晶、提炼并创造了线程转储分析模式。

线程转储是根本原因分析的重要组成部分。你的应用突然变得没有反应了吗?您的应用程序的中央处理器在没有增加流量、没有进行任何代码更改或任何环境更改的情况下开始激增了吗?您的应用程序的响应是否开始下降?您的应用程序在运行数天/数周后是否开始出现内存问题?线程转储中提供了几个这样复杂问题的答案。但是它们被埋藏在堆积如山的细节中。为了阐明这些隐藏的答案,我创建了线程转储分析模式。

在这篇文章中,让我向你介绍“妖精陷阱”模式。在垃圾收集期间,具有finalize()方法的对象与没有这些方法的对象受到不同的处理。在垃圾收集期间,带有finalize()的对象不会被立即从内存中逐出。相反,作为第一步,这些对象被添加到java.lang.ref.Finalizer对象的内部队列中。

有一个低优先级的JVM线程,名为终结器执行队列中每个对象的finalize()方法。只有在执行finalize()之后,对象才有资格进行垃圾收集。由于finalize()的实现不佳,如果终结器线程被阻塞,那么它将对JVM产生严重的、有害的级联效应。

如果终结器被阻塞,那么java.lang.ref.Finalize的内部队列将开始增长。这会导致JVM的内存消耗快速增长。这将导致OutOfMemoryError,危及整个JVM的可用性。因此,在分析线程转储时,强烈建议研究终结器线程的堆栈跟踪。

现实世界的例子

下面是一个终结器被阻塞的线程:

"Finalizer" daemon prio=10 tid=0x00007fb2dc32b000 nid=0x7a21 waiting for monitor entry [0x00007fb2cdcb6000]
   java.lang.Thread.State: BLOCKED (on object monitor)
at net.sourceforge.jtds.jdbc.JtdsConnection.releaseTds(JtdsConnection.java:2024)
- waiting to lock 0x00000007d50d98f0 (a net.sourceforge.jtds.jdbc.JtdsConnection)
at net.sourceforge.jtds.jdbc.JtdsStatement.close(JtdsStatement.java:972)
at net.sourceforge.jtds.jdbc.JtdsStatement.finalize(JtdsStatement.java:219)
at java.lang.ref.Finalizer.invokeFinalizeMethod(Native Method)
at java.lang.ref.Finalizer.runFinalizer(Finalizer.java:101)
at java.lang.ref.Finalizer.access$100(Finalizer.java:32)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:178)


上面的堆栈跟踪是从使用旧版本的JTDS JDBC driver。显然,这个版本的驱动程序有问题;您可以在调用JtdsConnection#releaseTds()方法的. net . source forge . jtds . JDBC . JtDsStatement对象中看到finalize()方法。显然,这种方法被阻止了,再也没有回来。因此终结器线程在JtdsConnection#releaseTds()方法中被无限期阻塞。因此,终结器无法处理具有finalize()方法的其他对象。因此,应用程序开始遭受OutOfMemoryError。在最新版本的JTDS·JDBC驱动程序中,这个问题得到了解决。关键是,当您实现finalize()方法时,要非常小心。

为什么被命名为妖精陷阱?

一些西方国家的孩子建造了妖精陷阱,作为庆祝圣帕特里克节的一部分。小妖精是童话人物——基本上是一个穿着绿色外套、戴着帽子的非常小的老人,他囤积并搜寻金币。孩子们为小妖精们设计创意陷阱,用金币引诱他们。同样,焦虑终结器线程总是在搜索具有finalize()方法来执行它们的对象。如果finalize()方法被错误地实现,它可能会捕获终结器线。由于这种相似性,我们将其命名为妖精陷阱。

工具

你可以学习more such patterns here。另一方面,为了方便用户,我们构建了一个通用的线程转储分析工具:fastthread.io其中包含了所有线程转储分析模式。您只需将您的线程转储上传到这个在线工具,所有的线程转储分析模式都会自动应用,问题的根本原因会在闪存中报告给您。