自动内存管理机制的实现原理
做内存管理,主要需要考虑申请内存和内存回收这两个部分。
申请内存的逻辑非常简单:
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 对象差不多的新对象。
对于需要频繁使用,占用内存较大的一次性对象,我们可以考虑自行回收并重用这些对象。实现的方法是这样的:我们可以为这些对象建立一个对象池。收到请求后,在对象池内申请一个对象,使用完后再放回到对象池中,这样就可以反复地重用这些对象,非常有效地避免频繁触发垃圾回收。
如果可能的话,使用更大内存的服务器,也可以非常有效地缓解这个问题。
从根本上来解决这个问题,办法只有一个,那就是绕开自动垃圾回收机制,自己来实现内存管理,但很麻烦还会带来很多问题。
来源:CSDN
作者:码天码地
链接:https://blog.csdn.net/u013140345/article/details/104302521