总结的内存溢出.docx
《总结的内存溢出.docx》由会员分享,可在线阅读,更多相关《总结的内存溢出.docx(9页珍藏版)》请在冰豆网上搜索。
总结的内存溢出
1、JVM的内存模型
从大的方面来讲,JVM的内存模型分为两大块:
永久性内存(Permanentspace)和堆内存(heapspace)。
栈内存(stackspace)一般不归在JVM内存模型中,栈内存属于线程级别。
每个线程都有独立的栈内存空间。
∙YoungGen:
年轻代,包括1个Eden区和2个Survivor区,新创建的对象(大部分为短周期的对象)将进入这个区,虚拟机会频繁地对这个区进行垃圾回收。
∙OldGen:
年老代,当对象在YoungGen呆地足够久(经过几次的垃圾回收仍然存在)或YoungGen空间不足时,对象将进入OldGen,由于一般是生命周期比较长的对象,因此虚拟机对这块内存的回收频度会比较低,一旦回收,使用的将是一个耗时的FullGC,另外,一旦堆空间不足时,虚拟机也会尝试去回收这个区。
∙PermGen:
持久代,一些常量定义和类、方法声明及其bytecode都会放在这个区。
1.1Permanentspace
Permanentspace是指内存的永久保存区域,主要用于存放加载的class类级对象如class本身、method、field等。
它和存放类实例的Heap区域不同,GC(GarbageCoolection)不会在主程序运行期对Permanentspace进行清理。
如果应用中有很有class的话,且permanentspace设置得比较小就有可能会出现这样的错误。
Permanentspace大小的设置:
-XX:
PermSize用于设置永久保存区域初始大小;-XX:
MaxPermSize用于设置永久保存区域的最大值。
1.2Heapspace
Heapspace由OldGeneration和NewGeneration组成,OldGeneration存放生命周期长久的实例对象,而新的对象实例一般放在NewGeneration。
NewGeneration可再分为Eden区、Survivor区,新对象实例总是首先放在Eden区,Survivor作为Eden区和Old区的缓冲,可以向Old区转移活动的对象实例。
其默认空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。
如果内存剩余不到40%,JVM就会增大堆到Xmx设置的值,内存剩余超过70%,JVM就会减小堆到Xms设置的值。
所以服务器的Xmx和Xms设置一般应该设置相同避免每次GC后都要调整虚拟机堆的大小。
注意:
如果Xms超过了Xmx值,或者堆最大值和非堆最大值的总和超过了物理内存或者操作系统的最大限制都会引起服务器启动不起来。
垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收OLD段中的垃圾;1级或以上为部分垃圾回收,只会回收New中的垃圾,内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
当一个URL被访问时,内存申请过程如下:
A.JVM会试图为相关Java对象在Eden区中初始化一块内存区域
B.当Eden空间足够时,内存申请结束。
否则到下一步
C.JVM试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区
D.Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区
E.当OLD区空间不够时,JVM会在OLD区进行完全的垃圾收集(0级)
F.完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"outofmemory错误"、
2、内存分配的相关参数
ms/mx:
定义NEW+OLD段的总尺寸,ms为JVM启动时NEW+OLD的内存大小;mx为最大可占用的NEW+OLD内存大小。
在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。
NewSize/MaxNewSize:
定义NEW段的尺寸,NewSize为JVM启动时NEW的内存大小;MaxNewSize为最大可占用的NEW内存大小。
在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。
PermSize/MaxPermSize:
定义Perm段的尺寸,PermSize为JVM启动时Perm的内存大小;MaxPermSize为最大可占用的Perm内存大小。
在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。
SurvivorRatio:
设置Survivor空间和Eden空间的比例。
NewRatio:
设置Old区域与NEW区域的比例。
3、GC的算法
1)标记非活动对象
--何为非活动对象,通俗的讲,就是无引用的对象。
追踪root对象算法:
深度追踪root对象,将heap中所有被引用到的root做标志,所有未被标志的对象视为非活动对象,所占用的空间视为非活动内存。
2)清理非活动对象
Copy算法:
方法:
将内存分为两个区域(fromspace和tospace)。
所有的对象分配内存都分配到fromspace。
在清理非活动对象阶段,把所有标志为活动的对象,copy到tospace,之后清楚fromspace空间。
然后互换fromsapce和tospace的身份。
既原先的fromspace变成tosapce,原先的tospace变成fromspace。
每次清理,重复上述过程。
优点:
copy算法不理会非活动对象,copy数量仅仅取决为活动对象的数量。
并且在copy的同时,整理了heap空间,即,tospace的空间使用始终是连续的,内存使用效率得到提高。
缺点:
划分fromspace和tospace,内存的使用率是1/2。
Compaction算法:
方法:
在清理非活动对象阶段,删除非活动对象占用内存,并且把活动对象向heap的底部移动,直到所有的活动对象被移到heap的一侧。
优点:
无须划分fromsapce和tospace,提高内存的使用率。
并且compaction后的内存空间也是连续分配的。
缺点:
该算法相对比较复杂。
如果能对这两种对象区分对象,那么会提高GC的效率。
在sunjdkgc中(具体的说,是在jdk1.4之后的版本),提出了不同生命周期的GC策略。
younggeneration:
生命周期很短的对象,归为younggeneration。
由于生命周期很短,这部分对象在gc的时候,很大部分的对象已经成为非活动对象。
因此针对young generation的对象,采用copy算法,只需要将少量的存活下来的对象copy到tospace。
存活的对象数量越少,那么copy算法的效率越高。
younggeneration的gc称为minorgc。
经过数次minorgc,依旧存活的对象,将被移出younggeneration,移到tenuredgeneration(下面将会介绍)
younggeneration分为:
eden:
每当对象创建的时候,总是被分配在这个区域
survivor1:
copy算法中的fromspace
survivor2:
copy算法中的tosapce(备注:
其中survivor1和survivor2的身份在每次minorgc后被互换)minorgc的时候,会把eden+survivor1
(2)的对象copy到survivor2
(1)去。
tenuredgeneration:
生命周期较常的对象,归入到tenuredgeneration。
一般是经过多次minorgc,还依旧存活的对象,将移入到tenuredgeneration。
(当然,在minorgc中如果存活的对象的超过survivor的容量,放不下的对象会直接移入到tenuredgeneration)。
tenuredgeneration的gc称为majorgc,就是通常说的fullgc。
采用compactiion算法。
由于tenuredgeneraion区域比较大,而且通常对象生命周期都比较常,compaction需要一定时间。
所以这部分的gc时间比较长。
minorgc可能引发fullgc。
当eden+fromspace的空间大于tenuredgeneration区的剩余空间时,会引发fullgc。
这是悲观算法,要确保eden+fromspace的对象如果都存活,必须有足够的tenuredgeneration空间存放这些对象。
PermanetGeneration:
该区域比较稳定,主要用于存放classloader信息,比如类信息和method信息。
对于springhibernate这些需要动态类型支持的框架,这个区域需要足够的空间。
这部分内容相对比较理论,可以结合jstat,jmap等命令(当然也可以使用jconsole,jprofile,gciewer等工具),观察jdkgc的情况。
4、jdk自带的jpsjmapjstat等工具
jps:
相当于linux下的ps,列出所有java相关线程的pid等信息
example:
$jps
18861Bootstrap
1418Jps
jmap:
显示java进程内存使用的相关信息
jmappid打印内存使用的摘要信息
jmap-histopid>mem.txt打印比较简单的各个有多少个对象占了多少内存的信息,一般重定向的文件
jmap-dump:
format=b,file=mem.datpid将内存使用的详细情况输出到em.dat文件
jmap-dump:
live,format=b,file=heap.bin
analyzeheap
将二进制的heapdump文件解析成human-readable的信息,自然是需要专业工具的帮助,这里推荐MemoryAnalyzer。
用jhat命令可以参看jhat-port7000mem.dat
在浏览器中访问:
http:
//10.5.22.65:
7000/查看详细信息
jstat:
显示java虚拟机的一些统计信息
jstat-选项pid间隔显示时间显示次数
jstat-gc1886125010
jstat-gccapacity1886125010
jstat-gcnew1886125010
jstat-gcnewcapacity1886125010
jstat-gcold1886125010
jstat-gcutilpid1000200
表示每1秒打印一次,共200次
S0 —Heap上的Survivorspace0区已使用空间的百分比
S1 —Heap上的Survivorspace1区已使用空间的百分比
E —Heap上的Edenspace区已使用空间的百分比
—Heap上的Oldspace区已使用空间的百分比
P —Permspace区已使用空间的百分比
YGC—从应用程序启动到采样时发生YoungGC的次数
YGCT–从应用程序启动到采样时YoungGC所用的时间(单位秒)
FGC—从应用程序启动到采样时发生FullGC的次数
FGCT–从应用程序启动到采样时FullGC所用的时间(单位秒)
GCT—从应用程序启动到采样时用于垃圾回收的总时间(单位秒)
调试流程
内存泄漏一般都是有一定特征的,任何代码和数据都要占用内存,简单总结内存泄漏的特征是内存占用不可控制,GC不可回收。
追踪内存使用量的曲线发现一些特征,在估计虚拟机即将崩溃时,使用jmap-histopid>mem.txt发现相关内存泄漏的对象占用非常打比例的内存空间,然后很容易猜测问题大概的位置,一下子就解决了。
对于内存泄露的压力测试也开始实实在在的做起来。
经过这次问题的定位和解决以后,大致觉得对于一个大用户量应用要放心的话,那么需要做这么几步。
1.在GC输出的环境下,大压力下做多天的测试。
(可以在JAVA_OPTS增加-verbose:
gc-XX:
+PrintGCDetails-XX:
+HeapDumpOnOutOfMemoryError)
2.检查GC输出日志来判断是否有内存泄露。
(这部分后面有详细的实例说明)
3.如果出现内存泄露问题,则使用jprofiler等工具来排查内存泄露点(之所以不一开始使用,因为jprofiler等工具对于压力测试有影响,使得大压力无法上去,也使问题不那么容易暴露)
4.解决问题,并在重复2步骤。
(这里的内存泄露问题就是在以前blog写过的jdk的concurrent包内LinkedBlockingQueue的poll方法存在比较严重的内存泄露,调用频率越高,内存泄露的越厉害)
两次压力测试都差不多都是两天,测试方案如下:
开始50个并发,每个并发每次请求完毕后休息0.1秒,10分钟后增长50个并发,按此规律增长到500并发。
先说一下日志输出的结构:
(1.6和1.5略微有一些不同,只是1.6对于时间统计更加细致)
[GC[:
->,secs]->,secs]
GC收集器的名称
新生代在GC前占用的内存
新生代在GC后占用的内存
新生代局部收集时jvm暂停处理的时间
JVMHeap在GC前占用的内存
JVMHeap在GC后占用的内存
GC过程中jvm暂停处理的总时间
Jdk1.5log:
启动时GC输出:
[GC[DefNew:
209792K->4417K(235968K),0.0201630secs]246722K->41347K(498112K),0.0204050secs]
[GC[DefNew:
214209K->4381K(235968K),0.0139200secs]251139K->41312K(498112K),0.0141190secs]
一句输出:
新生代回收前209792K,回收后4417K,回收数量205375K,Heap总量回收前246722K回收后41347K,回收总量205375K。
这就表示100%的收回,没有任何新生代的对象被提升到中生代或者永久区(名字说的不一定准确,只是表达意思)。
第二句输出:
按照分析也就只是有1K内容被提升到中生代。
运行一段时间后:
[GC[DefNew:
210686K->979K(235968K),0.0257140secs]278070K->68379K(498244K),0.0261820secs]
[GC[DefNew:
210771K->1129K(235968K),0.0275160secs]278171K->68544K(498244K),0.0280050secs]
第一句输出:
新生代回收前210686K,回收后979K,回收数量209707K,Heap总量回收前278070K回收后68379K,回收总量209691K。
这就表示有16k没有被回收。
第二句输出:
新生代回收前210771K,回收后1129K,回收数量209642K,Heap总量回收前278171K回收后68544K,回收总量209627K。
这就表示有15k没有被回收。
比较一下启动时与现在的新生代占用内存情况和Heap使用情况发现Heap的使用增长很明显,新生代没有增长,而Heap使用总量增长了27M,这就表明可能存在内存泄露,虽然每一次泄露的字节数很少,但是频率很高,大部分泄露的对象都被升级到了中生代或者持久代。
又一段时间后:
[GC[DefNew:
211554K->1913K(235968K),0.0461130secs]350102K->140481K(648160K),0.0469790secs]
[GC[DefNew:
211707K->2327K(235968K),0.0546170secs]350275K->140921K(648160K),0.0555070secs]
第一句输出:
新生代回收前211554K,回收后1913K,回收数量209641K,Heap总量回收前350102K回收后140481K,回收总量209621K。
这就表示有20k没有被回收。
分析到这里就可以看出每一次泄露的内存只有10几K,但是在大压力长时间的测试下,内存泄露还是很明显的,此时Heap已经增长到了140M,较启动时已经增长了100M。
同时GC占用的时间越来越长。
后续的现象:
后续观察日志会发现,FullGC的频率越来越高,收集所花费时间也是越来越长。
(FullGC定期会执行,同时局部回收不能满足分配需求的情况下也会执行)。
[FullGC[Tenured:
786431K->786431K(786432K),3.4882390secs]1022399K->1022399K(1022400K),[Perm:
36711K->36711K(98304K)],3.4887920secs]
java.lang.OutOfMemoryError:
Javaheapspace
Dumpingheaptojava_pid7720.hprof...
出现这个语句表示内存真的被消耗完了。
Jdk1.6log:
启动时GC的输出:
[GC[PSYoungGen:
221697K->31960K(229376K)]225788K->36051K(491520K),0.0521830secs][Times:
user=0.26sys=0.05,real=0.05secs]
[GC[PSYoungGen:
228568K->32752K(229376K)]232659K->37036K(491520K),0.0408620secs][Times:
user=0.21sys=0.02,real=0.04secs]
第一句输出:
新生代回收前221697K,回收后31960K,回收数量189737K,Heap总量回收前225788K回收后36051K,回收总量189737K。
100%被回收。
运行一段时间后输出:
[GC[PSYoungGen:
258944K->2536K(259328K)]853863K->598135K(997888K),0.0471620secs][Times:
user=0.15sys=0.00,real=0.05secs]
[GC[PSYoungGen:
259048K->2624K(259328K)]854647K->598907K(997888K),0.0462980secs][Times:
user=0.16sys=0.02,real=0.04secs]
第一句输出:
新生代回收前258944K,回收后2536K,回收数量256408K,Heap总量回收前853863K回收后598135K,回收总量255728K。
680K没有被回收,但这并不意味着就会产生内存泄露。
同时可以看出GC回收时间并没有增加。
在运行一段时间后输出:
[GC[PSYoungGen:
258904K->2488K(259264K)]969663K->713923K(1045696K),0.0485140secs][Times:
user=0.16sys=0.01,real=0.04secs]
[GC[PSYoungGen:
258872K->2448K(259328K)]970307K->714563K(1045760K),0.0473770secs][Times:
user=0.16sys=0.01,real=0.05secs]
第一句输出:
新生代回收前258904K,回收后2488K,回收数量256416K,Heap总量回收前969663K回收后713923K,回收总量255740K。
676K没有被回收,同时总的Heap也有所增加。
此时看起来好像和1.5的状况一样。
但是查看了一下FullGC的执行还是400-500次GC执行一次,因此继续观察。
运行一天多以后输出:
[GC[PSYoungGen:
257016K->3304K(257984K)]1019358K->766310K(1044416K),0.0567120secs][Times:
user=0.18sys=0.01,real=0.06secs]
[GC[PSYoungGen:
257128K->2920K(258112K)]1020134K->766622K(1044544K),0.0549570secs][Times:
user=0.19sys=0.00,real=0.05secs]
可以发现Heap增长趋缓。
运行两天以后输出:
[GC[PSYoungGen:
256936K->3584K(257792K)]859561K->606969K(1044224K),0.0565910secs][Times:
user=0.18sys=0.01,real=0.06secs]
[GC[PSYoungGen:
256960K->3368K(257728K)]860345K->607445K(1044160K),0.0553780secs][Times:
user=0.18sys=0.01,real=0.06secs]
发现Heap反而减少了,此时可以对内存泄露问题作初步排除了。
(其实在jdk1.6环境下用jprofiler来观察,对于concurrent那个内存泄露点的跟踪发现,内存的确还是会不断增长的,不过在一段时间后还是有回收,因此也就可以部分解释前面出现的情况)
总结:
对于GC输出的观察需要分两个维度来看。
一个是纵向比较,也就是一次回收对于内存变化的观察。
一个是横向比较,对于长时间内存分配占用情况的比较,这部分比较需要较长时间的观察,不能仅仅凭短时间的几个抽样比较,因为对于抽样来说,FullGC前后的区别,运行时长的区别,资源瞬时占用的区别都会影响判断。
同时要结合FullGC发生的时间周期,每一次GC收集所耗费的时间作为辅助判断标准。
顺便说一下,Heap的YoungGen,OldGen,PermGen的设置也是需要注意的,并不是越大越好,越大执行收集的时间越久,但是可能执行