本文已收录GitHub,更有互联网大厂面试真题,面试攻略,高效学习资料等

不知道你有没有发现,在高并发、高吞吐量的极限情形下,简朴的事情就会变得没有那么简朴了。一个营业逻辑异常简朴的微服务,一样平常情形下都能稳固运行,为什么一到大促就卡死甚至历程挂掉?再好比,一个做数据汇总的应用,凭据小时、天这样的粒度举行数据汇总都没问题,到年底需要汇总整年数据的时刻,没等数据汇总出来,程序就死掉了。

之以是泛起这些情形,大部门的缘故原由是,程序在设计的时刻,没有针对高并发高吞吐量的情形做好内存治理。要想解决这类问题,首先你要领会内存治理机制。

现代的编程语言,像 Java语言等,接纳的都是自动内存治理机制。我们在编写代码的时刻,不需要显式去申请和释放内存。当我们确立一个新工具的时刻,系统会自动分配一块内存用于存放新确立的工具,工具使用完毕后,系统会自动择机收回这块内存,完全不需要开发者干预。

对于开发者来说,这种自动内存治理的机制,显然是异常利便的,不仅极大降低了开发难度,提升了开发效率,更主要的是,它完美地解决了内存泄露的问题。是不是很厉害?昔时,Java 语言能够迅速普及和盛行,逾越 C 和 C++,自动内存治理机制是异常主要的一个因素。然则它也会带来一些问题,什么问题呢?这就要从它的实现原理中来剖析。

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

做内存治理,主要需要思量申请内存和内存接纳这两个部门。

申请内存的逻辑异常简朴:

  1. 盘算要确立工具所需要占用的内存大小;
  2. 在内存中找一块儿延续而且是空闲的内存空间,符号为已占用;
  3. 把申请的内存地址绑定到工具的引用上,这时刻工具就可以使用了。

内存接纳的历程就异常庞大了,总体上,内存接纳需要做这样两件事儿:先是要找出所有可以接纳的工具,将对应的内存符号为空闲,然后,还需要整理内存碎片。

若何找出可以接纳的工具呢?现代的 GC 算法大多接纳的是“符号 - 消灭”算法或是它的变种算法,这种算法分为符号和消灭两个阶段:

  • 符号阶段:从 GC Root 最先,你可以简朴地把 GC Root 理解为程序入口的谁人工具,符号所有可达的工具,由于程序中所有在用的工具一定都会被这个 GC Root 工具直接或者间接引用。
  • 消灭阶段:遍历所有工具,找出所有没有符号的工具。这些没有符号的工具都是可以被接纳的,消灭这些工具,释放对应的内存即可。

这个算法有一个最大问题就是,在执行符号和消灭历程中,必须把历程暂停,否则盘算的效果就是不准确的。这也就是为什么发生垃圾接纳的时刻,我们的程序会卡死的缘故原由。后续发生了许多变种的算法,这些算法加倍庞大,可以削减一些历程暂停的时间,但都不能完全制止暂停历程。

完成工具接纳后,还需要整理内存碎片。什么是内存碎片呢?我举个例子你就明了了。

假设,我们的内存只有 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,总体看来,效果并不是稀奇好。因此,一样平常情形下我并不推荐你这样做,详细照样要凭据你的应用情形,综合权衡做出一个相对最优的选择。

总结

现代的编程语言,大多接纳自动内存治理机制,虚拟机遇不定期执行垃圾接纳,自动释放我们不再使用的内存,然则执行垃圾接纳的历程会导致历程暂停。

在高并发的场景下,会发生大量的待接纳的工具,需要频仍地执行垃圾接纳,导致程序长时间暂停,我们的程序看起来就像卡死了一样。为了缓解这个问题,我们需要只管少地使用一次性工具,对于需要频仍使用,占用内存较大的一次性工具,我们可以思量自行接纳并重用这些工具,来减轻垃圾接纳的压力。