听《Java常见问题排查》分享笔记
原因一般和Java的ClassLoader机制有关,
常见的问题是Jar包版本冲突问题,有的容易解决,编译时mvn做版本检测,如果版本不一样但是id一样,这样的时候编译会报错。
通常遇到的问题是,很多开源框架依赖了某个jar包,把这个jar包的代码拷过来打成自己的jar包,这时mvn不知道。
有可能用了新版本的方法,在生产环节因为加载了老的方法,就会找不到。
甚至可能出现的奇怪的现象是,生产环节100台机器,里面可能只有5台找不到,环境一模一样。原因是java在装载一个目录下所有jar包时,它加载的顺序完全取决于操作系统!!而Linux的顺序完全取决于INode的顺序,INode的顺序不完全能一致。
(INode讲解: http://www.cnblogs.com/itech/archive/2012/05/15/2502284.html)
95台刚好新版本在前面,另外5台刚好是低版本在前面
淘宝遇到过发布时第一批是正常的,后几批全挂了。。。
理论上最正确的做法是java加载目录时按自己的顺序加载,这样不会出现集群不一致现象,要挂全挂~
这种问题排查起来不难,第一个方法加启动参数,它会把这个类是从哪个jar包加载的打印出来。
有问题的话一般是来源的jar包不符合期望。
第二个方法比较高端,不用加启动参数。
tomcat通常从自己的lib下加载jar包,还有应用WEB-INF/lib下加载jar包。可以去这两个目录下解压(-tvf不会真的解压,只是看列表),打印出所有的类,看是不是有同样包名同样类名的从两个不同的jar包出来,并且他们的md5出来是不一样的,如果如此,就是版本冲突了。
版本冲突意味着这个环境一直是存在风险的,就看命运了。。。
解决方法在mvn中做exclusion,难解决的是别的开源框架把它依赖的打包进去了
尽量在打包阶段中将版本冲突解掉。
OOM主要有七种,一般不会超过这七种异常
最常见的OOM的原因是这两个,GC超过限制和Heap被用满
这张图适用JDK7但是不适用JDK8,因为JDK8中没有PermGen
(参考:JDK8内存模型浅析—消失的PermGen http://www.2cto.com/kf/201603/495746.html
Java 8: 从永久代(PermGen)到元空间(Metaspace) http://blog.csdn.net/zhushuai1221/article/details/52122880)
通常-Xms和-Xmx在生产环节会设置成完全一样,
原因是如果不设置成一样,-Xms小(比如有人出于好心在不用内存时少用点),造成的结果是java内存到达一定大小后,觉得内存不够用了而又没到最大值,会做一遍GC把内存放大。当觉得要缩小,也会先做GC再缩小。
最后因为这两个参数设置不一样,造成了多次无用GC。所以线上生产环节不允许这两个值不同!
当Java堆满了就要知道堆都被谁用了,首先要有内存dump文件。
为了确保OOM后可以拿到dump文件,启动参数上都会加HeapDumpOnOutOfMemoryError。
作用是当java报OOM后会在target目录或工作目录下生成dump文件,结尾是.hprof
但是这个参数的问题是:只会在应用第一次OOM时dump,但是后面再OOM就不用生成了,因为默认已经有了
就有可能第一次OOM时的dump没用,后面的OOM才有用。
当登录的时候幸运地正在OOM,可以手工dump,用jmap -dump这个。但是这个不要随意在线上做!!因为当执行这条命令时,java会强制执行一次fullGC!
有时jmap会dump不出来,就不用强行dump了,这时可以尝试用gcore,先生成core dump,再用jmap从这个core dump中提取出。最好的做法是用gcore而不是jmap,因为gcore特别快,jmap特别慢。
因为jmap执行时除了fullGC,还会扫描整个java的heap,通常java堆有多大,生成的文件就有多大。
这是java排查的致命弱点,heap特别几十G时无法排查,重启就好了。。。业界没有很好地解决方案
拿到dump文件后,需要分析
在阿里可以用Zprofile这个网页版工具,可以在生产环境直接把dump文件拷过去,在Zprofile的机器上分析,这台机子内存有100多G。。。
通常文件分析出来以后需要看对象树的视图,不要用jmap -histo。
对象树视图的好处是可以看到哪个线程占的内存最多。
为了更精确找出问题,需要另一个神器,btrace。
它的作用是可以在java还在运行时,在另外一处挂一个脚本,来trace目前java进程所有动作。
唯一不能做的是不能改参数。建议所有java工程师尝试
但是在生产环境不要随意btrace,有人拿这个跟map的put,导致业务挂死P1故障。
性能优化也可以用btrace,因为可以跟踪进一个方法耗时多久,因为不可能每个方法都打印耗时多久。
前面都是铺垫,btrace才是终结技。
大多问题用以上方法可以解决~
特殊现象也有,系统在报OOM,但是dump后分析出来heap没用多少。出现过的原因是突然分配了个巨大的数组。
这种情况下,阿里版的jdk会打一行日志,关键字是allocating large。如果出现这个,说明分配了巨大数组。
还有一种现象是死循环,
之前出现过应用OOM了,死循环在循环体里不断创建对象,在dump的时候查不到是因为
报OOM的时候,这个线程已经退出了,引用关系已经没有了,对象不存活了。每次dump都在OOM后面导致排查不到。
那次的解决办法是写定时脚本,每三秒jmap -histo一次,从一天日志查。
新手遇到的问题是OOM打印的触发OOM的堆栈,但是很多时候这个堆栈里有关的人并不是导致OOM的人,因为OOM不是一个地方造成的,而是一个累积效应
多数问题是因为自增长的数据结构。
通常用的数据结构比如Collection都是自增长的,比如List<String> list = new ArrayList<String>();
第一次造成的严重故障是淘宝全挂,整个集群OOM,原因是有个hashmap太大了,里面全是用户信息。
阿里很多OOM都是因为这个。
应该线下用代码造OOM,尝试分析解决。
linux会给每个用户做限制,比如每个用户最多创建2000个线程,如果超过了2000就会报这个错。
登上出现这个问题的linux机时,输入-ls等指令都会报没有资源了,原因在于-ls也是创建线程。
机器上ulimit -a可以看到,但看到的不一定是真实的,因为Linux不光可以在用户维度限制,还可以在进程维度限制。
最好的方法是linux pro -pid 里面有linux文件,这个文件里的信息都是对的
内核层面还有个参数kernel.pid_max,pid是个数字,是有最大限制的。超过这个数就悲剧了
首先统计java进程创建了多少线程,ps -eLf | grep java -c可以统计。如果创建太多都是有bug
所有线程一定要起名字!!不然无法排查!!
这个问题和自定义数据结构很像,用线程池时很多人用Executors.newCachedThreadPool,但不明白newCachedThreadPool的意思是线程池最大可以创建int整型最大值个线程。
所以用线程池也必须设置大小!!
搞阿里开发规约建议 在阿里强制不允许用Executors.new.....,应该用new threadpoolExecutor(),清晰知道自己传的每个参数是什么意思。如果都不管的话都不知道自己创建的线程池会发生什么,所以必须用最原始的API。
开源界很多API很烂,不能随便用。
classLoader每装载一个类,都会把这个类的所有信息写进PermGen,包括类的名字方法的名字方法体等等。
类装载越多PermGen占用越多。Spring是导致PermGen变大的原因,因为Spring主要是把一个类生成一个新的类来增强。
Groovy脚本易犯错误是它默认情况下每装载一次,会生成一个新的类名,需要自己做一个cache,如果是一样的就用缓存的。
PermGen用满后,在java8之前会触发fullGC,很慢 全暂停。
java8以后叫 meta space size,另外一个参数控制大小
PermGen原因通常是装载类太多了,所以很容易查。用btrace跟踪谁调了ClassLoader.defineClass方法。
btrace可以跟踪java所有问题,因为所有API最后都会落到jdk上某个方法,跟这个方法的话可以跟到所有问题
比如PermGen可以用跟defineClass方法来追踪,看是谁一直创建Class
通常写通讯类程序的人看这个问题多一点,不管是Netty还是什么,背后在做网卡字节流装载是用Direct ByteBuffer
默认大小是Java堆多大,它设多大。在巧合情况下Java堆用了8G,DirectByteBuffer也用了8G,导致虚拟机挂掉
所有java启动参数的默认值,网上没有一份是正确的。需要用java自动工具去看,jinfo -flags
生产环节建议加这个参数,1G很稳。网络通讯很容易出这种问题
这个问题不是在java日志里看到,只会在另外一个单独文件看到。
出了这个问题,java进程一定已经挂掉了。
地址空间不够用在目64位环境下不会出现了
Deflater和Inflater要成对出现init end方法,如果没有成对出现肯定会泄露,这个方法是用来压缩数据包
(参考:Deflater和Inflater的用法 http://blog.csdn.net/daniel_cao_/article/details/3899597)
最好的方法是用代码把上面的场景都造出来,然后练习处理。
对Java内存掌握考验之一就是写段代码,用这段代码控制youngGC,fullGC并能够控制频率。
性能优化场景碰到。
GC太频繁的判断方法是:用top,看每个核的消耗,如果总是有一个核耗100%,通常是因为这个。看日志也可以知道
第三点是最复杂的,这说明代码写得好,充分利用了CPU和并行系统。第二点是代码写的烂。
jstat可以常用一点,可以看GC频率,java堆每个区域大小。建议少用jmap
阿里的应用优化到最后问题会集中在两个地方:
序列化反序列化 和 GC。
序列化和反序列化目前没有革命性的突破,只能想想能不能砍掉一些要序列化的项。
死锁好查的情况是jstat -l后看到JVM告诉说线程堆栈发生了死锁,看到最后告诉哪两个线程发生死锁。
死锁容易发生的原因是软件多人并行开发。
如果没有查到但是又怀疑死锁,除了jstack,还可以用pstack。
可以看C堆栈状况,因为java很多锁不在java堆栈而在C堆栈,因为jdk最后会调到C代码。
比如装载一个类,其实JVM会给这个类加一把锁,保证这个类只会初始化一次,这个锁可以在pstack看到但是不会在jstack看到
spring之前有死锁bug就是因为它的代码是多人写的。
处理线程池耗光 以前碰到过调用远程业务时没加超时时间。HSF默认有超时时间所以不会出现,但是自己写的HttpClient很容易忘记!外一调用的那端卡住了,自己这边也会全部卡住。所以所有分布式系统中只要访问另外一方,一定要设超时
hs_err_pid.log会描述为何退出,但是一般看不懂的。。
core dump一般都是已打开的。
生产环境可以看dmesg,看是不是操作系统杀掉了进程。因为虚拟机操作系统发现内存被用满后,通常会挑选一个内存消耗最大的进程杀掉。如果被杀掉,在dmesg中会有日志。如果是这样,是因为程序内存应用用的太多了
如果想看上面所说的日志,可以执行这行命令试试,作用是故意展示java怎么crash
如果是java上的线程栈溢出了会报stackoverflow,如果是native上的栈溢出了会直接导致java crash。
这种情况很容易判断,在这种场景下会生成core dump文件,但是不会生成hs_err_pid那个文件。core dump文件作用非常大!
jvm在jdk6以前bug很多,到7,8以后质量飙升。
排查所有问题时,最重要的是知道发生问题背后最根本的原因,知道java层面到底发生了什么。
做到这样可以不用动上层业务代码。
还要熟练运用工具。多尝试多用,不用等线上真实问题,学自己造问题