JAVA问题定位排查

1.Java程序运行状态

1.1 java运行状态

  • RUNNABLE:线程处于正在运行状态。

  • TIMED_WAITING(on object monitor) 表示当前线程被挂起一段时间,说明该线程正在执行obj.wait(int time)方法.

  • TIMED_WAITING(sleeping) 表示当前线程被挂起一段时间,即正在执行Thread.sleep(int time)方法.

  • TIMED_WAITING(parking) 当前线程被挂起一段时间,即正在执行Thread.sleep(int time)方法.

  • WAINTING(on object monitor) 当前线程被挂起,即正在执行obj.wait()方法(无参数的wait()方法). 下面的线程正处于WAITING状态,表示当前线程被挂起,如obj.wait()(只能通过notify()唤醒)。因此该线程当前不消耗CPU。

处于TIMED_WAITING、WAINTING状态的线程一定不消耗CPU. 处于RUNNABLE的线程,要结合当前线程代码的性质判断,是否消耗CPU.

  1. 如果是纯Java运算代码,则消耗CPU.

  2. 如果是网络IO,很少消耗CPU.

  3. 如果是本地代码,结合本地代码的性质判断(可以通过pstack/jstack获取本地线程堆栈),如果是纯运算代码,则消耗CPU, 如果被挂起,则不消耗CPU,如果是IO,则不怎么消耗CPU。

1.2 线程的state

1>>RUNNABLE: 线程正在执行中,占用了资源,比如处理某个请求/进行计算/文件操作等

2>>BLOCKED/Waiting to lock(需关注):
   >>>线程处于阻塞状态,等待某种资源(可理解为等待资源超时的线程);
   >>>"waiting to lock <xxx>",即等待给xxx上锁,grep stack文件找locked <xxx> 查找获得锁的线程;
   >>>"waiting for monitor entry" 线程通过synchronized(obj){……}申请进入了临界区,但该obj对应的monitor被其他线程拥有,从而处于等待。

3>>WAITING/TIMED_WAITING{定时}(关注):
   >>>"TIMED_WAITING (parking)":等待状态,且指定了时间,到达指定的时间后自动退出等待状态,parking指线程处于挂起中;
   >>>"waiting on condition"需与堆栈中的"parking to wait for  <xxx> (atjava.util.concurrent.SynchronousQueue$TransferStack)"结合来看。first-->此线程是在等待某个条件的发生,来把自己唤醒,second-->SynchronousQueue不是一个队列,其是线程之间移交信息的机制,当我们把一个元素放入到 SynchronousQueue 中时必须有另一个线程正在等待接受移交的任务,因此这就是本线程在等待的条件。

4>>Deadlock(需关注):死锁,资源相互占用。
   
           线程状态为“waiting for monitor entry”
                   意味着它在等待进入一个临界区 ,所以它在”Entry Set“队列中等待。
                   此时线程状态一般都是 Blocked:
                   java.lang.Thread.State: BLOCKED (on object monitor)
   
            线程状态为“waiting on condition”
                   说明它在等待另一个条件的发生,来把自己唤醒,或者干脆它是调用了 sleep(N)。
                   此时线程状态大致为以下几种:
                   java.lang.Thread.State: WAITING (parking):一直等那个条件发生;
                   java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定时的,那个条件不到来,也将定时唤醒自己。
   
            如果大量线程在“waiting for monitor entry”
                   可能是一个全局锁阻塞住了大量线程。
                   如果短时间内打印的 thread dump 文件反映,随着时间流逝,waiting for monitor entry 的线程越来越多,没有减少的趋势,可能意味着某些线程在临界区里呆的时间太长了,以至于越来越多新线程迟迟无法进入临界区。
   
            如果大量线程在“waiting on condition”
                   可能是它们又跑去获取第三方资源,尤其是第三方网络资源,迟迟获取不到Response,导致大量线程进入等待状态。
                   所以如果你发现有大量的线程都处在 Wait on condition,从线程堆栈看,正等待网络读写,这可能是一个网络瓶颈的征兆,因为网络阻塞导致线程无法执行。
                   线程状态为“in Object.wait()”:
                   说明它获得了监视器之后,又调用了 java.lang.Object.wait() 方法。
                   每个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。
                   当线程获得了 Monitor,如果发现线程继续运行的条件没有满足,它则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃了 Monitor,进入 “Wait Set”队列。
                   此时线程状态大致为以下几种:
                   java.lang.Thread.State: TIMED_WAITING (on object monitor);
                   java.lang.Thread.State: WAITING (on object monitor);
                   一般都是RMI相关线程(RMI RenewClean、 GC Daemon、RMI Reaper),GC线程(Finalizer),引用对象垃圾回收线程(Reference Handler)等系统线程处于这种状态。

1.3 死锁:Found one Java-level deadlock

