Java 多线程或内存泄漏缺陷排查的一些经验

Java 多线程或内存泄漏缺陷排查的一些经验


JVM Thread DUMP 基本功
Windows 下用Ctrl-Break,Unix 下用 kill -3 <pid> 的命令让JVM输出 thread dump。
每隔几秒 thread dump 一次,多做几次,分析比较。

Thread Dump分析的一些经验
1 找出这几次Thread dump 文件中,有哪些 Java Thread 处于长时间等待状态,很有可能就是问题之所在。
2 如果Java 线程等在某些不可能出错的地方,如 java.lang.XXX/java.util.XXX对象的某个方法,则很有可能是因为出现了 OutOfMemoryError 异常,原因不外乎是JVM 堆内存过小或出现内存泄漏。
3 对于死锁,最直接的表现就是至少两个线程长时间等待相互持有的对象(每个线程所持有的对象和它当前等待的对象都可以从 dump 中看到)。
4 对于死循环,要辅助CPU占用率确定;如果发现CPU至少一颗使用率为100%,并且有线程长时间位于用户代码处,则很有可能是死循环引起。


多线程缺陷排查
对于Java死锁问题很少出现,多线程访问变量时冲突很常见。
一般出在多线程共享同一对象实例如全局Map,Servlet,Interceptor,或如多线程同时访问某个静态方法,而此静态方法不巧又访问另一个静态变量。
这类问题自测发现不了,在并发压力测试时才能发现。如果代码的入口检查做得好,多半会抛出一些莫名其妙的异常;要不然就会出现正常运行但数据库记录不对的情况。
对这种问题,并无多好的办法解决,主要还是靠看异常堆栈和静态代码分析来解决。


内存泄漏排查
一般用商用辅助工具排查,但有可能出现在JVM heap dump 模式下,运行极度缓慢的情况。
笨笨曾经用过一个非常简单的工具,效果不错,它可以做到在不影响jvm 执行速度的情况下,做heap dump,然后对dump出的文件进行排序,检查即可。

