一、问题解决思路
1.遇到内存溢出问题首先应该去查看debug日志,一般都会有相关报错信息。找不到再去查看gc.log和dump文件,如果系统没生成gc.log文件需要添加配置,下次内存溢出后就可以看到日志了。
-Xloggc:./temp/gc.log
-XX:HeapDumpPath=./temp/dump.hprof
2.通过在gc.log搜索FuIl GC等关键词,可以知道发生的时间点,然后可以去指定时间搜索相关debug日志,模糊的定位到问题代码,结合dump文件和实际的代码确认问题所在。
二、内存分析
1.生成dump文件
通过ps命令找到指定应用pid带入jmap命令即可
ps -ef|grep java
jmap -dump:format=b,file=heap.hprof pid
2.使用工具分析dump文件
(1)这里推荐常用的Jprofiler,附上下载地址:https://www.ej-technologies.com/jprofiler/download
(2)Jprofiler官网介绍文档:https://www.ej-technologies.com/resources/jprofiler/help/doc/main/introduction.html
(3)直接使用Jprofiler打开即可.hprof文件即可。
J
①查看占比较多的类,后面有用
②查看大对象
③对大对象进行引用分析
④可以一步步点进去,或者使用图表去分析最终的引用,发现就是第一步发现的那些占用较多的类。
3.结合使用的内存配置分析大对象
例:jvm配置为server -Xms1024M -Xmx1024M -Xmn512M -Xss512K,而大对象为ibatis相关对象,推测为数据库中的数据过一次性查的太多,或者请求数据库次数太多。
(1)分析内存配置
①初始堆内存(Initial Heap Size, -Xms) 和 最大堆内存(Maximum Heap Size, -Xmx) 均设置为1024MB(即1GB)。
这意味着JVM可以使用的堆内存总量被限制在1GB以内。
②年轻代(Young Generation, -Xmn)
年轻代大小设置为512MB。
由于大对象通常直接分配在老年代,因此年轻代的大小设置对这个大对象是否导致内存溢出没有直接影响。但是,如果年轻代频繁进行GC且回收效率低下,可能会间接影响堆内存的整体使用情况,从而增加内存溢出的风险。
③ 线程堆栈(Thread Stack, -Xss)每个线程的堆栈大小设置为512KB。
这个设置与大对象是否导致内存溢出无关,因为它影响的是线程私有内存区域的大小,而不是堆内存。
综上,一般来说一个大对象不接近1G就不会有太大问题。
(2)分析大对象所占内存
①通过gc.log等方法找到相关的sql,分析是否跟sql查询的数据过多有关。
②比如有100万条记录,每个记录所占堆空间大小为1408字节,最终所占堆大小为100100001408/1024/1024=1342.2M>1G。
由此可知肯定会导致内存溢出。
a.每个记录所占内存大小可以通过本地代码调试计算0bjectsizecalculator.getobjectsize(object)
;
b.如果无法调试代码,则可以通过表结构来进行简单估算一行数据所占内存大小(有一定误差)。
JAVA常用对象的内存占用空间:
字符串的内存占用包括对象头、字符数组以及对象对齐需要的填充字节。是因为在Java虚拟机中,对象的内存布局是按照一定的规则进行排列的,这种排列方式叫做对象对齐,它的占用内存一般为8个字节的整数倍。
对于不同的字符串字符数组的长度是主要影响最终所占内存的因素。一个字符占2个字节。比如长度100的String_100比长度为0的String_0长2*100=200左右。而受填充字节的影响0-4的字符的都在8以内,都是48。
比如有这么一张表
CREATE TABLE `emp` (
`id` int(11) NOT NULL,
`create_time` datetime DEFAULT NULL,
`modify_time` datetime DEFAULT NULL,
`name` varchar(16) DEFAULT NULL,
`address` varchar(256) DEFAULT NULL,
`age` smallint(6) DEFAULT NULL,
`height` decimal(10,2) DEFAULT NULL,
`weight` decimal(10,2) DEFAULT NULL,
`phone` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
以下是对emp表中各字段在Java中可能占用的内存大小的估算(这里假设使用Java的标准数据类型和对象来表示这些字段):
*id(int):16字节。
*create_time(datetime):24字节。
*modify_time(datetime):24字节。
*name(varchar(16)):长度一般在4以内,估算为48字节。
*age:16字节。
address(varchar(256)):估算长度为在20左右之间,48+202=88。
*height(decimal(10,2))和weight(decimal(10,2)):40字节。
phone(varchar(20)):长度一般为11,估算为48字节+112取8的倍数64。
现在,我们将这些值加在一起来估算整个对象的内存占用:
最终结果:16(id)+ 24(create_time)+ 24(modify_time)+ 48(name)+16(age)+88(address)+ 40(height)+ 40(weight)+ 64(phone)= 360字节
但是,请注意,这还没有包括Java对象头的开销,也没有考虑JVM的内存对齐要求(即对象大小可能需要是8的倍数)。因此,实际占用的内存可能会比这个数字稍大一些。
样本数据为:
hm.put("id", 1988);
hm.put("createTime", new Date());
hm.put("modifyTime", new Date());
hm.put("name", "张三丰");
hm.put("address","浙江杭州市西湖大道188号808室");
hm.put("age",88);
hm.put("weight",new BigDecimal(88));
hm.put("height",new BigDecimal(188));
hm.put("phone","1388888888");
实际所占空间大小为384字节,考虑到实际情况会有很多空字段,360字节也算比较接近的一个答案了。