死锁的两个或多个线程是不消耗CPU的,有的人认为CPU 100%的使用率是线程死锁导致的,这个说法是完全错误的。无限循环(即死循环),并且在循环中代码都是CPU密集型,才有可能导致CPU的100%使用率,像socket或者数据库等IO操作是不怎么消耗CPU的。

死循环:
  1. 通过前面介绍的堆栈获取方法获取第一次堆栈信息(详细请参考第 3页第 §1.1节)。

  2. 等待一定的时间,再获取第二次堆栈信息。

  3. 预处理两次堆栈信息,首先去掉处于sleeping或者waiting状态的线程,因为这种线程是不消耗CPU的。

  4. 比较第一次堆栈和第二次堆栈预处理后的线程,找出这段时间一直活跃的线程,如果两次堆栈中同一个线程处于同样的调用上下文,那么就应该列为重点怀疑对象。结合代码逻辑检查该线程的执行上下文所对应的代码段是否属于应该长期运行的代码。如果不属 于,那么就要仔细检查,为什么这个线程长期执行不完那段代码,这段代码是否可能存在一个死循环。 如果通过堆栈定位,没有发现热点代码段,那么CPU过高可能是不恰当的内存设置导致的频繁GC,从而导致CPU过高

  • HashMap等线程不安全的容器,用在多线程读/写的场合,导致HashMap的方法调用形成死循环。

  • 多线程场合,对共享变量没有进行保护,导致数据混乱,从而使循环退出的条件永远不满足,导致死循环的发生,如for,while循环中的退出条件永远不满足导致的死循环。链表等数据结构首尾相接,导致遍历永远无法停止。

  • 其它错误的编码。

1.4 性能瓶颈分析

  • sleep的滥用

  • 不恰当的线程模型

  • 效率低下的SQL语句或者不恰当的数据库设计

  • 不恰当的GC参数设置导致的性能低下

  • 线程数量不足

  • 内存泄漏导致的频繁GC

在Java中,内存的分配是由程序完成的,而内存的释放是由GC完成的,任何一种垃圾收集算法一般要做2件基本的事情:

  1. 发现无用信息对象;

  2. 回收被无用对象占用的内存空间。

Java进程内存=Java堆内存+本地内存+Perm内存,其中堆内存和Perm内存都可以通过参数设置它的大小。而本地内存的大小是不需要设置的

OOM问题
内存溢出(OOM)的原因

JAVA问题定位排查

  • 堆溢出(Java heap space):

    • 原因:占用大量堆空间,直接溢出

    • 解决方法:增大堆空间,及时释放内存

  • 永久区溢出(PermGen space):

    • 原因:类的数量太多导致

    • 解决方法:增大Perm,允许Class回收

  • 栈溢出(unable to create new native thread):

    • 原因:在创建线程的时候,操作系统无法给出足够的空间

    • 解决方法:合理减少堆空间,减少线程栈大小/超过最大运行线程数,调整系统内核参数

  • 直接内存溢出:

    • 原因:ByteBuffer.allocateDirect()无法从操作系统获得足够的空间

    • 解决方法:合理减少堆空间,有意触发GC

  • 堆内存不足,如果JVM 不能在java堆中获得更多内存来分配更多java 对象,将会抛出java堆内存不足(java OOM) 错误。如果java 堆充满了活动对象,并且JVM无法再扩展java堆, 那么它将不能分配更多java 对象。

  • 本地内存不足, 如果JVM 无法获得更多本地内存,它将抛出本地内存不足(本地OOM) 错误。当进程用到的内存到达操作系统的最大限值,或者当计算机用完RAM和交换空间时,通常会发生这种情况。当发生这种情况时,JVM处于本地内存OOM状态,此时虚拟机会打印相关信息并退出。通常情况下,JVM收到sigabort信号时将会生成一个核心文件。

  • 加载类(字节码)的Perm内存不足.即指定的Permsize不足以加载系统运行使用的.class字节码文件,就会发生Perm内存不足的错误。

在应用程序中需要检查以下方面:
  1. 应用程序中的缓存功能如果应用程序在内存中缓存java对象,则应确保此缓存并没有不断增大。对缓存中的对象数应有一个限值。我们可以尝试减少此限值,来观察其是否降低java堆使用量。

  2. 大量的长期活动对象如果应用程序中有长期活动对象,而且占用的内存比较大,则可以尝试尽可能减少这些对象的存在期,或者通过更改设计避免这种需要大量内存的长期对象。 本地内存泄漏导致的OOM,一般原因有如果几个可能:

  • 如果系统中存在JNI调用,本地内存泄漏可能存在于JNI代码中。

  • JDK的Bug

  • 操作系统的Bug.

