前言
线上问题也是挺高频的,基本上要求你有实际的经验。实在没有的话只能了解些基础理论,现编了。
基础理论
如果JVM出现了问题,可以使用哪些命令或程序进行定位?
指令
-
jps:查询服务器所有Java进程信息,通常用于快速定位出现问题的Java进程id;
-
jmap:输出某个Java进程的内存情况;
jmap -heap pid=> 进程堆内存新生代、老年代、持久代、GC算法等信息jmap -histo:live pid | head -n 20=> 进程内存中所有对象实例数(instances)和大小(bytes)- 堆内存输出为heap dump文件,交给eclipse mat等工具做分析
-
jstack:打印某个Java线程的线程栈信息,比如想要查询某进程中内存占用最高的线程;
top -Hp pid=> 查出占CPU最高的线程pidprintf '%x\n' tid=> 线程id十进制转为十六进制jstack pid | grep tid -C 30 --color=> 输出线程栈信息
-
jinfo:用于查看jvm的配置参数。
-
jstat:可以查看堆内存各部分的使用量,以及加载类的数量。
jstat -gc pid
工具
Eclipse MAT,针对heap dump文件进行内存分析。
- 右侧的饼图显示当前快照中最大的对象。
- 单击工具栏上的柱状图,可以查看当前堆的类信息,包括类的对象数量、浅堆(Shallow heap,一个对象结构所占用内存的大小)、深堆(Retained Heap,一个对象被回收后,可以真实释放的内存大小)。
- 支配树(The Dominator Tree): 列出了堆中最大的对象,第二层级的节点表示当被第一层级的节点所引用到的对象,当第一层级对象被回收时,这些对象也将被回收。这个工具可以帮助我们定位对象间的引用依赖关系 。
- Path to GC Roots:被JVM持有的对象被称为GC Roots,从一个对象到GC Roots的引用链被称为Path to GC Roots, 通过分析Path to GC Roots可以找出JAVA的内存泄露问题,当程序不再访问该对象时仍存在到该对象的引用路径。
线上环境中,JVM配置了哪些启动参数?
必要:
-XX:HeapDumpOnOutOfMemoryError。如果线上出现OOM异常,导出heap dump文件。-XX:HeapDumpPath=/tmp/app.dump。指定heap dump文件导出路径。
可选:
-XX:+HeapDumpBeforeFulGC。如果线上频繁FullGC,可以使用此选项在FullGC的时候导出heap dump。
线上如果出现了问题,想要从大量日志文件中搜索某个关键字,使用什么linux指令?
如搜索OOM异常,先定位到日志所在目录:
grep -n "java.lang.OutOfMemoryError" *.log
-n:显示匹配行的行号
JVM在哪些情况下会触发Full GC?
System.gc(),建议jvm执行fullgc,并不一定会执行;- 执行了
jmap -histo:live pid命令,这个会立即触发fullgc; - 使用了大对象,大对象会直接进入老年代;
- 对象年龄达到指定阈值也会进入老年代;
- 在执行
minor gc的时候进行的一系列检查。- 执行Minor GC(新生代gc)的时候,JVM会检查老年代中最大连续可用空间是否大于了当前新生代所有对象的总大小。
- 如果大于,则直接执行Minor GC(这个时候执行是没有风险的)。
- 如果小于了,JVM会检查是否开启了空间分配担保机制,如果没有开启则直接改为执行Full GC。
- 如果开启了,则JVM会检查老年代中最大连续可用空间是否大于了历次晋升到老年代中的平均大小,如果小于则执行改为执行Full GC。
- 如果大于则会执行Minor GC,如果Minor GC执行失败则会执行Full GC。
线上环境中,如果发现JVM频繁FullGC如何解决?
案例:数据源使用了阿里巴巴的druid,druid自带了一个sql语句监视功能,程序内也开启了此功能,导致JVM频繁FullGC。在druid的配置文件中关闭了sql语句监视功能,从而解决了该问题。
线上环境中,如果发现Java进程CPU使用率较高如何解决?
解决案例:
- 定位进程:使用
top指令(类似于Windows的任务管理器),发现Java进程的CPU占用率飘高(如181%); - 定位线程:使用
top -Hp pid找到CPU占用率最高的线程,记录tid; - tid转为十六进制:tid默认是十进制的,通过
printf %x tid转为十六机制; - 定位代码:
jstack pid |grep -A 200 tid,打印线程的线程栈信息,定位代码。
如发现BeanValidator.java的第30行是有可能存在问题,我们发现,我们自定义了一个BeanValidator,封装了Hibernate的Validator,然后在validate方法中,通过Validation.buildDefaultValidatorFactory().getValidator()初始化一个Validator实例,通过分析发现这个实例化的过程比较耗时。 我们重构了一下代码,把Validator实例的初始化提到方法外,在类初始化的时候创建一次就解决了问题。
什么是OOM问题?
OutOfMemoryError(以下缩写为oom)是java中最常见的内存问题,也是一旦发生影响就非常大的问题。oom出现的原因就是内存不够用了,GC虽然在回收,然后回收的速度赶不上新对象分配了或者根本就没有对象可以被回收,就会抛出OutOfMemoryError错误。常见的OOM(java.lang.OutOfMemoryErro)错误有五种:
-
Java heap space:比较复杂,单独分析
-
unable to create new native thread
没有
native内存了。无法创建更多的操作系统线程。说明当前系统的线程数过多,超过了系统线程数的上限,减少当前应用创建的线程即可。
- Metaspace
说明
Metaspace(永久代空间)不够用,修改-XX:MaxMetaspaceSize启动参数,调大永久代空间即可。
- Direct buffer memory
堆外内存(
Direct Byte Buffer)的默认大小为64MB,一旦使用超出限制,就会抛出Direct buffer memory错误。
- GC overhead limit exceeded
当jvm98%的时间都在GC时,就会出现该异常。这种情况需要增大堆或者结合Java heap space的解决方式处理。
OOM Java heap space如何解决?
oom发生的原因有两种:
-
应用正常,但是堆设置过小,无法支持正常的对象分配
-
应用发生了内存泄漏,导致应该被清理的对象没有清理,无限增长
通过分析heap dump,我们基本可以区分出两种情况,第一种情况,对象的分布都正确,没有那种一下占用30%,甚至50%的对象。第二种情况就是某一种类型的对象,占据了大量的内存空间。
第一种情况的解决方案就是:
- 增大堆内存
- 优化应用内存使用效率
第二种情况,就需要针对性分析了,这里笔者给出常见的oom原因,大家在问题排查时,可以逐个排除,直到找到正确答案:
ThreadLocal未清理- 出现了未指定分页的大数据量查询
- 定时任务中
list忘记清空,每次都追加数据 - 监控系统中使用了不可控的字段作为
label,导致label无限增长
如何知道线上环境是否发生了OOM?
-
日志监控(
filebeat或者flume):通过监控日志中关键字java.lang.OutOfMemoryError,就可以知道应用是否出现oom。 -
JVM配置参数
-XX:+ExitOnOutOfMemoryError:当jvm启用该参数时,如果出现了oom,就会自动退出程序,咱们的健康检测自然能发现应用不存在了,从而能发出告警。 -
使用jstat监控jvm内存使用率:
jstat -gcutil $pid 1000命令即实现每秒打印一次jvm的内存信息,如果老年代(查询结果的O列)使用率一直是100%,并且期间还在一直不断GC(YGC和FGC分别代表新生代GC次数和老年代GC次数),这也是发生了oom的一种现象。
实际遇到的
使用iTextPDF导致的oom问题?
使用iTextPDF将HTML文件转换为PDF时,文本数量超大(百万级别),导致了OOM异常。 自研的小说网,将网络小说导出为pdf时可能出现。
OOM异常的类型是Java heap space,导出heap dump后,使用eclipse mat对其进行分析,发现对象内存分配均匀,则是因为堆内存不够导致的。
经过分析,发现:iTextPDF在处理HTML转换为PDF时,会将整个HTML内容加载到内存中,并生成PDF文档。如果html超大,内存占用超过JVM堆空间的最大限制时,就会抛出OutOfMemoryError: Java heap space异常。
解决方案:分卷,单一pdf最大50万行。