消息队列_12(内存管理)

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

java、Go语言等,采用的都是自动内存管理机制。
自动内存管理机制优点:方便,降低开发难度,提升效率,完美地解决内存泄漏问题。
同时会存在一些问题。

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

内存管理,主要考虑申请内存和内存回收。

申请内存的逻辑:

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

内存回收需要:首先找出所有可以回收的对象,将对应的内存标记为空闲,然后,还需要整理内存碎片。

消息队列_12(内存管理)

1.1 回收对象

如何找出可以回收的对象?
现代的GC算法大多采用的是“标记 - 清除” 算法或者它的变种算法,分为两个阶段:

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

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

1.2 整体内存碎片

eg.
假设,内存只有10个字节,开始都为空闲。初始化5个Short类型对象,每个对象占2个字节,正好占满。程序运行一段时间后,有2个Short对象用完并被回收。这时候,如果需要创建一个占4个字节的Int对象,是否可以创建成功?
不一定。这两段2字节的空闲内存不一定为连续的,创建对象必须为连续的内存空间。这就是内存碎片问题。

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

2. 为什么在高并发下进程容易卡死

分析:
微服务收到一个请求后,执行一段业务逻辑,然后返回响应。这个过程中,会创建一些对象,随着这个请求响应的处理流程结束,创建的对象将会在下一次垃圾回收过程中被释放。

低并发情况下,单位时间内需要处理的请求不多,创建的对象数量不会很多,自动垃圾回收机制可以很好地发挥作用。

高并发情况下,短时间内创建大量的对象,并迅速占满内存,当没有内存可以使用时,垃圾回收被迫开始启动,且由于海量的对象处理时间会比较长,这个过程就会导致进程长时间暂停。进程长时间暂停,又会导致大量的请求积压等待处理。最后垃圾回收的速度跟不到创建对象的速度,就可能会产生内存溢出的现象。

3. 高并发下内存管理技巧

对于开发者,垃圾回收哦是不可控且无法避免。

通过一些方法来降低回收的频率,减少进程暂停的时长。

尽量少产生一次性的对象的方法:优化代码中请求的业务逻辑,特别是占用内存较大的一次性对象。
对于需要频繁使用,占用内存较大的一次性对象,可以自行回收并重用。实现方法:为这些对象创建对象池。反复地重用这些对象,有效地避免频繁触发垃圾回收。
也可以使用更大内存的服务器缓解这个问题。

**课外了解:流计算平台Flink,**其自行实现一套内存管理机制,但是不太好。

4. 问题

如果微服务的需求是处理大量的文本,每次请求会传入10KB 左右的文本,在高并发的情况下,该如何优化这个程序来尽量避免由于垃圾回收导致的进程卡死问题?

如果有一个微服务是处理大量的文本,感觉这种一般不会要求时延,大部分都会进行异步处理,更加注重服务的吞吐率,服务可以在更大的内存服务器进行部署,然后把新生代的eden设置的更大些,因为这些文本处理完不会再拿来复用,朝生夕灭,可以在新生代Minor GC,防止对象晋升到老年代,防止频繁的Major GC,如果晋升的对象过多大于老年代的连续内存空间也会有触发Full Gc,然后在这些处理文本的业务流程中,防止频繁的创建一次性的大对象,把文本对象做为业务流程直接传递下去,如果这些文本需要复用可以将他保存起来,防止频繁的创建。也为了保证服务的高可用,也需对服务做限流、负载、兜底的一些策略。

5. 拓展

来源: https://www.cnblogs.com/honey01/p/9475726.html

Java GC、新生代、老年代

Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为

三个区域:Eden、From Survivor、To Survivor。

这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

堆的内存模型大致为:
消息队列_12(内存管理)
从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。

(本人使用的是 JDK1.6,以下涉及的 JVM 默认值均以该版本为准。)
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定

),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young )

被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。

默认的,Edem : from : to = 8 :1 : 1 ( 可以通过参数–XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的

新生代空间大小,from = to = 1/10 的新生代空间大小。

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor

区域是空闲着的。

因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。