《JVM故障诊断指南》之3 - Java 线程: JVM持有内存的分析

jopen 9年前

原文链接 原文作者:James D Bloom 翻译:梅小西(904516706)

前面我们已经讨论过JVM里不同的堆空间,这节我们会给你提供教程,是关于如何从你的活动的应用Java线程中确定它持有多少堆空间,以及在哪里占用。这里有个来自Oracle Weblogic 10.0生产环境的真实案例,它能使你更好的理解分析过程。

我们也会演示这种情况,过多的垃圾收集或者堆空间内存占用问题并不总是由于真实的内存泄露引起,也可能是由于线程执行模型问题和太多的短生命对象引起。

后台

Java线程是JVM基础的一部分。你的Java堆空间内存占用不仅仅是由于静态的和长生命的对象导致,还有可能因为短生命对象。

OutOfMemoryError 问题经常被误认为是内存泄露引起。我们经常忽略错误的线程执行模型和它们持有的JVM里的短生命对象,直到它们的执行完成我们才发现。在这种问题情形下:

• 你“预期”的程序中短生命/无状态对象(XML,JSON数据负载等)被线程持有的时间会变得很长(线程锁争用,大量数据负载,远程系统的慢响应时间等)。
• 最后,这种短生命对象会因为垃圾收集而晋升到长生命空间,比如老年代空间。
• 副作用是会导致老年代空间很快被占满,增加了Full GC(major 收集)的频率。
• 由于这种严重的情况,它将导致更多的GC 垃圾收集,增加JVM暂停时间和最终的“OutOfMemoryError: Java 堆空间”。
• 你的应用此时被停掉,你很疑惑到底怎么回事。
• 最后,你考虑增加Java堆空间或者寻找哪里有内存泄露,你真的找对路了么?

上面这种情况,你应该找到线程执行模型并确定在给定的时间内每个线程需要持有多少内存。

OK我找到了这张图片,但是线程栈大小到底是多少呢?

避免在线程栈大小和Java堆内存占用之间产生混淆是非常重要的。线程栈大小是一种特殊的内存空间,它被JVM用于存储每个方法调用。当一个线程调用方法A,它将这个调用入栈。如果方法A调用方法B,同样也会入栈。一旦方法执行完毕,这个调用便从栈里出栈。

这种线程方法调用会导致Java对象产生,并分配在Java堆里。增加线程栈的大小是没有任何效果的。而调整线程栈大小通常是要处理 java.lang.stackoverflowerror错误或者“OutOfMemoryError: unable to create new native thread”错误的时候才会需要。

这里写图片描述
案例研究和问题环境

下面的分析是基于我们最近调查的一个真实的生产线问题。
1. 改变了用户web接口(使用Google Web Toolkit 和 JSON作为数据负载)后,发现Weblogic 10.0生产环境上出现了某些性能下降。
2. 初始分析发现出现了“OutOfMemoryError: Java heap space”问题并伴有过多的垃圾收集。在OOM出现后自动(XX:+HeapDumpOnOutOfMemoryError)生成了Java堆转储文件。
3. 通过verbose:gc 日志分析确认32-bit HotSpot JVM 老年代空间(1 GB 容量)被完全消耗。
4. 问题发生前和发生时自动产生了线程转储快照。
5. 此时唯一可能减轻问题的方法是在问题发生时重启受影响的Weblogic 服务器。
6. 最终解决了这个问题是将所有的改变回滚。
7.
团队首先怀疑新代码引起了内存泄露。

线程转储分析:寻找嫌疑

第一步我们要做的是对产生的线程转储数据进行分析。这种数据通常会告诉你在JVM堆里面内存分配的罪魁祸首线程。同样的它也会显示任何一个尝试从远程系统发送和接受数据的贪婪或者阻塞的线程。

我们注意的第一个样例是在Weblogic 控制服务器(JVM线程)里观察到的OOM事件和阻塞线程之间有很近的关联关系。下面是找到的原始线程模式:
这里写图片描述

000337> < [STUCK] ExecuteThread: '22' for queue: 'weblogic.kernel.Default (self-tuning)' has been busy for "672" seconds working on the request which is more than the configured time of "600" seconds.

如你所见,上面的线程出现了阻塞并且花费了很长时间从远程系统里读和接收JSON响应。一旦我们找到这个样例,接下来是找出它和JVM堆转储分析之间的关联,并且确定这个阻塞线程从堆里占用了多少内存。

堆转储分析:暴露留存的对象!

使用MAT工具来做Java堆转储分析。我们会列出不同的分析步骤,它会允许我们精确查明持有的内存大小和源头。

1.加载HotSpotJVM堆转储文件

这里写图片描述

2.选择HISTOGRAM 查看并通过ExecuteThread过滤。
* ExecuteThread 是一种Java Class,它被Weblogic内核用于对象的创建和执行*
这里写图片描述
如你所见,这个图非常有启迪作用。我们可以看到总共210个Weblogic线程被创建。
这些线程总共持有的内存占用是806M。这对于带有1G 老年代的32位的JVM进程来说是非常值得注意的。这个图也告诉了我们这个问题的核心和源于线程自己的内存占有。

3.深入分析线程内存占用

接下来是深入分析线程内存持有。右键点击ExecuteThread 类并且选择“列出所有外部引用的对象”。
这里写图片描述
如你所见,我们通过线程堆转储分析可以发现“STUCK(阻塞)”线程和大量内存占用有很大关系。这个发现非常意外。

4.线程局部变量鉴别Thread Java Local variables identification

分析的最后一步是需要我们展开几个线程示例并了解内存占用的原始来源。

这里写图片描述

如你所见,最后一步分析发现根源在于大量的JSON数据响应。通过对转储分析,这个问题可以早点暴露出来,我们发现少量的线程花费太多时间去读取和接收JSON响应,这是大量数据负载的一个明显的症状。

很重要一点,通过方法局部变量创建的短生命对象也会出现在堆转储分析中。然而,其中的一些仅仅能被他们的父线程看到,这是由于他们没有被其他对象引用,比如这个例子。为了找出真正的调用者你或许也需要分析这个线程栈,随后通过代码审查确定最终的根源。

通过这些发现,我们的交付团队能确定最近的JSON错误代码变化的产生,在某些情形下,大量的JSON数据可以达到45M以上。如果环境使用了32位JVM而且仅仅只有1G的老年代,基于这个事实,你就能理解为什么只需要几个线程就足够触发一些性能下降。

这个案例说明,合适的容量预计和堆分析,包括你活动的应用程序的内存占有和Java EE容器线程内存占有都是非常重要的。

译者介绍:
梅小西
Java工程师,关注JVM,并发编程,喜欢研究Python,Scala,Golang等。

译者相关译文:
JVM内部原理
《JVM故障诊断指南》之1 ——JVM概览与介绍
《JVM故障诊断指南》之2 ——调整合适的Java堆大小的技巧
《JVM故障诊断指南》之3 ——Java 线程: JVM持有内存的分析
《JVM故障诊断指南》之4 ——Java 8:从持久代到metaspace

原创文章,转载请注明: 转载自并发编程网 – ifeve.com