JVM调优与故障排查

本文转自:https://zhuanlan.zhihu.com/p/137502691
线上故障主要会包括 CPU、磁盘、内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍。同时例如
jstack、jmap 等工具也是不囿于一个方面的问题的,基本上出问题就是 df、free、top 三连,然后依次 jstack、jmap伺候,具体问题具体分析即可。

CPU

一般来讲CPU 异常往往还是比较好定位的。首先排查 CPU 方面的问题:

  • 业务逻辑问题(死循环)
  • 频繁 gc
  • 上下文切换过多。

业务逻辑问题

  • top命令查看cpu使用率较高的进程与线程
    • 查看所有进程:top
    • 查看某个进程的所有线程:top -Hp pid -H 显示线程信息 -p 指定pid
  • 将jstack dump到文件中,便于查看。jstack pid/nid > myjstack.log
    • pid指进程的id,包含该进程的所有线程的堆栈信息(GC的线程包含)
    • nid指线程pid的16进制 可以通过printf %x 1234,显示1234的十六进制

需要重点关注"java.lang.Thread.State",WAITING、TIMED_WAITING、BLOCKED,这些表示状态阻塞、等待的线程,一般的,死循环的线程处于RUNNABLE状态,而且堆栈信息会出现循环调用,值得注意。

GC

  • 查看GC状态: jstat -gc pid 1000,1000表示采样间隔。
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
2560.0 2560.0 385.7   0.0    4096.0   1078.1   11264.0     7476.1   14848.0 14366.9 1792.0 1679.0     12    0.057   0      0.000    0.057

一次采样信息如上,含义为:

  • 对于S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU ,尾部字符C表示容量,U表示使用量。S0/S1为两个Survivor区,E表示Eden区,O表示老年代,M表示元数据区。
  • 对于YGC YGCT FGC FGCT GCT,尾部字符C表示GC的次数,T表示GC的耗时,YG表示Young GC,FGC表示Full GC,GCT表示所有GC的总耗时。

上下文切换次数

  • 查看上下文的切换次数:vmstat pid
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
3  0      0 145536 165596 2099072    0    0    10    53    0    0  1  0 98  1  0

cs(context switch)一列代表了上下文切换次数

磁盘

  • 磁盘空间:df
  • 磁盘读写状态与io事件的cpu使用率:iostat -x
  • 哪些进程正在进行读写:iotop,此命令也会实时展示io事件的cpu使用率

内存

内存问题主要包括 OOM、StackOverFlow、GC 问题和堆外内存。

首先用free命令查看内存的概况。

              total        used        free      shared  buff/cache   available
Mem:        2027960      624004      327788        1064     1076168     1250612
Swap:       1048572           0     1048572

OutOfMemory&StackOverFlow

JMV 中的堆内内存不足,会抛出异常OutOfMemory、StackOverFlow,大致可以分为以下几种:
Caused by: Exception in thread “main” java.lang.OutOfMemoryError:

  1. unable to create new native thread
  2. Java heap space
  3. Meta space
  4. Exception in thread “main” java.lang.StackOverflowError

1. unable to create new native thread:没有足够的内存空间给线程分配 Java 栈

  • 首先使用pstree -p pid | wc -l查看该进程的总体线程数量,是否线程过多?
  • 基本上还是线程池代码写的有问题,比如说忘记 shutdown,所以说应该首先从代码层面来寻找问题,使用 jstack 或者 jmap。
  • 如果一切都正常,JVM 方面可以通过指定Xss来减少单个 thread stack 的大小

2.java heap space:最常见的OOM错误,堆的内存占用已经达到-Xmx 设置的最大值,解决思路仍然是先从代码层面排查,怀疑存在内存泄露,通过jstack、jmap定位问题。如果说一切都正常,才需要通过调整Xmx的值来扩大内存。

3.Meta space:元数据区的内存占用已经达到XX:MaxMetaspaceSize设置的最大值,排查思路和上面的一致,参数方面可以通过XX:MaxPermSize来进行调整(这里就不说 1.8 以前的永久代了)。

