zoukankan      html  css  js  c++  java
  • 内存管理:避免内存溢出和频繁的垃圾回收

    一、前言

    在高并发、高吞吐量的极限情况下,简单的事情就会变得没有那么简单了。

    • 一个业务逻辑非常简单的微服务,日常情况下都能稳定运行,一到大促就卡死甚至进程挂掉。
    • 一个做数据汇总的应用,按照小时、天这样的粒度进行数据汇总都没有问题,到年底需要汇总全年数据的时候,没等数据汇总出来,程序就死掉了。

    出现这些情况的大部分原因是,在程序设计时,没有针对高并发高吞吐量的情况做好内存管理

    二、内存管理机制

    现代编程语言,想Java、Go等,采用的都是自动内存管理机制。在编写代码时,不需要显式去申请和释放内存。当创建一个新对象时,系统会自动分配一块内存用于存放新创建的对象,对象使用完毕后,系统会自动择机收回这块内存,完全不需要开发者干预。对于开发者,这种自动内存管理机制,显然是非常方便的,不仅极大降低了开发难度,提升了开发效率,更重要的是,它完美地解决了内存泄漏的问题。但是它也会带来一些问题,这要从它的实现原理来分析。

    做内存管理,主要需要考虑申请内存和内存回收:

    1、申请内存的逻辑

    • 计算要创建对象所需要占用的内存大小;
    • 在内存中找一块儿连续并且是空闲的内存空间,标记为已占用;
    • 把申请的内存地址绑定到对象的引用上,这时候对象就可以使用了。

    2、内存回收的逻辑

    (1)、先是要找到所有可以回收的对象,将对应的内存标记为空闲
    现代的GC算法大多采用的是“标记 - 清除” 算法或是它的变种算法,这种算法分为标记和清除两个阶段:

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

    这个算法有一个最大的问题是,在执行标记和清除过程中,必须把进程暂停,否则计算的结果就是不准确的。这也就是为什么发生垃圾回收时,我们的程序会卡死的原因。虽然后续产生了许多变种的算法,可以减少一些进程暂停的时间,但都不能完全避免暂停进程。

    (2)、然后还需要整理内存碎片。

    假设,我们的内存只有 10 个字节,一开始这 10 个字节都是空闲的。我们初始化了 5 个 Short 类型的对象,每个 Short 占 2 个字节,正好占满 10 个字节的内存空间。程序运行一段时间后,其中的 2 个 Short 对象用完并被回收了。这时候,如果我需要创建一个占 4 个字节的 Int 对象,是否可以创建成功呢?

    答案是,不一定。我们刚刚回收了 2 个 Short,正好是 4 个字节,但是,创建一个 Int 对象需要连续 4 个字节的内存空间,2 段 2 个字节的内存,并不一定就等于一段连续的 4 字节内存。如果这两段 2 字节的空闲内存不连续,我们就无法创建 Int 对象,这就是内存碎片问题。

    所以,垃圾回收完成后,还需要进行内存碎片整理,将不连续的空闲内存移动到一起,以便空出足够的连续内存空间供后续使用。和垃圾回收算法一样,内存碎片整理也有很多非常复杂的实现方法,但由于整理过程中需要移动内存中的数据,也都不可避免地需要暂停进程

    在高并发场景下,这种自动内存管理的机制会更容易触发进程暂停。一般来说,在收到一个请求后,执行一段业务逻辑,然后返回响应。这个过程中,会创建一些对象,比如说请求对象、响应对象和处理中间业务逻辑中需要使用的一些对象等等。随着这个请求响应的处理流程结束,我们创建的这些对象也就都没有用了,它们将会在下次垃圾回收过程中被释放。需要注意的是,直到下一次垃圾回收之前,这些已经没有用的对象会一直占用内存。

    虚拟机决定什么时候来执行垃圾回收,这里面的策略非常复杂,也有很多不同的实现,但是无论是什么策略,内存不够用了,肯定要执行一次垃圾回收,否则程序无法继续运行。

    • 在低并发下,单位时间需要处理的请求不多,创建的对象数量不会很多,自动垃圾回收机制可以很好地发挥作用,可以选择在系统不太忙的时候来执行垃圾回收,每次垃圾回收的对象数量也不多,相应的,程序暂停的时间非常短,短到无法感知这个暂停。这是一个良性循环。
    • 在高并发下,短时间内就会创建大量的对象,这些对象将会迅速占满内存,这时候,由于没有内存可以使用了,垃圾回收被迫开始启动,并且,这次被迫执行的垃圾回收面临的是占满整个内存的海量对象,它执行的时间也会比较长,相应的,这个回收过程会导致进程长时间暂停。进程长时间暂停,又会导致大量的请求积压等待处理,垃圾回收刚刚结束,更多的请求立刻涌进来,迅速占满内存,再次被迫执行垃圾回收,进入了一个恶性循环。如果垃圾回收的速度跟不上创建对象的速度,还可能会产生内存溢出的现象。

    四、高并发下的内存管理技巧

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

    • 优化处理请求的代码逻辑,尽量少的创建一次性对象,特别是占用内存较大的对象。比如说:把收到请求的Request对象在业务流程中一直传递下去,而不是每执行一个步骤,就创建一个内容和Request对象差不多的新对象。
    • 建立一个对象池。对于需要频繁使用,占用内存较大的一次性对象,可以考虑自行回收并重用这些对象。收到请求后,在对象池内申请一个对象,使用完后再放回到对象池中,这样就可以反复地重用这些对象,非常有效地避免频繁触发垃圾回收。
    • 使用更大内存的服务器

    以上这些方法,都可以在一定程度上缓解由于垃圾回收导致的进程暂停,如果你优化的好,是可以达到一个还不错的效果的。当然,要从根本上来解决这个问题,办法只有一个,那就是绕开自动垃圾回收机制,自己来实现内存管理。但是,自行管理内存将会带来非常多的问题,比如说极大增加了程序的复杂度,可能会引起内存泄漏等等。

    流计算平台 Flink,就是自行实现了一套内存管理机制,一定程度上缓解了处理大量数据时垃圾回收的问题,但是也带来了一些问题和 Bug,总体看来,效果并不是特别好。因此,一般情况下并不推荐你这样做,具体还是要根据你的应用情况,综合权衡做出一个相对最优的选择。

  • 相关阅读:
    mac os programming
    Rejecting Good Engineers?
    Do Undergrads in MIT Struggle to Obtain Good Grades?
    Go to industry?
    LaTex Tricks
    Convert jupyter notebooks to python files
    How to get gradients with respect to the inputs in pytorch
    Uninstall cuda 9.1 and install cuda 8.0
    How to edit codes on the server which runs jupyter notebook using your pc's bwroser
    Leetcode No.94 Binary Tree Inorder Traversal二叉树中序遍历(c++实现)
  • 原文地址:https://www.cnblogs.com/chjxbt/p/11473971.html
Copyright © 2011-2022 走看看