JVM遇到问题不要慌

当一个系统发生OOM的时候,行为可能会让你感到非常困惑。因为JVM是运行在操作系统上的,故障排查是一个综合性的技术问题,在日常工作中要增加自己的知识广度。多总结、多思考、多记录,这才是正确的晋级方式。
现在的互联网服务,一般做了负载均衡。如果一个实例发生了问题,不要着急去重启,万能的重启会暂时缓解问题,但如果不能保留现场,可能会错失了解决问题的根本,担心的事迟早还会来。
所以当实例发生问题的时候,第一步就是隔离,第二步才是问题排查,下面讲一下如何一步一步的排查问题,在排查过程中,涉及到非常多的Linux命令,对JVM故障排查的帮助非常大,可以逐个击破。

1、GC引起CPU飙升

单节点程序在运行一段时间后,CPU的使用会飙升,一旦飙升,一般怀疑某个业务逻辑的计算量太大,或者触发了死循环(比如HashMap高并发引起的死循环),但排查到最后其实都是GC的问题。
在Linux上,分析那个线程引起的CPU问题,通常有个固定的步骤,下面来分解下这个过程,


JVM遇到问题不要慌_第1张图片
固定步骤

(1)使用top命令,查找到使用CPU最多的进程,并记录进程号(pid)
(2)再次使用top命令 加-Hp参数(H显示线程id,p指定pid),查看某个进程中使用CPU最多的某个线程,记录线程的ID top -Hp pid
(3)使用printf 函数将十进制的tid转化成十六进制 pirntf %x pid
(4)使用jstack命令,查看Java进程的线程栈 jstack pid>pid.log
(5)使用less命令查看生成的文件,并查找刚才转化的十六进制tid,找到发生问题的线程上下文 less $pid.log

接下来的具体问题排查,就需要把内存dump下来,使用MAT等工具进行分析具体原因

2、保留现场

现场保留可以使用自动化方式将必要的信息保存下来,那一般在线上系统会保留那些信息呢?下面来总结下

2.1、瞬时态和历史态

瞬时态是指当时发生的、快照类型的元素;历史态是指按照频率抓取的,有固定监控项的资源变动图。
有很多信息,比如CPU、系统内存等,瞬时态的价值就不如历史态来的直观一些。因为瞬时态无法体现一个趋势性的问题(比如斜率、求导等),而这些信息的获取一般依靠监控系统的协作。
但对于lsof heap等,这种没有时间序列概念的混杂信息,体积都比较大,无法进入监控系统产生价值,就只能通过瞬时态进行分析。在这种情况下,瞬时态的价值反而更大一些,我们常见的堆快照,就属于瞬时态。
问题不是凭空产生的,在分析时,一般要收集系统的整体变更集合,比如代码变更、网络变更,甚至数据量的变更。

2.2、保留信息

1)系统当前网络连接
ss -antp>DUMP_DIR/ss.dump 2>&1
其中,ss命令将系统的所有网络连接输入到ss.dump文件中。使用ss命令而不是netstat的原因是,因为netstat在网络连接非常多的情况下,执行非常缓慢。后续的处理可以通过查看网络连接状态的梳理,来排查TIME_WAIT和CLOSE-WAIT,或者其它连接过高的问题,非常有用。
线上有个系统更新后,监控到CLOSE-WAIT的状态突增,最后整个JVM都无法响应。CLOSE-WAIT状态的产生一般都是代码的问题,使用jstack最终定位到是因为HttpClient的不当使用而引起的,多个连接不完全主动关闭。
2)网络状态统计
netstat -s >DUMP_DIR/netstat-s.dump 2>&1
此命令将网络通缉状态输出到netstat-s.dump文件中。它能够按照各个协议进行统计输出。对把握当时整个网络状态,有非常大的作用。
sar -n DEV 1 2 >DUMP_DIR/sar-traffic.dump 2>&1
上面这个命令,会使用sar输出当前网络流量。在一些速度非常高的模块上,比如Redis、Kafka,就经常发生跑满网卡的情况,如果你的Java程序和它们在一起运行,资源则会被挤占,变现形式就是网络通信非常缓慢。
3)进程资源
lsof -p PID >DUMP_DIR/lsof-$pid.dump 2>&1
这个是一个非常强大的命令,通过查看进程,能看到打开了那些文件,这是一个神器,可以以进程的维度查看整个资源的使用情况,包括每条网络连接、每个打开的文件句柄。同时,也可以很容易的看到连接到了那些服务器、使用了那些资源。这个命令在资源非常多的情况下,输出稍慢,请耐心等待。
4)CPU资源
mpstat >DUMP_DIR/mpstat.dump 2>&1
vmstat 1 3 >DUMP_DIR/vmstat.dump 2>&1
sar -p ALL >DUMP_DIR/sar-cpu.dump 2>&1
uptime >DUMP_DIR/uptime.dump 2>&1
上述命令主要用于输出当前系统的CPU和负载,便于时候排查,这几个命令的功能,有不少重合,使用者要注意甄别
5)I/O资源
iostat -x >DUMP_DIR/iostat.dump 2>&1
一般,以计算为主的服务节点,I/O资源会比较正常,但有时会发生问题,比如日志输出过多,或者磁盘问题等。此命令可以输出每块磁盘的基本性能信息,用来排查I/O问题。
6)内存问题
free -h >DUMP_DIR/free.dump 2>&1
free命令能够大体展现操作系统的内存概况,这是排查故障中一个非常重要的点,比如SWAP影响了GC,SLAB区挤占了JVM的内存。
7)其他全局
ps -df >DUMP_DIR/ps.dump 2>&1
dmesg >DUMP_DIR/dmsesg.dump 2>&1
sysctl -a >DUMP_DIR/sysctl.dump 2>&1
dmsg是许多静悄悄死掉的服务留下的最后一点线索。当然ps 作为执行频率最高的一个命令,它当时的输出信息,也必然有一些可以参考的价值。另外,由于内核的配置参数,会对系统和JVM产生影响,所以也输出了一份。
8)进程快照,最后的一言(jinfo)
{JDK_BIN)jinfo PID >DUMP_DIR/jinfo.dump 2>&1
此命令将输出Java的基本精诚信息,包括环境变量和参数配置,可以查看是否因为一些错误的配置导致的JVM问题
9)dump 堆信息
{JDK_BIN}jstat -gcutil PID >DUMP_DIR/jstat-gcutil.dump 2>&1
{JDK_BIN}jstat -gccapacity PID >DUMP_DIR/jstat-gccapacity.dump 2>&1
jstat 将输出当前的GC信息,一般基本能大体看出问题所在,如果不能,可将借助jmap来进行分析
10)堆信息
{JDK_BIN}jmap PID >DUMP_DIR/jmap.dump 2>&1
{JDK_BIN}jmap -heap PID >DUMP_DIR/jmap-heap.dump 2>&1
{JDK_BIN}jmap -histo PID>DUMP_DIR/jmap-histo.dump 2>&1
{JDK_BIN}jmap -dump:format=b,file=DUMP_DIR.heap.bin PID >/dev/null 2>&1
jmap 将会得到当前Java进程的dump信息,
最有用的就是第四个命令,一般产生的文件都非常的大,而且需要下载下来,导入MAT这样的工具进行深入分析,才能获取结果,这是分析内存泄漏的一个必经的过程。
11)JVM执行栈
{JDK_BIN}jstack PID 》DUMP_DIR/jstack.dump 2>&1
jstack 将会获取当前的执行栈。一般会多次取值,这些信息非常有用,能够还原Java进程中的线程情况
top -Hp PID -b -n 1 -c >DUMP_DIR/top-PID.dump 2>&1
为了能够更加精细的信息,使用top命令,来获取进程中所有线程的CPU信息,这样,就可以看到资源消耗在什么地方了。

