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

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

内存管理主要考虑:

1.1 申请内存

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

1.2 内存回收

内存回收大概做这俩事:

  1. 找出所有可回收对象,将对应内存标记为空闲
  2. 整理内存碎片

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

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

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

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

所以,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就是为了缓解这个问题,而产生的变种算法,它可以减少FullGC的次数,但没有办法完全避免FullGC。

内存清除这个动作具体是怎么实现的?是电平复位?还是打上可以继续使用的标位?如果打标位这个该怎么打呢?一位一位的打?还是一个字节一个字节的打?更或者是一块一块的打?

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

2 高并发下程序为何卡死

高并发时,这种自动内存管理机制更容易触发STW。

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

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

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

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

对开发者,GC不可控,无法避免。但可降低GC频率,减少进程暂停时长。

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

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

根本解决该问题,办法只有一个:绕开自动GC机制,自己实现内存管理。但自行管理内存带来很多问题,极大增加程序复杂度,可能引起内存泄漏等。

Flink就自行实现一套内存管理机制,一定程度缓解了处理大量数据时GC问题,但总体效果并非很好。

思考

如微服务需求是处理大量文本,如每次请求会传入10KB文本,在高并发时,如何来优化程序,尽量避免由于GC导致的STW?

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

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