4.StackOverFlow

栈内存也是JVM内存的一部分,表示线程栈需要的内存大于 Xss 值,同样也是先进行排查,参数方面通过Xss来调整,但调整的太大可能又会引起 OOM。

JMAP 定位内存泄漏

上述关于 OOM 和 StackOverflow 的代码排查方面,需要分析jvm内存。

  • 一般的,在启动参数中指定-XX:+HeapDumpOnOutOfMemoryError来自动保存 OOM 时的 dump 文件。
  • 也可以使用 JMAP来导出 dump 文件,然后使用jvisualvm打开dump文件,查看类的实例数量、类的实例内存占用比例来排查堆内存泄露,也可以查看堆转储上的线程。

jmap -dump:format=b,file=filename pid

  • 使用jmap -heap pid查看堆信息、GC状态,或者jmap -histo:live pid查看内存占用、类的实例数、大小。

jconsole是一个可以实时监控JVM的工具,和jvisualvm一样是jdk自带的工具。

内存泄露的原因主要有:文件流未正确关闭、静态的hashMap、List对象中的元素不会被回收,ByteBuffer缓存分配不合理。

GC问题

GC除了影响 CPU 也会影响内存,GC问题有:

  • youngGC 或者 fullGC 次数是不是太多
  • EU、OU 等指标增长是不是异常

排查思路也是一致的,仍然使用: jstat -gc pid 1000,1000表示采样间隔。

在启动参数中加上-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps来开启 GC 日志

  • youngGC 过频繁
    youngGC 频繁一般是短周期小对象较多,先考虑是不是 Eden 区/新生代设置的太小了,看能否通过调整-Xmn、-XX:SurvivorRatio 等参数设置来解决问题。如果参数正常,但是 young gc 频率还是太高,就需要使用 Jmap 和 MAT 对 dump 文件进行进一步排查了

  • youngGC 耗时过长
    耗时过长问题就要看 GC 日志里耗时耗在哪一块了。以 G1 日志为例,可以关注 Root
    Scanning、Object Copy、Ref Proc 等阶段。Ref Proc 耗时长,就要注意引用相关的对象。Root Scanning
    耗时长,就要注意线程数、跨代引用。Object Copy 则需要关注对象生存周期。

  • 触发 fullGC

    fullGC 的原因可能包括以下这些,以及参数调整方面的一些思路:

    • 并发阶段失败:在并发标记阶段,MixGC 之前老年代就被填满了,那么这时候 G1 就会放弃标记周期。这种情况,可能就需要增加堆大小,或者调整并发标记线程数-XX:ConcGCThreads。
    • 晋升失败:在 GC 的时候没有足够的内存供存活/晋升对象使用,所以触发了 Full GC。这时候可以通过-XX:G1ReservePercent来增加预留内存百分比,减少-XX:InitiatingHeapOccupancyPercent来提前启动标记,-XX:ConcGCThreads来增加标记线程数也是可以的。
    • 大对象分配失败:大对象找不到合适的 region 空间进行分配,就会进行 fullGC,这种情况下可以增大内存或者增大-XX:G1HeapRegionSize。
    • 程序主动执行 System.gc():不要随便写就对了。

​ 在启动参数中配置-XX:HeapDumpPath=/xxx/dump.hprof来 dump fullGC 相关的文件,并通过 jinfo 来进行 gc 前后的 dump

  • jinfo -flag +HeapDumpBeforeFullGC pid
  • jinfo -flag +HeapDumpAfterFullGC pid

堆外内存

堆外内存溢出表现就是物理常驻内存增长快,报错的话视使用方式都不确定

  • 如果由于使用 Netty 导致的,错误日志里可能会出现OutOfDirectMemoryError错误
  • 如果直接是 DirectByteBuffer,那会报OutOfMemoryError: Direct buffer memory。

堆外内存溢出往往是和 NIO 的使用相关,通过 pmap 来查看进程占用的内存情况,隔段时间采样一次,看看是否增长的过快

pmap -x pid