3、内存泄漏的现象

jmap -heap -pid
jmap -pid
jmap -histo pid
jmap -binaryheap -pid

-heap 参数能够帮我们看到大体的内存布局,以及每一个年代中的内存情况。和之前介绍的内存布局以及在VisualVM中看到的没有什么不同。但由于它是命令行,所以使用更加广泛。

-histo 能够大概的看到系统中每一种类型占用的空间的大小,用于初步判断问题,比如某个对象的instances数量很小,但占用的空间很大。这就说明存在大对象。但它也只能看大概的问题,要找具体原因,还是要dump出当前live的对象。

一般内存溢出,变现形式就是Old区的占用持续上升,即使经过了多轮GC也没有明显改善。例如GC Roots,内存泄漏的根本就是,有些对象并没有切断和GC Roots的关系,可通过一些工具,能够看到它们的联系。

4、内存泄漏

内存溢出是一个结果,内存泄漏是一个原因。内存溢出的原因有内存空间不足、配置错误等因素。
不再被使用的对象、没有被回收、没有及时切断与GC Roots的联系,这就是内存泄漏。内存泄漏是一些错误的编程方式,或者过多的无用对象创建引起的。
举个例子,有团队使用了HashMap做缓存,但是并没有设置超时时间或者LRU策略,造成了放入Map对象的数据越来越多,而产生了内存泄漏。
再来看一个经常发生内存泄漏的例子,也是由HashMap导致的。代码如下,由于没有重写Key类的hashCode和equals方法,造成了放入HashMap的所有对象都无法被取出来,它们和外界失联了,所以执行结果就是null

public static class Key{
    String title;
    public Key(String title) {
        this.title = title;
    }
}

public static void main(String[] args) {
    Map map = new HashMap();
    map.put(new Key("1"), 1);
    map.put(new Key("2"), 3);
    map.put(new Key("3"), 4);
    Integer integer = map.get(new Key("1"));
    System.out.println(integer);
}

即使提供了equals方法和hashCode方法也要非常小心,尽量避免使用自定义的对象作为key。
再看一个例子,关于文件处理器的应用,在读取或者写入一些文件之后,由于发生了一些异常,close方法又没有放在finally块里面,造成了文件句柄的泄漏。由于文件处理十分频繁,产生了严重的内存泄漏问题。
另外对JavaAPI的一些不当使用,也会造成内存泄漏。很多人喜欢使用String的intern方法,但如果字符串本身是一个非常长的字符串,而且创建之后不再被使用,则会造成内存泄漏。
static String getLongStr() {
StringBuilder sb = new StringBuilder();
for(int i=0;i<10000;i++) {
sb.append(UUID.randomUUID());
}
return sb.toString();
}
public static void main(String [] args) {
while(true) {
System.out.println(getLongStr().intern());
}
}

总结

这节总结了很多非常使用的Linux命令,用于定位分析问题,所有的命令都是可以实际操作的,能够让你详细地把握整个JVM乃至操作系统的运行状况。其中,jinfo、jstat、jstack、jmap等是经常使用的一些工具,尤其是jmap,在分析处理内存泄漏的时候,是必须的,
详细看了下内存泄漏的概念和几个实际的例子,从例子中能明显的看到内存泄漏的结果,但是反向去找这些问题代码就不是那么容易了,后面在慢慢学习吧!

你可能感兴趣的:(JVM遇到问题不要慌)