本文介绍了在性能测试过程中Java进程消耗CPU过高的问题排查方法、线程死锁问题排查方法和内存泄露的排查方法

Java进程消耗CPU过高的问题排查方法

CPU利用率过高,查看JVM中线程占用cpu大小的方法

ps –Lfp pid

top –p pid -H

查看高占用的线程并转化为十六进制的方法printf "%x\n"  其中x为线程号

然后 jstack -l  16进制的线程号

线程死锁问题排查方法

死锁定义

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。

典型死锁

Java应用必选掌握的性能调优知识点_压力测试

线程状态

NEW,未启动的。不会出现在Dump中。

RUNNABLE,在虚拟机内执行的。运行中状态,可能里面还能看到locked字样,表明它获得了某把锁。

BLOCKED,受阻塞并等待监视器锁。被某个锁(synchronizers)给block住了。

WATING,无限期等待另一个线程执行特定操作。等待某个condition或monitor发生,一般停留在park(), wait(), sleep(),join() 等语句里。

TIMED_WATING,有时限的等待另一个线程的特定操作。和WAITING的区别是wait() 等语句加上了时间限制 wait(timeout)。

TERMINATED,已退出的。

解决

使用jstack –l pid 进行分析

jstack  -l  long listing. Prints additional information about locks

锁竞争排查方法

如果一个线程尝试进入另一个线程正在执行的同步块或者方法时,便会出现锁竞争。第二个线程就必须等待前一个线程执行完这个同步块并释放掉监视器(monitor)。JDK1.5之前,实现同步主要是使用synchronized,而在JDK1.5中新增了java.util.concurrent包及其两个子包locks和atomic,其中子包locks中定义了系列关于锁的抽象的类。java.util.concurrentLock.locks的介绍lock -> 调用后一直阻塞到获得锁tryLock -> 尝试是否能获得锁 如果不能获得立即返回lockInterruptibly(可中断锁)

-> 调用后一直阻塞到获得锁 但是接受中断信号。先说说线程的打扰机制,每个线程都有一个打扰标志。这里分两种情况,

1.线程在sleep或wait,join, 此时如果别的进程调用此进程的

interrupt()方法,此线程会被唤醒并被要求处理InterruptedException;

2. 此线程在运行中,

则不会收到提醒。但是 此线程的 “打扰标志”会被设置, 可以通过isInterrupted()查看并

作出处理。lockInterruptibly()和上面的第一种情况是一样的,

线程在请求lock并被阻塞时,如果被interrupt,则“此线程会被唤醒并被要求处理InterruptedException”。并且如果线程已经被interrupt,再使用lockInterruptibly的时候,此线程也会被要求处理interruptedException

排查方法:

Jstack pid

如果大量线程在“waiting for monitor entry”: 则可能是一个全局锁阻塞住了大量线程。

解决:

降低锁的竞争可以提高并发程序的性能和可伸缩性,有3种方式可以降低锁的竞争:

1. 减少锁的持有时间(缩小锁的范围)

2. 降低锁的请求频率(降低锁的粒度)

3. 放弃使用独占锁,使用并发容器,原子变量,读写锁等等来代替它。

 缩小锁的范围

缩小锁的范围意味着在这个锁中执行更少的操作,因为与锁无关的代码越多,锁的持有时间就越长(去公厕上厕所,就不要在里面慢慢欣赏AV了),这样会加剧锁的竞争。

减小锁的粒度

降低线程请求锁的频率可以通过两种手段来实现

锁分解

锁分段

如果一个锁需要保护多个相互独立的状态变量,那么可以将这个锁分解为多个锁,每个锁只保护一个变量,从而提高可伸缩性并降低锁被请求的频率。

有时候可以将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况称为所分段

 避免热点域

当每个操作都请求多个变量时,锁的粒度很难降低,常见的优化措施例如将一些反复计算的结果缓存起来,这时候就会引入一些“热点域”。ConcurrentHashMap中的size将对每个分段进行枚举并将每个分段中的元素数量进行相加,而不是维护一个全局计数,它为每个分段都维护了一个独立的计数来避免枚举每个元素,并通过每个分段的锁来维护这个值,有效地避免了热点域问题。

内存泄露排查方法

内存泄露(memory leak)