精确的分析堆外内存的步骤:

    1. 在启动参数中加入 -XX:NativeMemoryTracking=detail
    1. 为堆外内存设置一个基准值,就是做一个当前的快照jcmd pid VM.native_memory baseline
    1. 过一段时间后,采样并与快照进行对比jcmd pid VM.native_memory detail.diff

jcmd 分析出来的内存十分详细,包括堆内、线程以及 gc(所以上述其他内存异常其实都可以用 nmt 来分析),这边堆外内存我们重点关注 Internal 的内存增长,如果增长十分明显的话那就是有问题了。

不过其实上面那些操作也很难定位到具体的问题点,关键还是要看错误日志栈,找到可疑的对象,搞清楚它的回收机制,然后去分析对应的对象。

DirectByteBuffer对象是需要 full GC 或者手动 system.gc 来进行回收的那么其实我们可以跟踪一下 DirectByteBuffer 对象的内存情况。

  • 手动触发full gc,查看堆外内存是否有变化,如果被回收了,大概率是堆外内存分配的太小,通过-XX:MaxDirectMemorySize进行调整,如果没什么变化,就要使用 jmap 去分析那些不能被 gc 的对象,以及和 DirectByteBuffer 之间的引用关系。

    手动触发full GC有两种方式:

    • jmap -histo:live pid
    • jmap -dump:file=filename pid

网络

在TCP网络通信时,server端维护两个队列:syns queue(半连接队列)、accept queue(全连接队列)。

  • 第一次握手,在 server 收到 client 的 syn 后,把消息放到 syns queue
  • 第二次握手,server 回复 syn+ack 给 client
  • 第三次握手,server 收到 client 的 ack,如果这时 accept queue 没满,那就从 syns queue 拿出socket_fd放入 accept queue 中,否则按 tcp_abort_on_overflow 指示的执行。
    • tcp_abort_on_overflow 0 表示如果accept queue 满了那么 server 扔掉 client 发过来的 ack
    • tcp_abort_on_overflow 1表示如果accept queue 满了那么 server 一个RST包给 client(RST 包表示连接重置,用于关闭一些无用的连接,通常表示异常关闭,区别于四次挥手的FIN包),表示废掉这个握手过程和这个连接,此时就出现了错误日志connection reset / connection reset by peer。

因此,在实际的开发中,需要判断全连接队列是否满了,查看全连接队列的大小。

ss命令,查看全连接队列的使用量

Recv-Q表示全连接队列当前使用了多少,Send-Q表示全连接队列最大的容量。

ubuntu@VM-1-40-ubuntu:~$ ss -lnt
State       Recv-Q Send-Q      Local Address:Port   Peer Address:Port 
LISTEN      0      100         127.0.0.1:3658       *:*     
LISTEN      0      10          *:8778               *:*  

netstat命令,查看全连接队列的历史溢出数

overflowed表示全连接队列溢出的次数,sockets dropped表示半连接队列溢出的次数。

ubuntu@VM-1-40-ubuntu:~$ netstat -s | egrep "listen|LISTEN
99 times the listen queue of a socket overflowed
99 SYNS to LISTEN sockets dropped

在Tomcat容器中,全连接数量可以通过acceptCount参数设置。在 Jetty 里面则是acceptQueueSize。

RST包

RST 包表示连接重置,用于关闭一些无用的连接,通常表示异常关闭,区别于四次挥手的FIN包,在一个已关闭的连接上读操作会报connection reset,而在一个已关闭的连接上写操作则会报connection reset by peer。

TIME_WAIT
time_wait 的存在一是为了丢失的数据包被后面连接复用,二是为了在 2MSL 的时间范围内正常关闭连接。它的存在其实会大大减少 RST 包的出现。

打开SOKCET_REUSE会关闭TIME_WAIT。也可以设置内核参数:

net.ipv4.tcp_tw_reuse = 1,表示重用tcp连接,默认为0表示关闭

net.ipv4.tcp_tw_recycle = 1,表示快速回收TIME_WAIT状态的sockets,默认为0表示关闭。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值