Jvm堆区
Heap(堆区):
堆区线程共享,存储的是创建的实例对象,不再使用的对象会被垃圾收集器回收。一般情况下,堆所占内存空间是JVM内存中最大的,如果创建对象不克制就会造成OOM(内存溢出)。
堆区内存空间可自定义大小和运行时动态修改,通过修改-Xms(最小初始值)和-Xmx(最大值)两个参数改变。通常将-Xms和-Xmx参数值设置一致,避免服务器动态运行时,堆区内存不断调整,从而增加服务器压力。同时也避免GC(垃圾回收)之后调整堆大小带来的压力。
堆区又分为新生代和老生代,新生代又分为1哥Eden区和2个s区。对象创建初期会在新生代的Eden区生成,当Eden区没有多余空间容纳新对象时,就会触发YGC(年轻的垃圾回收)把没有引用的对象回收,有引用的移到S区。每次YGC时,会将存活的对象复制到未使用过的空间,再将使用过的空间完全清除,再交换两个空间的使用状态。如果YGC时s区无法容纳,就将对象之间移送到老生代。
因为存在两个s区,所以交换时是两个空间之间的交换。
在YGC过程中,每个对象都有一个计数器,没进行一次YGC就会+1。通过-XX:MAXTenuringThrehold参数可以配置,当达到配置值时,对象就从新生代移送到老生代。
YGC和FGC
YGC :对新生代堆进行gc。频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小。
FGC :全堆范围的gc。默认堆空间使用到达80%(可调整)的时候会触发fgc。以生产环境为例,一般比较少会触发fgc,有时10天或一周左右会有一次。
什么时候执行YGC和FGC
a.Eden空间不足,执行 young gc
b.old空间不足,perm空间不足,调用方法System.gc() ,ygc时的悲观策略, dump live的内存信息时(jmap –dump:live),都会执行full gc
JVM调优
而目前大家常说的JVM的调优就是针对于JVM堆区的,通过对JVM堆区的了解,这里把JVM调优也一起学习一下。
调优就是指性能的优化,对于系统性能优化主要指系统的吞吐量和响应时间。
吞吐量:指系统在单位时间内处理请求的数量。对于并发系统,通常需要用吞吐量作为性能指标。
响应时间:指系统对请求作出响应的时间。对于单用户的系统,响应时间可以很好地度量系统的性能。
JVM调优主要就是为了解决OOM(内存溢出)、系统响应慢、死锁等问题。
举个例子:
设置JVM堆区的参数-Xms和-Xmx一致(这里设置一致的原因我上面提过)为2G(2048M);
堆区新生代和老年代默认比例1:2,则新生代内存约为682.67M,老年代内存约为1365.33M。
上面说过:新生代分为Eden区和两个S区,默认比例8:1:1,则Eden区内存约为546.14M,两个s区分别为68.26M。
假设产生对象的速度为50M/s,则Eden区11s左右就满了,两个s区只有68.26M,则50M的对象最后会被移送到老年代。
按上面说的老年代内存空间使用80%之后触发FGC,则老年代*80%,达到1092.26M时触发FGC。
所以大约22次YGC后触发FGC,242s左右触发FGC。这对于拥有大量用户的系统而言,产生的卡顿,运行效率是很可怕的。而一般正常情况是一周或者更长时间才触发一次FGC。
通过分析上述过程可以知道,Eden区内存满触发YGC时,S区内存过小,无法存储产生的50MB的对象从而直接移送到老年代。从上面对堆区的介绍,我们可以了解到,YGC时会清除Eden区和其中一个S区的未引用对象,将还在引用的对象存入另一个S区。所以我们是不是可以增加新生代的内存从而增加S区的内存,使YGC时能够存放Eden区还在引用的对象;再减少对象进入老年代的配置值,让存放在S区的对象提前进入老年代,提前腾出S区的空间。
比如,以上面的例子为例:
增加S区的内存容量到200M,则Eden区内存就可以达到1600M。
那么对于50M/s的对象需要32s填满Eden区,从而触发YGC。
产生的50M的对象也会放入S1区,等32s后下一次YGC时,新的对象放入S2区,Eden区和S1区垃圾对象被清理。
从而大幅度减少YGC和FGC。实现JVM调优。减少JVM响应慢和卡顿问题。
对于OOM(内存溢出)和死锁问题:
OOM排查
内存飙高一般都是堆中对象无法回收造成,因为java中的对象大部分存储在堆内存中。其实也就是常见的oom问题(Out Of Memory)。
1.jinfo pid,可以查看当前进行虚拟机的相关信息列举出来;
2.jstat -gc pid ms,多长毫秒打印一次gc信息,打印信息如下,里面包含gc测试,年轻代/老年带gc信息;
3.jmap -histo pid | head -20,查找当前进程堆中的对象信息,加上管道符后面的信息以后,代表查询对象数量最多的20个。
4.jmap -dump:format=b,file=xxx pid,可以生成堆信息的文件
但是这个命令不建议在生产环境使用,因为当内存较大时,执行该命令会占用大量系统资源,甚至造成卡顿。建议在项目启动时添加下面的命令,在发生oom时自动生成堆信息文件:-XX:+HeapDumpOnOutOfMemory。如果需要在线上进行堆信息分析,如果当前服务存在多个节点,可以下线一个节点,生成堆信息,或者使用第三方工具,阿里的arthas。
对于调优的一些经验,总结我之前看的博客来说:
JVM配置方面,一般情况可以先用默认配置(基本的一些初始参数可以保证一般的应用跑的比较稳定了),在测试中根据系统运行状况(会话并发情况、会话时间等),结合gc日志、内存监控、使用的垃圾收集器等进行合理的调整,当老年代内存过小时可能引起频繁FGC,当内存过大时 FGC时间会特别长。
物理内存一定的情况下,新生代设置越大,老年代就越小,FGC频率就越高,但FGC时间越短;相反新生代设置越小,老年代就越大,FGC频率就越低,但每次FGC消耗的时间越大。建议如下:
-Xms和-Xmx的值设置成相等,堆大小默认为-Xms指定的大小,默认空闲堆内存小于40%时,JVM会扩大堆到-Xmx指定的大小;空闲堆内存大于70%时,JVM会减小堆到-Xms指定的大小。如果在Full GC后满足不了内存需求会动态调整,这个阶段比较耗费资源。
新生代尽量设置大一些,让对象在新生代多存活一段时间,每次YGC 都要尽可能多的收集垃圾对象,防止或延迟对象进入老年代的机会,以减少应用程序发生FGC的频率。
老年代如果使用CMS收集器,新生代可以不用太大,因为CMS的并行收集速度也很快,收集过程比较耗时的并发标记和并发清除阶段都可以与用户线程并发执行。
方法区大小的设置,1.6之前的需要考虑系统运行时动态增加的常量、静态变量等,1.7只要差不多能装下启动时和后期动态加载的类信息就行。
代码实现方面,性能出现问题比如程序等待、内存泄漏除了JVM配置可能存在问题,代码实现上也有很大关系:
避免创建过大的对象及数组:过大的对象或数组在新生代没有足够空间容纳时会直接进入老年代,如果是短命的大对象,会提前出发FGC。
避免同时加载大量数据,如一次从数据库中取出大量数据,或者一次从Excel中读取大量记录,可以分批读取,用完尽快清空引用。
当集合中有对象的引用,这些对象使用完之后要尽快把集合中的引用清空,这些无用对象尽快回收避免进入老年代。
可以在合适的场景(如实现缓存)采用软引用、弱引用,比如用软引用来为ObjectA分配实例:SoftReference objectA=new SoftReference(); 在发生内存溢出前,会将objectA列入回收范围进行二次回收,如果这次回收还没有足够内存,才会抛出内存溢出的异常。
避免产生死循环,产生死循环后,循环体内可能重复产生大量实例,导致内存空间被迅速占满。
尽量避免长时间等待外部资源(数据库、网络、设备资源等)的情况,缩小对象的生命周期,避免进入老年代,如果不能及时返回结果可以适当采用异步处理的方式等。