转载是一种动力 分享是一种美德
忙了一天,结果发现好像什么也没做。
请原谅我又标题党了,与其说是性能调优不如说是找bug,但对我来说确实是件大事,毕竟是线上出了问题,渠道找运营反应,运营催我修复,于是带着沉重的心情忙活了一天。
都是使用内存缓存惹的祸,都是为了QPS!继续说内存缓存那些事。由于只会有一个定时任务发生写操作,故使用一个静态Map缓存数据。起初是由于忽略了其它服务以及第三方调用接口是多线程,导致数据更新不及时,这可以改为使用volatile声明即可,保证定时任务更新数据对其它线程可见。之所以不用线程安全的Map也是出于性能考虑。
然而,再精心的设计也会有Bug的时候,坑了我一整天的时间。此次并非堆大小没配置,但也确实堆大小导致的bug,虽然程序没有挂掉,但是数据已经不更新了。数据从数据库中加载到内存需要做一些业务上的处理,根据配置的过滤规则需要过滤掉一些屏蔽某些渠道的数据,然后将根据渠道id与其能访问到的数据映射到Map中。根据测试预估数据缓存在内存会占用1.6g内存,我给jvm设置了2.3g左右的堆大小,从逻辑上看并不存在什么问题,但却少考虑了数据的交换问题。
来个伪代码:
{
Var 数据1 = 从数据库加载;
Var 数据2 = 从数据库加载;
Var 数据3 = 从数据库加载;
………
Var 数据10 = 从数据库加载;
Var 最终数据 = 数据1根据数据2~数据10过滤后;
内存缓存的数据 = 最终数据;
}
假设数据1到数据10占内存总和为2.0g,,执行到“内存缓存的数据=最终数据;” 这行代码时,内存总共消费2.0G,执行完这行代码后也就是方法执行完后内存只耗1.6g,因为产升了gc。过滤数据过程中,用于保存临时结果的ArrayList之类的对象不会导致内存飙升太严重,因为数组只是存储对象的引用。
然后当任务执行第二次的时候,由于当前缓存已经消耗了1.6g,在第二次执行“内存缓存的数据=最终数据;”这行代码之前,严格来说是这个方法执行完之前,就需要堆大小大于“当前缓存数据1.6g”+“从数据库中加载的数据1~数据10占的2.0g” = 3.6g 程序才能正常运行。
所以问题清晰了,就是这样导致的内存溢出OutOfMemoryError异常。由于这是定时任务抛出的异常,并未导致服务挂掉,只是执行加载数据的那个任务线程挂掉了。结果就是缓存的数据并未得到更新,永远都是第一次加装的数据,因为“内存缓存的数据=最终数据;”这行代码永远得不到第二次执行。
使用“jstat -gcutil 进程id” 可以看到系统频繁的full gc,full gc次数是young gc的一半,短短几分钟内执行了5次full gc。想起某个视频中学到的“大对象直接进入年老代”,就是大的对象会直接进入年老代,而年老代已经不够容纳新的对象进行,或者已经接近满了,于是就会触发full gc。
谈谈我对“大对象直接进入年老代”的理解:ArrayList以及Map都只是存对象的引用,即便一个ArrayList存了几万个对象,但它所占内存并不是这一万个对象所占内存的总和,这句应该都能理解吧?他的大小只是存储几万个对象的引用的大小。
假如有一个User类,一个User对象占1m大小:
User{
byte[1024*1024] //注意:是基本数据类型数据,占1024*1024个字节
}
而如果ArrayList 存了一万个User对象,那它占的内存大小就是一万*4个字节,没有异议吧?那这也是个大对象了。是不是大对象是根据你配置的堆大小而言的,如果一个对象存不进年轻代的Eden区,那它就是个大对象。由于ArrayList又是动态分配大小的,每往ArrayList中add一个对象都可能导致动态分配内存,当添加的对象很多时,这个ArrayList就变成了一个大对象,再往里面add对象就会发生两个大对象的内存拷贝,这是一个值得考虑的问题,所以这里又得出了在new ArrayList时指定大小new Array(10000)提升的性能的重要性。
下面给出关于jdk.8 jstat命令的使用以及结果的各列所代表的意思。不知道的学一下,知道的就复习一下,不需要背,有个印象就好了,需要的时候百度即可。
声明:pid ==> 进程id
垃圾回收统计>jstat -gc pid
结果列说明:
S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
新生代垃圾回收统计>jstat -gcnew pid
结果列说明:
S0C:第一个幸存区大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
TT:对象在新生代存活的次数
MTT:对象在新生代存活的最大次数
DSS:期望的幸存区大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
老年代垃圾回收统计>jstat -gcold pid
结果列说明:
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
OC:老年代大小
OU:老年代使用大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
总垃圾回收统计>jstat -gcutil pid
结果列说明:
S0:幸存1区当前使用比例
S1:幸存2区当前使用比例
E:伊甸园区使用比例
O:老年代使用比例
M:元数据区使用比例
CCS:压缩使用比例
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
更多jstat的用法请自行百度学习。jstack -l pid是查看线程信息、jmap -heap pid是查看堆信息、jps查看java进程号。如果在服务器上执行jps指令获取不到进程信息,请切换到运行java服务的用户,或者sudo su切换到root用户。
如果觉得对您有帮助,请赏我个“好看”,记得动动手指哦!
公众号:java艺术
扫码关注最新动态