java堆内存溢出的问题
- 引言
- 堆内存工作原理
- 移除永久代?
- 分代是什么?
- 为什么分代?
- 为什么survivor分为两块相等大小的幸存空间?
- jvm堆内存常用参数
- 垃圾回收算法
- 垃圾收集器
-
- 串行收集器
- 并行收集器
- cms收集器
- g1收集器
- 垃圾收集器参数
- 为什么会堆内存溢出
-
- oom常见的几个原因
- 总结
现在做的这个项目后端代码用的是java语言的,我们用的tomcat来盛放java代码。这段时间老是遇到java堆内存溢出的问题。所以我就查了很多知识,赶紧过来先总结一下,分享交流一下。
java堆内存管理是影响性性能主要因素之一。堆内存溢出又是java项目非常常见的故障。在解决问题之前,必须先了解java堆内存的工作流程。
- jvm内存划分分为堆内存和非堆内存。堆内存分为年轻代、老年代;非堆内存一个永久代。
- 年轻代有分为eden和survivor区。survivor区由fromspace和tospace组成。eden区占的容量大,survivor两个区占的容量小。默认比例是8:1:1。
- 堆内存的作用就是用来存放对象。垃圾收集器就是用来收集这些对象,然后根据gc算法回收。
- 非对内存堆放的是永久代,也称为方法区,存储程序运行时长期存活的对象。
在jdk1.8版本废弃了永久代,替代的时元空间(metaspace),元空间与永久代类似,都是方法区的实现,他们最大的区别是:元空间并不在jvm中,而是使用本地内存。
元空间由注意有两个参数:
- metaspacesize:初始化元空间大小,控制发生gc阀值
- maxmetaspacesize:限制元空间大小上限。防止异常占用过多物理内存。
移除永久代:
为了融合hotspot jvm与jrockit vm而做出的改变,因为jrockit没有永久代。有了元空间就可以有效防止出现永久代oom问题
新生成的对象首先放到年轻代eden区,当eden空间满了,就会触发minor gc,存活下来的对象移动到survivor0区,surviver0满了会再次触发minor gc,survivor区存货对象移动到survivor1区,这样保证了一段时间总有一个survivor区为空。经过多次minor gc仍然存活的对象移动到老年代。
老年代存储长期存货的对象,占满时会触发major gc=full gc,gc期间会停止所有线程等待gc完成,所以对响应要求高的应用尽量减少发生major gc,避免响应超时。
minor gc:清理年轻代
major gc:清理老年代
full gc:清理整个堆空间,包括年轻代和永久代
所以gc都会停止应用所有线程
将对象根据存活概率进行分类,堆存活时间长的对象,放到固定区,从而减少扫描垃圾时间及gc频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
主要为了解决碎片化。如果内存碎片化严重,也就是两个对象占用不连续的内存,已有的连续内存不够新对象存放,就会主动触发gc
参数 | 描述 |
---|---|
-xms | 堆内存初始大小,单位m、g |
-xmx(maxheapsize) | 堆内存最大允许大小,一般不要大于物理内存的80% |
-xx:permsize | 非堆内存初始大小,一般应用设置初始化200m,最大1024m就够了 |
-xx:maxpermsize | 非堆内存最大允许大小 |
-xx:newsize(-xns) | 年轻代内存初始大小 |
-xx:maxnewsize(-xmn) | 年轻代内存最大允许大小,也可以缩写 |
-xx:survivorratio=8 | 年轻代中eden区与survivor区的容量比例值,默认为8,即8:1 |
-xss | 堆栈内存大小 |
- 标记-清除(mark-sweep)
gc分为两个阶段,标记和清除。首先标记所有可回收的对象,在标记完成后统一回收所有标记的对象,同时会产生连续的内存碎片化。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得再次触发gc
- 复制
将内存按容量划分为两块,每次只使用其中一块。当这块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收,也不用考虑内存碎片化问题,简单高效。缺点需要两倍的内存空间。
- 标记-整理
也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。此方法避免标记-清除算法的碎片化问题,同时也避免了复制算法的空间问题。
一般年轻代中执行gc后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集、而老年代中因为对象存活率高,没有额外过多的内存空间分配,就需要使用标记-清理活着标记-整理算法来进行回收。
串行收集器
比较老的收集器,单线程。收集时,必须暂停应用的工作线程,直到收集结束。
并行收集器
多条垃圾收集线程并行工作,在多核cpu下效率更高,应用线程仍然处于等待状态。
cms收集器
cms收集器是缩短暂停应用时间为目标而设计的,是基于标记-清除算法实现,整个过程分为4个步骤,包括:
- 初始化标记
- 并发标记
- 重新标记
- 并发清除
其中,初始标记、重新标记这两个步骤仍然需要暂停应用线程。初始标记只是标记一下gc roots能直接关联到的对象,速度很快,并发标记阶段是标记可回收对象,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作导致标记产生变动的那一部分对象的标记记录,这个阶段暂停时间比初始标记阶段稍长一点,但远比并发标记时间段。
由于整个过程中消耗最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,cms收集器内存回收与用户一起并发执行的,大大减少了暂停时间。
g1收集器
g1收集器将堆内存划分多个大小相等的独立区域(region),并且能预测暂停时间,能预测原因它能避免对整个堆进行全区收集。g1跟踪各个region里的垃圾堆积价值大小(所获得空间大小以及回收所需时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region,从而保证了再有限时间内获得更高的收集效率。
g1收集器工作工程分为4个步骤,包括:
- 初始化标记
- 并发标记
- 最终标记
- 筛选回收
初始标记与cms一样,标记一下gc roots能直接关联到的对象。并发标记从gc root开始标记存活对象,这个阶段耗时比较长,但也可以与应用线程并发执行。而最终标记也是为了修正在并发标记期间因用户程序继续运作而导致标记产生变化的那一部分标记记录。最后在筛选回收阶段对各个region回收价值和成本进行排序,根据用户所期望的gc暂停时间来执行回收。
参数 | 描述 |
---|---|
-xx: useserialgc | 串行收集器 |
-xx: useparallelgc | 并行收集器 |
-xx: useparallelgcthreads=8 | 并行收集器线程数,同时有多少个线程进行垃圾回收,一般与cpu数量相等 |
-xx: useparalleloldgc | 指定老年代为并行收集 |
-xx: useconcmarksweepgc | cms收集器(并发收集器) |
-xx: usecmscompactatfullcollection | 开启内存空间压缩和整理,防止过多内存碎片 |
-xx:cmsfullgcsbeforecompaction=0 | 表示多少次full gc后开始压缩和整理,0表示每次full gc后立即执行压缩和整理 |
-xx:cmsinitiatingoccupancyfraction=80% | 表示老年代内存空间使用80%时开始执行cms收集,防止过多的full gc |
-xx: useg1gc | g1收集器 |
-xx:maxtenuringthreshold=0 | 在年轻代经过几次gc后还存活,就进入老年代,0表示直接进入老年代 |
在年轻代中经过gc后还存活的对象会被复制到老年代中。当老年代空间不足,jvm会对老年代进行完全的垃圾回收full gc。如果full gc后还是无法存放从survivor区复制过来的对象,就会出现oom
oom常见的几个原因
1)老年代内存不足:java.lang.outofmemoryerror:javaheapspace
2)永久代内存不足:java.lang.outofmemoryerror:permgenspace
3)代码bug,占用内存无法及时回收。
oom在这几个内存区都有可能出现,实际遇到oom时,能根据异常信息定位到哪个区的内存溢出。
可以通过添加个参数-xx: heapdumponoutmemoryerror
,让虚拟机在出现内存溢出异常时dump出当前的内存堆转储快照以便事后分析。
下面是对java应用启动选项调优配置:
java_opts="-server -xms512m -xmx2g -xx: useg1gc -xx:survivorratio=6 -xx:maxgcpausemillis=400 -xx:g1reservepercent=15 -xx:parallelgcthreads=4 -xx:
concgcthreads=1 -xx:initiatingheapoccupancypercent=40 -xx: printgcdetails -xx: printgctimestamps -xloggc:../logs/gc.log"
- 设置堆内存最小和最大值,最大值参考历史利用率设置
- 设置gc垃圾收集器为g1
- 用gc日志,方便后期分析
- 选择高效的gc算法,可有效减少停止应用线程时间。
- full gc会增加暂停时间和cpu使用率,可以加大老年代空间大小降低full gc,但会增加回收时间,根据业务适当取舍。