如果对象一直被应用,jvm无法对其进行回收,创建新的对象时,无法从Heap中获取足够的内存分配给对象,这时候就会导致内存溢出。所以对象无法被GC回收(垃圾回收可以自动清空堆中不再使用的对象。在JAVA中对象是通过引用使用的。如果再没有引用指向该对象,那么该对象就无从处理或调用该对象,这样的对象称为不可到达(unreachable)。垃圾回收用于释放不可到达的对象所占据的内存。)就是造成内存泄露的原因!内存泄露的真因是:持有对象的强引用,且没有及时释放,进而造成内存单元一直被占用,浪费空间,甚至可能造成内存溢出!内存泄露是内存溢出的一种诱因,不是唯一因素。

以发生的方式来分类,内存泄漏可以分为4类: 

1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。 

2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。 

3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

4.隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但

是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

解决:

请参考以下内存溢出各种情况的解决方法。

堆内存溢出

异常:outOfMemoryError:java heap space

       在jvm规范中,堆中的内存是用来生成对象实例和数组的。

       如果细分,堆内存还可以分为年轻代和年老代,年轻代包括一个eden区和两个survivor区。

       当生成新对象时,内存的申请过程如下:

          a、jvm先尝试在eden区分配新建对象所需的内存;

          b、如果内存大小足够,申请结束,否则下一步;

          c、jvm启动youngGC,试图将eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;

          d、Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;

          e、 当OLD区空间不够时,JVM会在OLD区进行full GC;

          f、full GC后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out

of memory错误”:outOfMemoryError:java heap space

说明:

 

Java应用必选掌握的性能调优知识点_压力测试_02

这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。

(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小时或者几天)

解决:

这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。具体步骤参考如下:

一、使用jps查看线程ID

二、使用jstat -gc 3331 250 20 查看GC的增长情况。如果有大量的FGC就要查询是否有内存泄漏的问题了。(对进程3331监控,每250ms 收集一次,收集20次)

jstat -gcutil 25299

三、使用jstat -gccause:额外输出上次GC原因

四、使用jmap -dump:format=b,file=heapDump 3331生成堆转储文件

五、使用MAT工具分析堆情况。

六、结合代码解决内存溢出或泄露问题。

永久保存区域内存溢出

异常:outOfMemoryError:permgem space

我们知道Hotspot

jvm通过持久带实现了Java虚拟机规范中的方法区,而运行时的常量池就是保存在方法区中的,因此持久带溢出有可能是运行时常量池溢出,也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。当持久带溢出的时候抛出java.lang.OutOfMemoryError:

PermGen space。可能在如下几种场景下出现:

使用一些应用服务器的热部署的时候,我们就会遇到热部署几次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的class没有被卸载掉。

如果应用程序本身比较大,涉及的类库比较多,但是我们分配给持久带的内存(通过-XX:PermSize和-XX:MaxPermSize来设置)比较小的时候也可能出现此种问题。

一些第三方框架,比如spring,hibernate都通过字节码生成技术(比如CGLib)来实现一些增强的功能,这种情况可能需要更大的方法区来存储动态生成的Class文件。

总之如果程序加载的类过多,或者使用反射、gclib等这种动态代理生成类的技术,就可能导致该区发生内存溢出。

解决:

调整 -XX:MaxPermSize=16m

堆栈溢出

异常:java.lang.StackOverflowError

说明:一般就是递归没返回,或者循环调用造成。

解决:尽量减少递归调用,和循环调用,-Xss 为jvm启动的每个线程分配的内存大小

线程堆栈满

异常:Fatal: Stack size too small

说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。

解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。

系统内存被占满

异常:java.lang.OutOfMemoryError: unable to create new native thread

说明:这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。

解决:

1. 重新设计系统减少线程数量。

2. 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。

总结

栈内存溢出:程序所要求的栈深度过大导致。

堆内存溢出: 分清 内存泄露还是 内存容量不足。泄露则看对象如何被 GC Root 引用。不足则通过 调大 -Xms,-Xmx参数。

持久带内存溢出:Class对象未被释放,Class对象占用信息过多,有过多的Class对象。

无法创建本地线程:总容量不变,堆内存,非堆内存设置过大,会导致能给线程的内存不足。