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

老子叫甜甜 提交于 2020-02-14 01:41:54

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

做内存管理,主要需要考虑申请内存和内存回收这两个部分。

 

申请内存的逻辑非常简单:

1、计算要创建对象所需要占用的内存大小;

2、在内存中找一块儿连续并且是空闲的内存空间,标记为已占用;

3、把申请的内存地址绑定到对象的引用上,这时候对象就可以使用了。

 

内存回收

内存回收需要做这样两件事儿:

1、先是要找出所有可以回收的对象,将对应的内存标记为空闲

2、整理内存碎片。

采用GC算法找出可回收对象,此类算法大多采用“标记-清除“算法或其变种,此类算法分为标记和清除两阶段:

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

缺点:效率不高、空间问题、在执行标记和清除过程中,必须把进程暂停(Stop the world)。

 

java虚拟机中常见的垃圾收集器及其对应垃圾收集算法:

垃圾收集器

1、Serial收集器。(虚拟机运行在Client模式下的默认新生代收集器)

单线程收集器,在进行垃圾收集时,必须暂停所有其他的工作线程,直至结束。

2、ParNew收集器

Serial收集器的多线程版本。Server模式下的虚拟机首选

当老年代选择CMS时,新生代只能选择ParNew或者Serial

3、Parallel Scavenge收集器     复制算法

CMS等收集器关注点在响应时间,尽量缩短用户的停顿时间。而该收集器目的在于可控的吞吐量上。

GC停顿时间的缩短是以牺牲吞吐量和新生代空间换来的.

自适应调节策略

MaxGCPauseMillis参数更关注最大停顿时间

GCTimeRatio更关注吞吐量

4、Serial Old收集器                  标记整理

Serial的老年代版本,单线程收集器,给Client模式下的虚拟机使用。可与Parallel Scavenge搭配使用。作为CMS的备用

5、Parallel Old收集器                 标记整理

可与Parallel Scavenge搭配使用

6、CMS收集器(并发标记清除,并发低延迟收集器) 标记清除

注重响应时间,适合互联网站或者B/S系统的服务端上。

整个过程分为4个步骤:初始标记、并发标记、重新标记、并发清除

初始标记和重新标记需要stop the world

缺点:CPU资源敏感,占用太多cpu资源

无法处理浮动垃圾

基于标记清除,产生大量空间碎片。容易触发Full GC

7、G1收集器(Garbage First)面向服务端

整个过程分为4个步骤:初始标记、并发标记、最终标记、筛选回收

 

内存碎片:

标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中,需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

所以,垃圾回收完成后,还需要进行内存碎片整理,将不连续的空闲内存移动到一起,以便空出足够的连续内存空间供后续使用。

 

为什么在高并发下程序会卡死?

一般来说,我们的微服务在收到一个请求后,执行一段业务逻辑,然后返回响应。这个过程中,会创建一些对象,比如说请求对象、响应对象和处理中间业务逻辑中需要使用的一些对象等等。随着这个请求响应的处理流程结束,我们创建的这些对象也就都没有用了,它们将会在下一次垃圾回收过程中被释放。直到下一次垃圾回收之前,这些已经没有用的对象会一直占用内存。

 

在低并发情况下,单位时间内需要处理的请求不多,创建的对象数量不会很多,自动垃圾回收机制可以很好地发挥作用,它可以选择在系统不太忙的时候来执行垃圾回收,每次垃圾回收的对象数量也不多,相应的,程序暂停的时间非常短,短到我们都无法感知到这个暂停。这是一个良性的循环。

 

在高并发的情况下,一切都变得不一样了。我们的程序会非常繁忙,短时间内就会创建大量的对象,这些对象将会迅速占满内存,这时候,由于没有内存可以使用了,垃圾回收被迫开始启动,并且,这次被迫执行的垃圾回收面临的是占满整个内存的海量对象,它执行的时间也会比较长,相应的,这个回收过程会导致进程长时间暂停。

进程长时间暂停,又会导致大量的请求积压等待处理,垃圾回收刚刚结束,更多的请求立刻涌进来,迅速占满内存,再次被迫执行垃圾回收,进入了一个恶性循环。如果垃圾回收的速度跟不上创建对象的速度,还可能会产生内存溢出的现象。

于是,一到大促销,大量请求过来,服务就卡死了。

 

高并发下的内存管理技巧

对于开发者来说,垃圾回收是不可控的,而且是无法避免的。但是,还是可以通过一些方法来降低垃圾回收的频率,减少进程暂停的时长。

只有使用过被丢弃的对象才是垃圾回收的目标,所以,我们需要想办法在处理大量请求的同时,尽量少的产生这种一次性对象。

最有效的方法就是,优化你的代码中处理请求的业务逻辑,尽量少的创建一次性对象,特别是占用内存较大的对象。比如说,我们可以把收到请求的 Request 对象在业务流程中一直传递下去,而不是每执行一个步骤,就创建一个内容和 Request 对象差不多的新对象。

对于需要频繁使用,占用内存较大的一次性对象,我们可以考虑自行回收并重用这些对象。实现的方法是这样的:我们可以为这些对象建立一个对象池。收到请求后,在对象池内申请一个对象,使用完后再放回到对象池中,这样就可以反复地重用这些对象,非常有效地避免频繁触发垃圾回收。

如果可能的话,使用更大内存的服务器,也可以非常有效地缓解这个问题。

从根本上来解决这个问题,办法只有一个,那就是绕开自动垃圾回收机制,自己来实现内存管理,但很麻烦还会带来很多问题。

 

内存管理:如何避免内存溢出和频繁的垃圾回收?——李玥

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!