如何避免内存溢出和频繁的垃圾回收

2023-01-15 09:04:57 浏览数 (2)

0 OOM和频繁GC预防方案

代码明明简单,日常跑没问题,怎么一大促就卡死甚至进程挂掉?大多因为设计时,就没针对高并发、高吞吐量case考虑过内存管理。

1 自动内存管理机制的实现原理

内存管理主要考虑:

1.1 申请内存

  1. 计算要创建对象所需要占用的内存大小
  2. 在内存中找一块儿连续且空闲内存空间,标记为已占用
  3. 把申请的内存地址绑定到对象的引用上,这时候对象就能使用

1.2 内存回收

1.2.1 找出所有可回收对象,将对应内存标记为空闲

找出可回收对象,现代GC算法大多采用“标记-清除”算法或变种:

  • 标记阶段:从GC Root开始,可简单将GC Root理解为程序入口的那个对象,标记所有可达对象,因为程序中所有在用的对象一定都会被这个GC Root对象直接或间接引用
  • 清除阶段:遍历所有对象,找出所有没标记的对象。这些没有标记的对象可被回收,清除这些对象,释放对应内存

该算法最大问题:在执行标记和清除过程中,须STW,否则计算结果不准确,所以程序会卡死。后续产生许多变种算法,但都只能减少一些进程暂停的间,不能完全避免STW。

1.2.2 整理内存碎片

完成对象回收后,还需要整理内存碎片。

所以,GC完成后,还需内存碎片整理,将不连续空闲内存移到一起,以空出足够连续内存空间。内存碎片整理也有很多实现,但由于整理过程中需移动内存数据,也须STW。

虽然自动内存管理机制有效解决内存泄漏问题,代价是执行垃圾回收时会STW,若暂停时间过长,程序就“卡死了”。

STW发生在标记阶段 or 清除阶段?

标记阶段需要暂停,清除阶段一般不需要。进程暂停这个实现过程是怎样的?暂停后需要再启动,这个又是一个怎样的过程?

https://stackoverflow.com/questions/16558746/what-mechanism-jvm-use-to-block-threads-during-stop-the-world-pause

STW为使计算结果更准确,好比打扫卫生,我一个房间一个房间来,也不耽误其他房间的事,是不是暂停是不必须的,其实 young gc 几乎不停发生,只有发生full gc 的时候性能才会大大降低?

对GC来说只有一个房间,你是没有办法分成多个完全独立的小房间。 像java中的young gc就是为缓解这个问题,而变种算法可减少Full GC次数,但没办法完全避免FullGC。

内存清除这个动作具体是怎么实现的?

内存是按页为单位管理,即一块块,JVM有一套复杂数据结构来记录它管理的所有页面与对象引用之间的关系。所谓清除和移动对象,就是修改这个记录关系的数据结构。

2 高并发下程序为何卡死

微服务收到一个请求后,执行一段业务逻辑,返回响应。这过程中,会创建一些对象,如请求对象、响应对象和处理中间业务逻辑的对象。随该请求响应的处理流程结束,创建的这些对象也都没用,将在下次GC时被释放。直到下次GC前,这些无用对象还会一直占用内存。

低并发时,单位时间需处理请求不多,创建对象数量也不多,自动GC机制发挥很好,它能选择在系统不太忙时执行GC,每次GC的对象也不多,因此STW时间很短,短到人类无法感知。

高并发时程序忙,短时内创建大量对象,迅速占满内存,这时无内存可用,GC开始启动,并且这次被迫执行的GC面临的是占满整个内存的海量对象,其执行时间也长,相应回收过程会导致进程长时间暂停,进一步导致大量请求被积压待处理。等GC刚结束,更多请求立刻涌进,迅速占满内存,再次被迫执行GC,进入恶性循环。若GC速度跟不上创建对象速度,还可能OOM。

3 高并发环境的内存管理

GC不可控,无法避免。但可降低GC频率,减少进程暂停时长。只有使用过被丢弃的对象才是GC目标,想办法在处理大量请求同时,尽量少的产生这种一次性对象:

最有效的,优化代码处理请求的业务逻辑,尽量少创建一次性对象,尤其大对象。如把收到请求的Request对象在业务流程中一直传递下去,而非每执行一个步骤,就创建一个和Request对象差不多的新对象。

需频繁使用,占用内存较大的一次性对象,可考虑自行回收并复用。为这些对象建立一个对象池。收到请求后,在对象池内申请一个对象,使用完后再放回对象池,就能复用对象,有效避免频繁GC

使用更大内存的服务器。

根本办法:绕开自动GC机制,自己实现内存管理。但自行管理内存带来很多问题,极大增加程序复杂度,可能引起内存泄漏等。Flink就自行实现一套内存管理机制,一定程度缓解了处理大量数据时GC问题,但总体效果并非很好。

FAQ

微服务需求是处理大量文本,如每次请求会传入10KB文本,高并发时,如何优化程序,尽量STW?

这种一般不要求时延,大部分异步处理,更注重服务吞吐率,服务可在更大内存服务器部署,然后把新生代eden设置更大,因为这些文本处理完不会再拿来复用,朝生夕灭,可在新生代Minor GC,防止对象晋升到老年代,防止频繁Major GC.

若晋升对象过多,大于老年代的连续内存空间,也会触发Full Gc,然后在这些处理文本的业务流程中,防止频繁创建一次性的大对象,把文本对象做为业务流程直接传递下去,若这些文本需复用,可将他保存起来,防止频繁创建。

JVM对字符串有优化,字符串是不可变对象,通过字符串常量池,可以复用一些字符串。

0 人点赞