java问题的排查这么多年几乎没有什么改进,还是老的方法;每年查的问题也都很类似,不会有什么太多特殊的问题出现;java一些很常见的问题基本可以用一个模式去解的(大部分问题,只是有些问题比较复杂)
所有的性能优化问题跟排查一个故障没有太大区别,因为最终都体现在一些系统指标上;
查java问题要先知道系统的指标,因为所有的异常通常来讲系统指标都会有体现;
一、排查java问题要先知道的
1.系统指标查看
1.1 top
可以查看实时的每个核的状况
1.2 tsvmstat
好处是可以查看分钟级,不管是cpu、内存、网卡,可以查历史的每一分钟的各项指标,对历史问题非常有帮助,只是不能看每个核的运行状况;
这两个是最重要的,所有的性能优化体现在系统上基本上都会体现在某个指标上;如果业务代码还可以,则会把某个硬件指标跑满,如果业务代码有问题,则硬件所有指标都没跑满,应用挂了。
2.要关注
2.1 cpu
2.1.1 us大多是应用本身的消耗
java应用压力越大,我们追求的通常是us越高越好,这样说明主要消耗都在业务上,才说明业务代码写得还不错;
2.1.2 sy 上下文切换和内核的消耗
java应用的cpu的sy不应该太高,因为java应用主要是线程上下文的切换,内核层面会有一点消耗,但通常不多,如果sy很高的话,通常是因为高并发程序几个线程抢锁抢得非常厉害;
2.1.3 iowait 磁盘
除非是用java做存储相关,做业务层面一般不会碰到io问题;
2.1.4 si 软中断,通常是网卡中断处理
通常可以把si压到瓶颈是网卡的中断处理,就是网卡中断处理不过来,目前认为这种在硬件情况下无法突破,已经说明你的软件写得非常好;
2.2 网卡带宽
cpu几乎可以展现所有的硬件状况,除了网卡带宽,用专用工具(tsvmstat)查看,通常不会把网卡跑满,除非有拉数据。
所以不管是做性能优化还是查问题之前,一定要看系统指标;
二、java常见问题
NoSuchMethodException、OutOfMemoryError、CPU us高、应用没响应、java进程没了
有可能本地没碰到NoSuchMethodException,但线上碰到了,这个挺正常的;几乎所有java开发人员都会碰到OOM,不管是性能优化还是故障排查,通常我们的应用最后都会变成围绕CPU us去解决问题;
1.NoSuchMethodException
1.1 出现这种现象的原因
1.1.1 java ClassLoader机制
跟类加载有很大关系,它不是一个很好的机制,它有很多的问题,比如说启动完它会分几个目录分别加载jar包,这样就碰到jar版本冲突问题;
1.1.2 java里让人极度头疼的jar版本冲突问题
工程引用了A和B,A和B又都引用了C,但引用C的版本不同(groupId相同),这种问题maven就能解决,java里碰到的通常不是这种,这种编译就搞定了;通常碰到的是很多开源的框架很讨厌,他们的做法是依赖一个jar包时,把jar包里所有的代码拷贝到自己的代码里,然后打成他们自己的包,这种情况maven就不可能知道了,这种情况几乎所有的开源框架都干,这个是java里面很难解决的问题,因为冲突的问题很正常,而且有些根本不是你造成的,一旦出现就很容易出现NoSuchMethodException,就是有可能你用了一个新版本的方法,由于它加载了老的可能就会找不到,这种情况可能导致生产环境发布时发布的环境一模一样,有几台正常,有几台却发布失败,是因为java在加载一个目录下所有jar包的顺序完全取决于OS,而linux系统完全取决于inode的顺序,而inode的顺序不完全能控制;这个问题太麻烦了,没人去解,理论上正确的解决方法是java加载jar包时是带加载自己的顺序去加载;有时候碰到很诡异的问题都是可以被解释的,也很正常;
1.2 同类型的问题
ClassNotFoundException/NoClassDefFoundError/ClassCastException
1.3 排查方法
1.3.1 -XX:+TraceClassLoading
会打印出类是从哪个jar包加载的,如果有问题的话,就是那个位置不是你想的那个位置,需要修改配置并重启;
1.3.2 jar -tvf *.jar
如果对应用的运行机制很清楚,应用通常来讲都是tomcat或者jboss,意味着jar包都会从tomcat自己的rive下或者应用的web-inf目录下加载,如果你很清楚,你可以解压所有的jar,tvf不会展开只是一个列表而已,打印出所有的类,查看是否有同样包名同样类名的东西在两个jar包里都有,如果他们两个md5 sum出来不一样,则说明这两个版本冲突,冲突说明环境一直存在风险,全部都存在风险;版本冲突不会死人,最多就是一点点问题;碰到问题会解就好,通常就直接用jar -tvf *.jar看哪个class冲突了;这个时候就可以写个脚本自动的查看有没有两个类冲突的问题; 2356
1.4 解决方法
1.4.1 mvn pom里去除不需要的版本provided;开源干的坏事那种只能作一个bugfix版本,只能这样;
1.4.2 在打包阶段就尽可能避免掉版本冲突问题
类加载相关的所有问题在java里都不算太难解,虽然不大难查问题,但解决起来稍微会有点复杂,就看问题有多麻烦;
2.1 GC overhead limit exceeded/Java Heap Space
最常见的OOM后面跟的原因描述通常会是这两种
2.1.1出现这两种现象的原因:java heap分配不出需要的内存了
java heap(-Xms -Xmx)包括eden/survivor/survivor/OldGen;NewGen(-Xmn,-XX:SurvivorRatio)包括Eden、Survivor0、Survivor1,PermGen(-XX:PermSize,-XX:MaxPermSize)
一个jvm内存分为java heap和c heap,7(含)以前perm gen还是在java heap中,7以后就移到c heap中了,大家更多接触的是java heap.-Xms -Xmx不允许设置成一样的值,否则会平白无故增加非常多的GC,比如说内存到了一定大小,它觉得不大够用了,还不到最大值,这个时候它会gc,把内存放大,它觉得要缩小了,也会gc,用来缩小;
2.1.2 排查方法(确定不是因为heap size大小的情况下)
既然java heap满了,我们就要知道这个堆被谁用掉,如果知道被谁用掉,这个问题理论上就可以被解决了,如果要知道堆被谁用掉,首先要拿到heap dump文件,否则谁也无法解决问题;这里说是oom不是cpu load高的问题;
a.拿到HeapDump文件
a.1 -XX:+HeapDumpOnOutOfMemoryError
这个启动参数只会在第一次OOM时产生dump文件(文件名以.hprof结尾),在target或者工作目录下,后面再报OOM,就不会生成了,它认为已经生成了,所以有可能第一次dump的文件没用,要后面的才行,也就是抓不到现场;如果登陆机器时正在OOM(有些应用一直在OOM),可执行jmap -dump
a.2 jmap -dump:file=<文件名>,format=b [pid]
如果正在执行gc,可执行此命令,执行时会强制执行一次Full GC(正是由于这个原因,不要在线上随便执行这个命令,FullGC耗时很多,可能导致线上应用挂掉),但有可能有时dump不出来,碰到了也很正常,那就不用强行dump了,没什么意思,这时可尝试一下用gcore
a.3 gcore [pid]
生成c版core dump,再运行jmap -dump从core dump提取出java的heap dump,jmap -dump:format=b,file=heap.hprof java路径 core.dump文件,最好的做法是用gcore,而不是jmap -dump,gcore特别快,jmap特别慢,因为jmap -dump除了会引发FullGC也会生成一个与java heap同大小的文件 ,java heap 有8g,生成的文件就有8g,java heap有100g时OOM就别查了,因为它会生成一个100g的文件,这是java排查问题一个致使的弱点,目前为止,业界也没有很好的解决方案;多数OOM的问题重启(绝对大招)是可以解决的,只是重启问题在于哪天又会出现。
b.分析HeapDump文件
zprofiler - Heap Dump 分析视图- 对象簇视图;
难道heap dump文件不能直接看,一定要用zprofiler分析查看?
通常来讲java自带的jmap -histo 真心没啥用,因为前4个基本是char[] byte int 除非不是这4个,比如char[]占了5g内存也不知道谁占的,真心没啥用,只有一次查问题,很幸运地看到排在前面是com.开关的一个类,但这个几乎不会出现;初学都写的代码就不一定了。。。
分析快慢取决于heap大小和对象的引用关系,对象簇视图的好处是可以看到哪个线程占的内存最多;如果哪个线程占的多,则可以进一步点知道是哪个类占的最多,如果知道是哪个类占最多,是可以查问题的,但通常不是哪个类,而是java.lang.ArrayList之类,也不知道是谁创建的,但有时可以根据数据里的内容知道是谁干的,但通常看不出来;这种情况下就要借助java的一个神器btrace;
c.根据zprofiler分析的结果来定位到代码
btrace
绝对是java里最经典的一个神器,这么多年都没改过,没有它的时候即使知道AarrayList占的最多也无能为力,因为根本不知道是代码哪个地方产生出来的,只能碰运气,瞎蒙,用btrace就非常简单;它可以在java在运行的时候在另外一个地方挂一个脚本,可以直接trace这个java进程现在在干什么,可以trace所有的动作,比如说谁在创建ArrayList或者谁在创建一个size>1000的ArraList,还可以trace其他很多东西,惟一不能改变是入参和出参,建议所有的java人员都去玩一下,至少不用等到出问题的时候再去学这个东西,那基本是在玩。在生产环境不要随便btrace,如用btrace去跟hashmap.put,一跟屏幕就一直刷根本停不住,业务就挂死了,最好加一些过滤条件,不要让它什么都去跟;它非常的强大,有时做性能优化也会用它,因为它可以跟踪进入一个方法消耗的时间,业界也有一些人写了一些脚本可以让方法的消耗时间变成树状。前面都是铺垫,用btrace来终结,比如用btrace查出来是哪个代码创建那么大一个ArrayList,剩下就简单了,就限制一下不要这么大啊之类的;
通常来讲,最常见的OOM按这个方法,多数问题都会被解决掉;
d. 分析HeapDump文件时可能会碰到
d.1 (dump文件)占用的内存并不多却OOM
d.1.1 突然分配了一个巨大的对象(一般是数组)grep -i ‘allocating large’ 日志文件
此时alij会在日志里打一行日志,关键字是’allocating large’(有个阀值可设置)
d.1.2 死循环(jstack)
死循环也可能引起OOM(jstack),一个循环里不断在创建自己的对象,但dump时却没有这个对象,dump文件也比较小,因为OOM时线程已经退出了,引用关系已经没有了,所以dump完之后这个对象已经不存活了,所以什么都看不到,每次dump都是没有的,因为每次dump都在OOM后面,这次很幸运查到是因为写定时脚本每3秒jmap -histo,捞了一天的日志,偶尔在里面发现几条排第一的是com.**开头的,剩下就很简单了,然后用btrace跟一下谁在创建这个对象。
通常来讲,如果碰到heap dump出来大小占用不多,但是又报OOM,可以尝试这两种思路分析一下,另外,很多新手会认为OOM时报的堆栈就是OOM的原因,其实多数没有一毛钱关系,因为OOM多数不是一个地方造成的,是一个累积的效应;如果是java heap space的问题,基本上用这两个方法可以解决所有的问题。
2.1.3 解决方法
a. 根据定位到的消耗较多内存的代码,针对性的处理
例如:自增长的数据结构对象没限制大小(90%);
引用未释放,造成内存泄露(这种原因已经不多了,10%);
Collection里所有都是自增长的,除了列队Queue之类会有大小限制,其它都是new一个多数人还不传参数,让它自己往上涨,如有个HashMap太大就可能导致OOM,90%的OOM都是这种原因,不可能放这么多的,事实就是放了这么多,
OOM里java heap space问题是最容易解的,并不复杂,多数人只要练几次手就会了,不会是因为从来没见过,但不用等生产环境,想造成这个现象太容易了,随便几行代码分分钟就OOM,如果想真正学会的话,应该自己造OOM,能造OOM的人显然比会解决OOM的人还厉害,能造的好说明对内存控制管理理解的非常深,大家可以去玩一下;
2.2 Unable to create new native thread
出现这种现象的机会不多,但偶尔会碰到
2.2.1 出现这个现象的原因
有很多种原因,如abc
a.线程数超过了ulimit限制
a.1 会导致ps、ls等出现resource temporarily unavailable
因为linux系统会默认给每个用户作资源限制,如一个用户最多创建2000个线程,超2000就会告诉你这个错,想看到这个错一点都不难,ulimit -u 100,程序创建101个线程,立刻就会看到这个现象;因为ps/ls这些也是进程或者线程也创建不出来了,一旦出现什么都不能干了,全崩盘;这时可以用别的账号登进去把那个用户的线程数进程数改掉就立刻恢复了;如果想check下,用ulimit -a,但ulimit -a看到的不一定是真相,有可能是假的,原因在于linux除了用户去限制线程数还可以在进程上直接限制,比如说可以在应用的启动脚本里写ulimit -u限制最大线程数,这些ulimit -a是看不到的,最好的方法是cat /proc/[pid]/limits,这个文件里面的信息全部是对的,就是真实的这个进程被限制了多少
a.2 出现时可以先临时调整下ulimit,以便能操作;
b.线程数超过了kernel.pid_max
这是第二种原因,应用并没有超过ulimit,但是超过了内核层面的限制,用sysctl -a | grep kernel.pid_max可以查看这个参数,这个参数作用是告诉linux进程的pid这个数字的最大限制是多少,linux老版本是65536,应用是跑在虚拟化体系里,很多个应用加起来有可能会超过这个数;一旦出现,在这台机器执行所有命令都没有用,不管有没有换账号登陆,惟一能做的就是重启,这就是要搞垮一台linux太简单了,线程数直接超过这台Linux就完蛋了,所以线上想搞挂,分分钟就挂了;
b.1 会导致执行ps、ls等出现resource temporarily unavailable,pid超过最大限制;
b.2 只能重启,只能是带外,因为你登不进去了
c.机器完全没有内存了
这种可能性几乎不会存在,java有一个启动参数-Xss它的默认值是1M,但它的意思是java的线程栈的大小最多是1M,不是只要创建一个线程就会耗掉1M内存,这是个误区,如果超过1M,就会在日志里看到stack overflow的异常;机器上完全没有内存去创建线程,可能性太小了;除非代码有严重bug,在不断的创建线程
2.2.2排查方法
a. ps -eLf | grep java -c
统计java进程创建了多少个线程;
b.cat /proc/[pid]/limits
如果太小,可以调大点max open processes值;
c.sysctl -a | grep kernel.pid_max;
d.如果真的是线程创建的太多了,不是系统参数造成的
d.1 jstack有线程名的话通常会比较好排查;
dump这个java进程的线程在干什么,线程椎栈,看一下是不是有很多的线程
d.2 btrace
new Thread || new ThreadPoolExecutor 方式创建的线程用jstack的话线程的名字不会有人看得懂,要起一个线程,一定要给线程起个名字,优良的习惯。不起名字在没有btrace的时候还真没法查;用btrace去跟谁在创建一个线程池或者线程,它可以跟一个方法的初始化;
2.2.3 解决方法(跟自增长数据结构的问题一样)
a. 用线程池也必须限制大小
建议不用newCachedThreadPool(它可以创建int型的最大值数量的线程)或者Executors.new*,而必须用newThreadPoolExecutor,这样的话你可以清清楚楚的知道你传的每个参数都是什么意思,不管的话不知道线程会发生什么,所以必须用最原始的api;
b. 对使用到的API一定要非常清楚
开源界的很多东西的api设计的很多都不是那么理想的,例如最著名的netty,netty的代码写非常非常好了,但它的api设计也很烂(api如果随便用会导致系统挂掉),虽然文档写了不能那么用,但多数人不会看,很容易误用的netty client有一个类,不是单态,每new一次,背后都会创建一个线程池,这样每次请求new一下都会创建线程池,线程池又不会退出,越来越多,有一天就挂了。
2.3 PermGen Space
2.3.1 出现这个问题的原因
a.PermGen被用满
PermGen的使用和回收:java每加载一个类的时候,就会把一个类的所有信息写进一个叫PermGen的地方,里面包括的类的名字、方法的名字、方法体等,简单的可以认为,类加载的越多,PermGen越大;如spring的应用喜欢一个类生成一个新的类来增强功能,所以说spring是导致PermGen变大的一个主要原因,另外还有groovy,groovy默认的情况下每装载一次就会生成一个新类名一个新的类,所以需要自己做一个cache来判断这一次我传给groovy的脚本是一样的,那就不应该生成一个新的类,应该直接做一个缓存,所以说开源的东西在稳定性方面还是有差距的。PermGen一旦被用满,在8以前就会触发一次FullGC,一旦它满,后果很严重,它一满就是FullGC。如果碰到就把这个区域调大一点,再论证是不是每次都装载造成的泄漏,在8以后叫做MetaSpace size。
2.3.2 排查方法
a. btrace
因为造成PermGen满的原因通常是装载的类太多了,所以PermGen出现问题很容易查,用btrace跟踪一下谁调用了ClassLoader.defineClass,因为ClassLoader虽然是可以自定义的(一般会有人写新的ClassLoader)但一般不会重写defineClass方法,这个方法是作用是把字节码装载进来并且还原成一个Class对象,除非要在字节码上做特殊动作才会改这个方法,否则没有人会动它的。所以,btrace几乎可以跟踪java的所有的问题,因为所有的api最后都会收敛到jdk的某一个方法,通过那个方法就可以跟踪所有的问题,就像PermGen就可以用btrace跟踪defineClass就知道是谁在创建Class。
2.3.3 解决方法
a.确认是不是真的需要装载那么多,如果是就调大PermSize
b.如果不是,控制ClassLoader,目前所有场景中常见于groovy的误用,目前常见的这种错误都是groovy造成的。跟groovy的api设计的不是很好有关。groovy的性能是没有问题的,groovy最后会转化成一个class文件,会正常的被jvm装载,剩下跟一个普通的java类没有任何区别。
2.4 Direct buffer memory
写通讯类的程序的人会看到的比较多一点,因为不管是netty还是等等,它背后在做网卡的字节流处理的时候都是用 Direct buffer memory去做处理的。
2.4.1 出现这个现象的原因
a.Direct ByteBuffer使用超出了限制的大小
a.1 默认的大小为-Xmx jinfo -flags
Direct Byte Buffer的区域在java heap 的外面,不占用java堆的大小,默认的大小为-Xmx,就是java heap多大它就默认多大,如果一台vm8核16g,java heap设成8g,有可能没有设 Direct buffer memory的大小,默认它也是8g,那就有可能java heap被用满时Direct ByteBuffer也用了8g,内存就被用光了,此时vm就挂了,这种现象碰到很多次;
如果大家想知道任何一个java启动参数(就是运行时真正的值)是多少,就用jinfo -flags [pid],它神奇的地方在于jinfo -flag(没有s)甚至可以动态的改变一个启动参数的值,但只能改部分;
a.2 java中只能通过ByteBuffer.allocateDirect来使用Direct ByteBuffer
2.4.2 排查方法
a.btrace
跟踪ByteBuffer.allocateDirect就可以了
2.4.3 解决方法
a. 如果真的不够用了,在内存够用的情况下可以调大-XX:MaxDirectMemorySize,如果生产环境还没有这个参数的话,建议加上,通常来讲设成1g就够了,否则也许有一天java就不见了;
b.常见的是类似网络通信框架未做限流这种
2.5 Map failed
2.5.1 出现这种现象的原因
a. FileChannel mapped的文件超出了系统参数大小 vm.max_map_count
java可以把一个文件映射到内存里面去,可以map到内存,
2.5.2 排查方法
a. btrace
映射文件在java里最后一定会调用FileChannel.map,用btrace跟踪FileChannel.map方法即可;
b. 看看是不是加了-XX:+DisableExplicitGC参数,
生产环境通常加了这个参数,用来避免自己在代码里调用System.gc,java比程序员更清楚啥时候要gc,不需要别人告诉它,System.gc是个异步方法,不知道啥时候返回,有时候会在Direct ByteBuffer或者FilerChannel的OOM的catch代码中看到System.gc;Thread.sleep(3s);其实它也不知道3s后gc有没有执行,它的本来意思OOM了,就触发一次gc,搞不好就回收了,关键是睡3s后gc不一定能执行完,只是在OOM的时候碰运气而已,但一定要碰;
2.5.3 解决方法
如果需要做把文件map到内存的话,就要注意下,如果不用做,就基本不会碰这个问题;
a. 有必要的话调大 vm.max_map_count
b. 如map file存活个数其实不多则去掉-XX:+DisableExplicitGC
在CMS GC的情况下,增加-XX:+ExplicitGCInvokesConcurrent.
2.6 request {} bytes for {},Out of swap space?
能看到这行日志,肯定不是在java日志里看到的,这个日志只会在另外一个单独的文件,因为这行日志一旦出现java一定会退出,java会crash. 这行日志很容易误导人,会让人觉得swap都不够了,但一看swap都没人用;生成这个文件的时候会同时生成内存的使用状况;
2.6.1 出现这个现象的原因只有两种
a.地址空间不够用
现在的java应用几乎不会出现了,因为现在都是64位;32位倒是有可能是这个原因;
b. 物理内存耗光;
2.6.2 排查方法
a. 物理内存耗光
a.1 按经验
目前经验来看原因有以下两种
a.1.1 btrace
Deflater.init|Deflater.end|Inflater.init|Inflater.end;
自己的代码可能不会调用压缩和解压缩,但依赖的代码可能会调用压缩和解压缩,Deflater/Inflater会在堆外分配一个空间,但这个空间会在自己的end方法去释放,除了end方法以外它还会用finalize方法去释放,finalize是要借助jvm的机制的,理论上jvm应该会调用finalize,但是jvm一直都承认finalize这个地方是有bug的,就是finalize这个方法它可能不会调用,正常情况它会在一次gc做完以后的下一次gc它会调用,但实际有可能是3次以后调,也有可能永远不会调;所以代码如果用到了压缩和解压缩一定要显示的调用end方法,而不是依赖finalize,很多人都不会显示调用,就挂了,但这个是偶然现象不一定会出现的;如果出现可以用btrace去跟一下代码里有没有调用Deflater/Inflater的init和end有没有成对出现;如果没有成对出现,是肯定会有泄漏的;这个地方就会出问题;
a.1.2 强制执行full gc
jmap -histo:live [pid]
第二个经验是强制执行full gc,这行命令加上live就会强制执行full gc;
如果执行几次后内存明显下降,则说明基本是Direct ByteBuffer造成的,就把-XX:MaxDirectMemorySize参数调整一下就可以了;这个参数的作用是当DirectByteBuffer用到这么大是就触发fullgc,它就会回收掉;
怎么判断堆外内存用了多少呢,如果一个java进程用了10g,那么可以认为有8g是java heap,另外2g是堆外内存用的;堆外有很多东西要用,如果看到一个java进程占的内存远超java heap其实是正常的;比如堆外要存入代码编译优化的代码code cache;
如果这两个经验都搞不定的话,就碰到目前比较少碰到的现象,就只能靠自己了;但这一招还是有工具的,java没有,google有,用来解决c的内存分配的优化之类,顺带用来解决java堆外的问题;
a.2 google Perftools
现在改名gPerftools,
2.6.3 解决方法
a. 地址空间不够用
那就升级到64bit;
b. 物理内存耗光
b.1 Inflater/Deflater问题的话则显示调用end
b.2 Direct ByteBuffer问题可以调小-XX:MaxDirectMemorySize
b.3 其他case根据google perftools显示的来跟进
如果启用perftools它会每隔一段时间它会打印一个heap dump,如果是Deflater/Inflater造成的话,heap dump会显示z aroke占用了最多的内存,它显示是c代码,然后再用google查一下java哪个地方会用zaroke,基本就会查出问题;gperftools还可以生成一张堆栈的图片,下载到本地就可以查看;
(装perftools会比较麻烦,要装一些乱七八糟的东西)
我们基本认为java如果碰到OOM的问题,以**的经验来讲,几乎都是可以解决掉的,因为几乎所有的场景都见过,如果自己去学的话,最好是自己把上面的问题全部造出来,面试的话,如果碰到说自己对内存很懂,就让写一段代码,先做一次young gc,再做一次full gc,两做两次young gc,再做一次full gc.
3.CPU us高
这个问题通常会在性能优化的场景碰到
3.1 出现这个现象的原因
3.1.1 CMS GC/Full GC频繁
怎么判断呢,top 1看下,如果总是会出现某一个核耗100%,通常是因为CMS GC/Full GC造成的,更简单的办法是看下gclog
3.1.2.代码中出现非常耗CPU的操作
这种情况可能就是代码写得烂了点
3.1.3.整体代码的消耗
是最复杂的,其实我们认为这样的代码写得挺好,充分利用了cpu,充分利用了并行性,在并发程序上已经是非常完美的代码,这种场景不是解决问题,只是做性能优化
3.2 排查方法
3.2.1 CMS GC/Full GC频繁
a. 查看gc log 或 jstat -gcutil [pid] 1000 10
jstat可以常用一点,因为它是一个数据统计工具,它可以看gc的频率或者java堆的每个区域的大小,不建议用jmap -heap, 在CMS GC时执行jmap -heap java进程就会卡住,然后线上就挂了,当然有一定概率,绝对不允许在线上执行jmap -heap,只允许执行jstat -gc之类,因为效果是一模一样,没有区别;
3.2.2 代码中出现非常耗CPU的操作
a. top -H + jstack,做pid到nid进制转化,printf ‘0x%x’;
这种情况表现是top -H如果看到排在前面的几个线程的id不大变化,就说明就那几个线程在经常耗cpu,这种情况碰到的很少,如果真的碰到就再用jstack(jstack用的是nid)一下就知道这个线程在干什么,如果找到线程在干什么,自然有优化办法;
3.2.3 整体代码的消耗
a. top -H 看到每个线程消耗都差不多,而且排在线程id不断变化.
请求在不断的进来,也是最复杂的情况,剩下就是看看有没有优化空间的问题
b. 变变
这种问题也是最难查的,在外没有什么好的工具,最好的工具是intel的weitan,weitan是收费的,芯片是它家,它想知道cpu在干什么,不能在生产环境使用,自己玩玩没关系;另外的一些工具如jprofile之类,这些工具都是有bug的,你看到的cpu谁耗的最多的那个方程是假的,不一定是真的,因为不到cpu芯片这一级,它看到很多数据不是真实数据;娈变之前是perf,perf是linux自带的,perf与jvm集成的不太好,如果用perf去看的话,会看到unknow占得最多;与变变的联系一下,它会推一个东西,重启压测生成一张图;
cpu用户态的问题其实就是这样,在内用变是可以跟出来的,以目前的经验来看,写到最后,优化的问题差不多会集中在两个地方,一是序列化反序列化,二是gc,当问题集中在这两个地方的时候, 其实没有什么太多好的优化方法,因为序列化目前没有革命性的突破,很难做到cpu消耗很少,因为每次反序列化都要构建那个类的对象,而且要一个字节一个字节来操作,所以没有办法,惟一能做的是看看能不能少做点序列化反序列化;
4.应用没响应
4.1 出现这种现象的典型原因
4.1.1 资源被耗光(CPU,内存),前面的内容已覆盖
4.1.2 死锁
4.1.3 处理线程池耗光
4.2 表现出来可能是
4.2.1 HTTP响应返回499、502、504
4.3 排查方法
4.3.1 死锁
a. jstack -l
死锁在java里面理论上最容易查的,但偶尔会有比较难查的现象;很好查的是jstack -l 之后第一行或者第二行jvm就告诉你线程堆栈里发现了死锁,跳到最后会告诉你哪两个线程持有了那把锁;死锁在一个非常复杂的高并发系统里是很容易出现的,原因在于多数的软件都是多人并行开发的,有可能会甲在这里写了一把锁,乙在写代码的时候根本不知道锁里面又嵌了一把锁; 如果没有看到但又怀疑死锁,另外一个办法可以用pstack,可以看到c的堆栈上面的状况,因为java的很多锁其实不在java的堆栈里而在c的堆栈里,因为所有的jdk代码都会调到c的堆栈上面,比如说jvm在加载类的时候会给类加一把锁,保证这个类只初始化一次,这把锁只能用pstack看到,jstack看不到,如果两边有类加载的死锁现象就只能用pstack去查;有时候也可能jstack没有死锁但其实里面是有死锁的,有时候它的分析可能也有问题,最好是自己稍微翻一下那个线程堆栈,也不是很难看;
b.仔细看线程堆栈信息
4.3.2 处理线程池耗光
这个现象需要对请求进来后业务的处理过程非常了解,可能要经过好几个线程池的处理过程,只要有一个用光
a.jstack
可以看那些线程都在干什么
b.查看从请求进来的路径上经过的处理线程池中的线程状况
例如,netty io threadpool --> business threadpool,像hsf要进到你的业务代码它要经过netty的io线程池,再经过一个业务的线程池,才会跳到你的代码,前面两个线程池都必须够用,只要有一个线程池用光,应用可能就没有响应,或者在压测的时候,硬件资源(cpu/网卡/磁盘)都没有耗满,但性能就是压不上去,这种通常是因为线程池不够用,或者业务代码的锁竞争太激烈;如果是tomcat就会先经过tomcat自己的线程池然后再跳;如果很清楚就很容易查,可以用jstack看那些线程在干什么,如果全部都在干活,通常可能是线程池真的不够用;
4.4 解决方法
4.4.1 死锁
想办法解开锁,例如spring 3.1.14前所有的版本都有死锁bug,就是因为它那段代码是多个人写的,大家串不起来
4.4.2 处理线程池耗光
通常是需要考虑加大线程池or减小超时时间等(这样线程复用的快),在所有的分布式系统里,只要你去访问另外一台机器的地方,如果调用的一端卡住,这边也就卡住了,一定要设超时,否则应用可能卡死那里。
5.java进程退出
5.1 出现这个现象的原因
原因非常非常的多,但是有一些常见的
5.2 排查方法
5.2.1 查看生成的hs_err_pid[pid].log
通常来讲java进程退出后会在工作目录下生成此文件,文件里会描述jvm为什么会退出了,可能看不懂,可能需要翻jvm的代码才知道。
5.2.2 确保core dump已打开(线上全部都会打开的),cat /proc/[pid]/limits
5.2.3 dmesg | grep -i kill
看一下是不是OS把你杀掉,这个vm总共8g内存,如果所有进程把内存用满了,这个时候它会挑选一个把进程杀掉,通常会内存占得最多的那个,通常是java进程被杀,如果被杀的话,dmesg里面会有日志的;如果是这样的原因被杀说明应用内存占得太多,通常这种原因就是前面的Direct ByteBuffer造成的,就是两个相加刚好超过
5.2.4 根据core dump文件做相应的分析(到这一步的话就不建议自己去做了,需要找jvm团队的人了)
a. gdb [java路径] core 文件 (c 调试技巧)
5.3 crash demo
5.3.1 jinfo -flag FLSLargestBlockCoalesceProximity
命令的作用就是让你看一下java是怎么crash的,一执行java立刻就会crash,crash之后就会看到那行日志
5.4 常见的cases
5.4.1 native stack 溢出导致java进程退出的case
如果是java上面的线程栈满了,就会报stackoverflow,如果是native上的栈溢出了,会导致java直接crach,这种情况非常容易判断,它会生成core dump文件,不会生成hs_err_的那个文件,原因是生成这个文件也是要线程堆栈的,那个线程堆栈它生成不出来,所以就挂了,这个很容易判断;如果有core dump文件的话,可以用jstack再把线程堆栈捞出来
5.4.2 编译不了某些代码导致的java进程退出的case
这种问题建议大家不要去查,因为也没法查,即使代码写得一点问题都没有,不知道什么jvm编译不了
-XX:CompileCommand=exclude,the/package/and/Class,methodName
这个启动参数告诉java这个方法别编译,当然对性能有损耗,一个更好的方法是把那个方法拆一下,一般就能通过编译了,如果碰到不能编译,就不要纠结为啥不能编译了。
5.4.3 内存问题导致的进程退出的case
就是刚刚说的
5.4.4 JVM自身bug导致退出的case
jdk6以前bug是相当多的,7以后质量飙升,oracle接管以后大家都觉得java发展变慢了,其实质量变好了很多,速度确实更慢了,而且特性差了
总结一下
知其因
惟手熟尔
在排查所有的java问题,基本上面的问题应该cover掉了大家碰到的大部分问题,在排查所有的java问题时,最重要的第一点是你要知道发生这个问题背后最根本的原因是什么,其实就是要知道java层面到底发生了什么,如果你知道的话,在排查问题的时候,可以根本不用懂上面的业务代码怎么回事,因为所有的问题都会收拢到jvm的几个领域,要么内存要么编译要么其它的,不会逃脱这几个领域,所以最重要是知道原因到底是什么;然后第二点是看谁工具用得熟,两个人,一个比较厉害,一个一般的,两个人的差别可能仅仅是另外一个人工具用得特别熟,可能不用翻资料什么的立刻就敲了,差距通常在这里;在知道这些工具的情况下可以尝试去用一下,不用等线上真的出问题再去练习,完全可以自己造问题;