如果Java应用程序存在内存泄漏,往往伴随着如下的现象:

  1. 系统越来越慢,并伴随CPU使用率过高。这主要是因为随着内存的泄漏,可用的内存越来越小,垃圾回收器频频进行垃圾回收(完全垃圾回收(FULL GC)一次接一次,每次耗时几秒,甚至几十秒),而垃圾回收是一个CPU密集型操作,频繁的GC会导致CPU持续居高不下,在有内存泄漏的场合,到了最后必然是伴随着CPU使用率几乎为100

  2. 系统运行一段时间,系统抛OutOfMemory异常,至此整个系统完全不工作

  3. 虚拟机core dump

出现java.lang.OutOfMemoryError: PermGen space异常,说明虚拟机的Perm内存(即永久区内存)不足

堆内存设置太小,很多的CPU时间片被用作垃圾回收,导致频繁GC,CPU过高,浪费很多CPU.严重的时候,会导致性能有几倍或者几十倍的下降。 OutOfMemory,堆内存设置过小,正常的内存分配也无法满足 ,造成事实的内存不足。

  • 实时是一种计算机响应等级,在这种等级下,用户可感知到足够快,或者计算机能够跟得上某个外部流程。

  • 延迟是指系统花在从一个指定点传输数据到另一个指定点的时间。

  • 抖动是延迟偏差。一个具有确定性的应用程序抖动应该较低。该术语基本上描述了一种度量确定性的方法。

  • 吞吐量是计算机在给定的时间帧中所能处理的工作量。

  • 确定性垃圾收集是一个执行概念,用于描述快速的、可预测暂停时间的内存堆垃圾收集。 垃圾收集是从堆中清理废弃对象以便收回空间用于新对象的过程。

常见报错:

Heap: JVM:HeapOutOfMemory   

Stack: JVM:StackOverFlow

Nio: JVM:ConstantOutOfMemory,JVM:DirectMemoryOutOfMemory

年老代堆空间被占满

异常:java.lang.OutOfMemoryError: Java heap space

说明:

这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。

如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小时或者几天)

解决:

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

持久代被占满

异常:java.lang.OutOfMemoryError: PermGen space

说明:

Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。

更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。

解决:

1. -XX:MaxPermSize=16m

2. 换用JDK。比如JRocket。

堆栈溢出

异常:java.lang.StackOverflowError

说明:这个就不多说了,一般就是递归没返回,或者循环调用造成

线程堆栈满

异常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减小单个线程大小。以便能生产更多的线程。

Minor GC 年轻代空间不足

异常:GC Allocation Failure

说明:GC原因是因为年轻代中没有任何合适的区域能够存放需要分配的数据结构触发的。

解决:增大Xmn设置

2.Java进程CPU使用率高排查

生产java应用,CPU使用率一直很高,经常达到100%,通过以下步骤可以快速定位,可以写成脚本更好。

1.jps 获取Java进程的PID。

2.jstack pid >> java.txt 导出CPU占用高进程的线程栈。

3.top -H -p PID 查看对应进程的哪个线程占用CPU过高。

4.echo “obase=16; PID” | bc 将线程的PID转换为16进制。

5.在第二步导出的Java.txt中查找转换成为16进制的线程PID。找到对应的线程栈。

6.分析负载高的线程栈都是什么业务操作。优化程序并处理问题。

Java线程实际上和本地线程指的是同一个东西,只有本地线程才是真正的线程实体,Java线程实际上就是指这个本地线程,它并不是一个另外存在的的实体。

• • •
当一个线程占有一个锁的时候,线程堆栈中会打印 —locked <0x22bffb60>
当一个线程正在等待其它线程释放该锁,线程堆栈中会打印 —waiting to lock <0x22bffb60>
当一个线程占有一个锁,但又执行到该锁的wait()上,线程堆栈中首先打印locked,然后又会打印—waiting on <0x22c03c60
>

3.Java问题排查常用命令

#查看java线程信息
pidof java
jps -m
top -H -p pid
pstree -p pid
netstat -nat|awk  '{print $6}'|sort|uniq -c|sort -rn
pstack -m pid
jmap -heap pid
jstat -gc pid 5000

#查看JVM中内存、栈信息
jmap -dump:format=b,file=文件名 [pid]  
jmap -histo 2815 | head -10 
jmap -dump:format=b,file=/tmp/$(hostname -i).hprof pid                    
jstack pid |grep 'java.lang.Thread.State' | awk '{print $2$3$4$5}' | sort | uniq -c    
strace -T -r -c -f -F -p pid

参考:

anzhihe安志合个人博客,版权所有丨 如未注明,均为原创 丨转载请注明转自:https://chegva.com/2887.html | ☆★★每天进步一点点,加油!★★☆

您可能还感兴趣的文章!

发表评论

电子邮件地址不会被公开。 必填项已用*标注