分类 Java 相关 下的文章

读懂 thread heap

  1. 第一行是线程基本信息, 分别是 线程名字, 是否是Daemon线程(如果不是就不显示), 线程优先级, 线程id, OS native 线程id, 当前运行的状态[当前在那个对象对象].

  2. 当前线程的状态

  3. 下面就是当前线程的Stack;

    "DefaultThreadPool-89" daemon prio=10 tid=0x00007f3974036000 nid=0xb4a waiting on condition [0x00007f393e7e4000]
    java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for <0x00000007ac99baf8> (a com.ebay.raptor.orchestration.impl.FutureCallableTask)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
    at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:425)
    at java.util.concurrent.FutureTask.get(FutureTask.java:187)

  4. Java 线程有6种状态: New, Runnable, Blocked, Waiting, Timed Waiting, Terminated. 对应到 thread dump 里面: 到现在做的 thread dump 里面没有看到状态是 New的. 也没有看到 Terminated 的. 其它都看到过:
    java.lang.Thread.State: RUNNABLE

java.lang.Thread.State: BLOCKED (on object monitor)

java.lang.Thread.State: TIMED_WAITING (sleeping)
java.lang.Thread.State: TIMED_WAITING (parking)
java.lang.Thread.State: TIMED_WAITING (on object monitor)

java.lang.Thread.State: WAITING (parking)
java.lang.Thread.State: WAITING (on object monitor)

  1. JVM 6种线程定义:

    A thread state. A thread can be in one of the following states:
    NEW
    A thread that has not yet started is in this state.
    RUNNABLE
    A thread executing in the Java virtual machine is in this state.
    BLOCKED
    A thread that is blocked waiting for a monitor lock is in this state.
    WAITING
    A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
    TIMED_WAITING
    A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
    TERMINATED
    A thread that has exited is in this state.
    A thread can be in only one state at a given point in time. These states are virtual machine states which do not reflect any operating system thread states.

  2. 什么情况下会进入 blocked 状态?
    根据 Thread.State 类的描述: 1. 当一个线程在准备进入Synchronized的块/方法的时候, 2. 或者该线程之前已经进入synchronized 块/方法, 之后又call 了 Object.wait, 这个时候, 该线程进入 Waiting状态 或者 timed_waiting 状态, 之后又被 notify 或notifyAll 唤醒, 等待重新进入Synchronized同步块/方法, 这时候又进入blocked 状态.

  3. 什么情况会进入 waiting 状态?
    等待某种事件发生.
    Object.wait with no timeout -> 等待notify 或 notifyAll
    Thread.join with no timeout -> 等待特定线程终止
    LockSupport.park -> 等待 unpark

  4. 什么情况会进入 timed_waiting 状态?
    虽然等待某种特殊事件发生, 不过最多只等待特定时间
    Thread.sleep
    Object.wait with timeout
    Thread.join with timeout
    LockSupport.parkNanos
    LockSupport.parkUntil

由 hypervisor 驱动内存泄漏导致的 VM CPU飙高的问题

今天有开发人员说他们同一个 cluster 里面运行同一版本的某些 server 出现 JVM CPU 非常高的情况, 而其它 server 的JVM
CPU 维持正常. 他们表示说以前没出现过这种情况, 而出现这种情况的server 比正常其它server 的CPU usage 要高很多, 所以被内部某些监控工具自动重启了. 据他们观察这些机器可能正在被内部的某些漏洞扫描工具在扫描, 但是又不能确认, 想请SRE帮忙确认一下原因是什么?

SRE 首先确认了这些 CPU usage 非常高的server 跟内部的漏洞扫描基本没关系, 因为这些漏洞扫描的 traffic 基本进不了程序内部代码逻辑, 在应用框架层就被拦截了, 基本不会造成CPU usage 高. 另外还有其它被漏洞扫描的server 并没有出现 CPU 飙高的情况.

SRE 另外明确看到, 这些出问题的server(其实都是通过OpenStack 虚拟出来的VM)的CPU usage大概都在40%左右, 不出问题的server 的CPU usage 大概在3%左右. 出问题server 的JVM CPU usage 大概在8%左右, 而没有问题的 server 的 JVM CPU usage 大概在1%左右. 所以可以大概得出结论, 这些CPU 大部分并不是被 JVM 所占用, 但是 JVM 也受到了一定的影响.

进一步观察发现出现问题的server 都是在同一台 hypervisor 上, 进一步去查看同一台 hypervisor 上面的其它 vm server, 也都表现出了 CPU 较高的情况.

登录到这台 Hypervisor 上面, 使用下面的命令可以看到, 这些Hypervisor 有kernel的内存泄漏问题:

admin@hv-8hhy:~$ smem -twk
Area                           Used      Cache   Noncache
firmware/hardware                 0          0          0
kernel image                      0          0          0
kernel dynamic memory        159.2G       6.5G     152.7G
userspace memory             139.3G     196.2M     139.1G
free memory                   15.6G      15.6G          0
----------------------------------------------------------
                             314.1G      22.3G     291.8G

在 kernel dynamic memory 这行的 Noncache 这列, 我们看到它使用了152.7G, 这明显是个问题. 对于 Cloud team来说这是一个已知的issue, 并且给出了 kernel 的fix link:
https://git.kernel.org/pub/scm/linux/kernel/git/davem/net.git/commit/drivers/net/ethernet/intel/i40e/i40e_txrx.c?id=2b9478ffc550f17c6cd8c69057234e91150f5972

java 启动参数 -XX:+PrintHeapAtGC

在启动时, 加入 -XX:+PrintHeapAtGC 将会看到如下log 打印