heapprofile(http://www.virtualmachine.de/)

thread dump的结果太长了,
如何能让thread dump的结果输出在文件中?

要分 JDK .
IBM JDK 始终输出 Thread Dump到某个文件中。
Sun/Jrockit JDK 会输出到 stdout 中。

对于 Sun/JRockit JDK,最简单的办法 是重定向 stdout 到某个文件: java xxx.jar > xxx.txt

最复杂的办法是用 JNI 实时修改 STDOUT/STDERR handler。
第一次翻译,大家凑合着看,英文好的请看原文:http://www.unixville.com/~moazam/stories/2004/05/18/debuggingHangsInTheJvm.html
在JVM挂起时调试线程
有时,JAVA用户或开发者会遇到JAVA应用程序看起来像挂起的问题,而且没有核心文件产生,没有任何的IO检测,这个过程只是坐在那里等待...通常这些问题可以追溯到操作系统和的JVM级别线程。

以下内容是从Solaris方面来介绍的,也许不久我会写些针对Linux线程的东西

两个线程模型的故事
Solaris 8和9有两个独立的线程模型。在sun的官方网站上有一个权威的解释 。点这里进入。我这边基本上长话短说, Solaris 8下,默认情况下,使用一个多对多线程模型,而Solaris 9采用了一对一线程模型。

在Solaris 9发布以后, Solaris 8还包括一对一线程模型作为'替代'的线程模型。用户如果想在Solaris 8使用“替代”的线程库,应当确保他们运行过最新的108993修补程序,早些时候反映此线程库有漏洞,后来采用上述补丁来修正。该线程模型的库是在/usr/lib/lwp下 。

当JAVA进程挂起时,你应该怎么做
在你的Java程序挂起时,有几个不同的事情,您可以尝试。

1.得到一个Java级别的堆栈跟踪。
2.获取当前LWPs(light weight process)和它们当前状态的快照
3.获得底层原生级别堆栈跟踪。
4.尝试对进程抛出SIGWAITING信号
5.强制进程dump一个core文件
6.切换到替代线程库,然后试着重现那个问题

下面更详细地说明每一个步骤..

1.得到一个Java级别堆栈跟踪。
这里的目的是从挂起进程中获得一个Java级别的堆栈跟踪。Java级别堆栈跟踪看起来如下所示:


可以从几个不同方式来介绍Java级别的堆栈跟踪。在UNIX环境中,您可以发送的进程SIGQUIT信号(kill -3 ,或kill -SIGQUIT ),然后进程将打印堆栈跟踪到控制台。在Windows中,你只能在启动这一进程的控制台按下Ctrl - \。

如果您已使用用-Xrs参数来启动JVM,发送SIGQUIT和SIGWAITING信号将无效 。-Xrs参数的目的是告诉JVM忽视系统的信号。

为了记录进程的堆栈跟踪信息,您可以在启动JAVA程序时就重定向stdout和stderr到一个文件。

例如,要把jEdit程序的stderr和stdout重定向到一个叫"console.txt'的文件中,我们可以这样做:

java -jar jedit.jar > console.txt 2>&1

这个方法在Unix和Windows都适合 。
如果只是想stdout 的线程堆栈在 txt中打印就向下面打印的:
运行中的窗口中 使用ctrl + break 就可以将线程信息打印在文本中
java -jar jedit.jar > console.txt 即可

2.获取当前LWPs(light weight process)和它们当前状态的快照
本节旨在针对Solaris用户。很多人使用'top'或'prstat'来查看每个进程消耗了多少CPU,内存等。'prstat'命令允许用户查看在LWP级别,粒度更细的资源状况。只需运行'prstat -L'即可

prstat输出看起来像这样:


从上面的输出,您可以看到,有一个Java的过程,但它有多个LWPs在运行。前两个( 13和10 )正消耗了6.1%和1.9%的CPU 。现在,当你接着往下看第三点的底层原生级别的堆栈跟踪,你将会明确知道某一个LWP正在做些什么

3.获得底层原生级别堆栈跟踪。
Solaris有一个非常有用的命令叫做'pstack' 。Linux用户也有福, 罗斯汤普森高人已经移植这个命令到Linux,你可以从这里下载 。

pstack命令能够根据指定的PID返回进程的调用过程。它显示了LWP的编号以及在pstack命令运行时的那时刻的系统调用信息。这对于检查为什么一个进程挂起或崩溃是有很大的帮助。

以下是底层原生堆栈跟踪,对应于上面的JAVA级别的堆栈跟踪。Java级别的跟踪显示了0x22n的nid,对应十六进制是34 。我可以从上面的Java级别堆栈跟踪输出的前两行可以看出来。

"SideKick #1" prio=1 tid=0x00593b80 nid=0x22

因此,我们将在pstack输出内容中寻找LWP为34的。您可能还注意到,在'prstat -L'的输出中的第6行,里面的LWP也是LWP 34, 而它当前消耗了0.3%的CPU。


4.尝试对进程抛出SIGWAITING信号。

SIGWAITING游戏
第一件事是我建议大家在JAVA程序挂起时,尝试发送一个SIGWAITING信号给它。这是Solaris 8要求的,但你在Solaris 9下不需要这样做,因为它有一个不同的默认线程的行为。什么是SIGWAITING ?它是信号32,且它的定义是:

"A signal that is implemented in Solaris and used to tell a threaded process that it should consider creating a new LWP."

通常,当一个发生挂起时,一个LWP中有多个线程在调度,其中的一个被阻塞,而其他的可能仍然在运行。其他正在运行的线程可能就会变成孤魂野鬼,因为没有LWP可以依附。当SIGWAITING信号发送到挂起进程时,线程库将建立一个新的LWP,并重新调度那些“其他正在运行“的线程。可能会把原来挂起的进程救活。

5.强制一个进程dump一个core文件
如果上面所有的方法都不行的话,你也许需要在堆栈跟踪中进行更深层次的分析。而一个core文件将会指出哪些是导致进程挂起的。当一个进程处在挂起状态时,它不会自动dump出core文件。在Solaris上有一个名为'gcore'的命令,它能够强制一个处在任何状态的进程dump出core文件。使用简单, gcore <pid> 。

请确认您已在Solaris使用coreadm或ulimit命令开启core dump功能。

例如,想把使用全局唯一命名的core文件保存到/var/core/目录下,切换到root用户并使用下面命令:

mkdir -p /var/cores
coreadm -g /var/cores/%f.%n.%p.core -e global -e process -e global-setid -e proc-setid -e log

我个人认为,Java级别的堆栈跟踪,pstack的底层原生的堆栈跟踪,及prstat -L的输出数据已经足够了。因此core文件并不一定需要。

6.切换到替换线程库,并尝试重现该问题。
如果SIGWAITING信号确实帮助挂起的进程重新活过来,那么你运气不错。此时你应该切换到替换线程库,看看是否会重新出现挂起现象。一般来说,它不会再发生。

把你的应用程序切换到替换线程库是比较简单的,不需要重新编译。只需重置LD_LIBRARY_PATH环境变量以指向新的线程库。例如: LD_LIBRARY_PATH = / usr / lib中/ lwp LD_LIBRARY_PATH_64 = / usr/lib/lwp/64

You can find much more details about the alternate thread library and how to use it by reading this article, Alternate Thread Library (T2) -- New in the Solaris 8 Operating Environment
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值