文章目录
常见的Java内存溢出
- java.lang.OutOfMemoryError: Java heap space (JVM堆溢出),JVM 在启动的时候会自动设置 JVM Heap 的值,其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)不可超过物理内存。可以利用 JVM提供的 -Xmn -Xms -Xmx 等选项可进行设置。
- java.lang.OutOfMemoryError: PermGen space :PermGen space老年代/永久代空间溢出,PermGen space 的全称是 Permanent Generation space,是指内存的永久保存区域。为什么会内存溢出,这是由于这块内存主要是被 JVM 存放Class 和 Meta 信息的,Class 在被 Load 的时候被放入 PermGen space 区域,它和存放 Instance 的 Heap 区域不同,sun 的 GC 不会在主程序运行期对 PermGen space 进行清理,所以如果你的 APP 会载入很多 CLASS 的话,就很可能出现 PermGen space 溢出
- java.lang.StackOverflowError : 栈溢出,递归调用层数太深超过虚拟机允许的深度,就会报StackOverflowError
1. 常见的JVM调优命令工具
1.1 jps
输入命令jps
后可以看到我启动的项目java进程和进程号PID,接着可以用其他命令对进程号(代表进程)进行操作。
1.2 jinfo
jinfo -flags PID
命令可以实时查看和调整所有JVM参数。jinfo -flag name PID
查看某个java进程的name属性值。jinfo -flag [+-]name PID
开启或者关闭对应名称的参数(只有被标记为manageable的参数才可以被动态修改)
1.3 jmap命令
jmap -histo 进程id > ./log.ext
可以查看内存信息,实例个数以及占用内存大小。此命令可以用来查看内存信息,实例个数以及占用内存大小,打开log.txt,文件内容如下:
- num:序号
- instances:实例数量
- bytes:占用空间大小
- class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]
堆信息
jmap -heap 进程id
堆内存dump(.hprof文件)
jmap -dump:format=b,file=XXX.hprof 进程id
也可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=./ (路径)
1.4 jstack命令
使用命令top -p <pid>
,显示你的java进程的内存情况,pid是你的java进程号。接下来用jstack加进程id查找死锁,按H获取每个线程的内存情况。
jstack 进程id
流程
1、使用命令top -p ,显示你的java进程的内存情况,pid是你的java进程号,比如19663
2、按H,获取每个线程的内存情况。
3、找到内存和cpu占用最高的线程tid,比如19664
4、转为十六进制得到 0x4cd0,此为线程id的十六进制表示
5、执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调
用方法。
6、查看对应的堆栈信息找出可能存在问题的代码
1.5 jstat命令(最常用)
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。用于监视虚拟机运行时状态信息的命令,可以查看虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。命令的格式如下:
jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]
垃圾回收统计
jstat -gc {pid}
最常用,可以评估程序内存使用及GC压力整体情况。
- S0C:第一个幸存区的大小,单位KB
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使用大小S1U:第二个幸存区的使用大小
- EC:伊甸园区的大小
- EU:伊甸园区的使用大小
- OC:老年代大小
- OU:老年代使用大小
- MC:方法区大小(元空间)
- MU:方法区使用大小
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间,单位s
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间,单位s
- GCT:垃圾回收消耗总时间,单位s
堆内存统计
jstat -gccapacity {pid}
可以查看堆内存的参数
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- S0C:第一个幸存区大小
- S1C:第二个幸存区的大小
- EC:伊甸园区的大小
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:当前老年代大小
- MCMN:最小元数据容量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- CCSMN:最小压缩类空间大小
- CCSMX:最大压缩类空间大小
- CCSC:当前压缩类空间大小
- YGC:年轻代gc次数
- FGC:老年代GC次数
新生代垃圾回收统计
jstat -gcnew {pid}
新生代内存统计
jstat -gvnewcapacity {pid}
老年代垃圾回收统计
jstat -gcold {pid}
老年代内存统计
jstat -gcoldcapacity {pid}
元数据空间统计
jstat -gcmetacapacity {pid}
2. JVM运行情况预估
用jstat gc -pid
命令可以计算出如下一些关键数据,有了这些数据就可以使用对应的优化策略。先给自己的系统设置一些初始性的JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。
2.1 年轻代对象增长的速率
执行命令jstat -gc pid 1000 10
(每隔1秒执行一次命令,共执行10次),通过观察EU(Eden区的实验)来估算每秒Eden区新增多少对象,如果系统负载不高,可以把频率换成1秒1分钟,甚至10分钟来观察整体情况。注:一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。
2.2 Young GC的触发频率和每次耗时
知道年轻代对象增长速率即能根据Eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC公式算出,可以知道系统大概多久会因为Young GC的执行而卡顿多久。
2.3 每次Young GC后有多少对象存活进入老年代
已经知道Young GC 频率的情况下,假设是每5分钟一次,那么可以执行命令jstat -gc pid 300000 10
,观察每次Eden、Survivor和老年代内存使用的变化情况,在每次GC后Eden区一般会大幅减少,Survivor和老年代都有可能增长,这些增长的对象就是 每次Young GC后存活的对象,同时可以看出每次Young GC后进去老年代大概多少对象,可以推算出老年代对象增长速率。
2.4 Full GC的触发频率和每次耗时
知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式FGCT/FGC计算得出。
3. 优化思路、常见的GC场景
简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代,尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。
3.1 Full GC比Minor GC还多
- 元空间不够导致的饿多余Full GC
- 显示调用System.gc()造成多余的Full GC,一般线上尽量通过-XX:+DisableExplicitGC参数禁用,如果加上了这个JCM启动参数,那么代码中调用System.gc()没有任何效果。
- 老年代空间分配担保机制
最快速度分析完这些我们推测的原因以及优化后,我们发现young gc和full gc依然很频繁了,而且看到有大量的对象频繁的被挪动到老年代,这种情况我们可以借助jmap命令大概看下是什么对象jmap -histo PID
,查到了有大量User对象产生,这个可能是问题所在,但不确定,还必须找到对应的代码确认,如何去找对应的代码了?
1、代码里全文搜索生成User对象的地方(适合只有少数几处地方的情况)
2、如果生成User对象的地方太多,无法定位具体代码,我们可以同时分析下占用cpu较高的线程,一般有大量对象不断产生,对应的方法代码肯定会被频繁调用,占用的cpu必然较高。
同时,java的代码也是需要优化的,一次查询出500M的对象出来,明显不合适,要根据之前说的各种原则尽量优化到合适的值,尽量消除这种朝生夕死的对象导致的full gc
3.2 内存泄漏
一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存,大多数同学可能为了图方便对于JVM级缓存就简单使用一个hashmap,于是不断往里面放缓存数据,但是很少考虑这个map的容量问题,结果这个缓存map越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁,这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致full gc,还有可能导致OOM。
这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。