{Heap before GC invocations=4 (full 0):
 PSYoungGen      total 17408K, used 17392K [0x00000000fdf00000, 0x0000000100000000, 0x0000000100000000)
  eden space 16384K, 100% used [0x00000000fdf00000,0x00000000fef00000,0x00000000fef00000)
  from space 1024K, 98% used [0x00000000fef00000,0x00000000feffc010,0x00000000ff000000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 68608K, used 34096K [0x00000000f9c00000, 0x00000000fdf00000, 0x00000000fdf00000)
  object space 68608K, 49% used [0x00000000f9c00000,0x00000000fbd4c000,0x00000000fdf00000)
 Metaspace       used 2612K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K

[GC (Allocation Failure) [PSYoungGen: 17392K->1024K(32768K)] 51488K->52816K(101376K), 0.0101398 secs] [Times: user=0.00 sys=0.00, real=0.00

Heap after GC invocations=4 (full 0):
 PSYoungGen      total 32768K, used 1024K [0x00000000fdf00000, 0x0000000100000000, 0x0000000100000000)
  eden space 31744K, 0% used [0x00000000fdf00000,0x00000000fdf00000,0x00000000ffe00000)
  from space 1024K, 100% used [0x00000000fff00000,0x0000000100000000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 68608K, used 51792K [0x00000000f9c00000, 0x00000000fdf00000, 0x00000000fdf00000)
  object space 68608K, 75% used [0x00000000f9c00000,0x00000000fce94050,0x00000000fdf00000)
 Metaspace       used 2612K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K
}

参考:
https://blogs.oracle.com/poonam/how-do-i-find-whats-getting-promoted-to-my-old-generation/comment-submitted?cid=216c187b-792f-487b-843b-a326449b7120

Java GC 之 CMS 的一些 trade off

  1. 若young gen 过小, 则会频繁发生young GC, gc overhead 过高, 同时大量premature对象被promote到 old gen, 同时old gen 可能会引起大量碎片, 最终引起碎片化;
  2. -XX:PrintFLSStatistics=1 打印free list in BinaryTreeDictionary. 如果=2, 则会花费更多时间打印更多信息.

参考:
https://blogs.oracle.com/poonam/can-young-generation-size-impact-the-application-response-times

Java 程序中导致CPU 100%的常见原因与诊断思路

上周某团队的某个业务的新功能1%上线, 上线不到1天, 开发人员发现该流程里下游有个应用不断有服务器挂掉, 症状表现为CPU 100%, 不能处理新请求. 为了临时快速解决该问题, 同事先是做了2个 heap dump 和 2个 CPU 在某个interval的usage的截屏, 然后让开发人员关闭该新功能, 同时下游服务器被完全重启一遍. 问题得以解决.

问题虽然被临时解决, 但是当时却没发现最终问题所在. 事后去查看相关log, 发现某些机器竟然在那个时间段, 没有把log发到中心log 服务器, 同时本地的某些log被覆盖. 根据截屏的 CPU usage, 发现有很多线程在当时在cache中查找某些值, 有几个线程在HashMap中get()某个值, 看上去很像HashMap的死循环问题, 可是另外有些花费很多CPU的线程又没在HashMap里面. 不过最终查看这些线程Stack, 确定问题是业务逻辑里面有如下代码:

TreeNode curNode = curNode.getParentNode();
Integer curVal = null;
while (null != curNode && null == curVal) {
    curVal = getSomeValueFromConfigCache(curNode);
}

仔细一看, 上面的代码是有问题的, 看上去哪些Node 是一个Tree 结构, 如果通过当前Node 查不到,就根据Parent去查, 结果这个逻辑里面并没有把向上替换的逻辑放到 while 循环里面. 根据当前的代码, 如果当前Node 查不到返回null, 那么这个while 就是一个死循环了. 所以改正很简单, 但是这个代价有点大. 测试代码可能99%都是当前Node 都能查到结果的, 可是就是有1%的数据, 可能要向上找到 ParentNode 才能查到.

对于这种导致CPU 100%的问题, 我们常见的有以下这几种:

  1. Java 内存不够或溢出导致GC overhead问题, GC overhead 导致的CPU 100%问题;
  2. 死循环问题. 如常见的HashMap被多个线程并发使用导致的死循环, 或者上面例子中的死循环;
  3. 某些特费CPU的操作被长期执行. 以前有个case, 使用正则表达式去判断是不是符合某个规则, 可是有些时候输入参数是一个几十K或更长的数据, 该正则写又不好, 导致CPU遇到这种输入, 就爆掉了.
  4. 频繁的获取/释放锁 大量的锁操作 比如大量线程快速同步写 log 到 System.out/System.err (PrintStream).

对于上面第一种类型, 基本可以通过GC overhead 的比例去看, 这种情况下CPU完全是GC 引起的, GC overhead 基本接近100%, 然后下一步就是做 heap dump, 去分析verbose GC log 和 heap dump;

对于上面第二种, 可以多次做CPU usage分析, 比如每次都是1s的interval 内所有线程消耗CPU的百分比, 因为CPU 已经100%, 所以很容易可以看到是哪些线程在大量消耗CPU, 然后去分析这些stacktrace, 基本上死循环的代码之外的stack每次都一样, 死循环以内的某些时候的stack有些差别. 可以通过结合 heap dump 里面的局部变量, 参数同时阅读stack里面的方法的调用关系, 去查看死循环的位置所在. 对于HashMap这种, 基本在几次tread dump中就能确定, 然后去 heap dump 去找到那个循环.

对于第三种, 基本和第二种一样, 首先观察下stack里面有没有看上去就是特耗CPU的操作, 如果有继续看源代码去查看. 最后结合heap dump里面的数据去验证.

对于第四种, 多次查看 thread dump, 就会发现频繁获取释放的锁.