我们系统的内存是有限的,假如我们的服务器运行的服务存在内存泄漏时,如果用户基数大,垃圾会充斥着内存,导致我们的服务运行变慢,甚至系统宕机。说道内存泄漏,我们需要了解内存中堆与栈的概念。(垃圾回收相当复杂,本文是触及到一点点点点点皮毛,估计90%的是错误的)  

堆(heap),栈(stack)的概念

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)

—–深入理解操作系统(第三版)587页

 

那内存中的堆怎么分配的呢?有两种分配方式,一种称为显示分配,另一种称为隐式分配

显示分配器

显示分配器要求应用显示的释放任何已分配的内存块。如我们使用的c语言标准库中提供malloc函数来分配内存块,并通过调用free函数来释放一个块。 C++中的new delete操作符与C中的malloc 和 free 相当。下面是相关的代码

void  garbage()
{
  int *p = (int *) malloc(15213);
  return 
}

上面的malloc 分配了15123个字节长度的内存,但是并没有使用free(p)来释放所分配的内存,就会导致内存无法释放,如果程序一直调用这个函数,就会有很多内存被占用,一直到程序运行结束,才释放内存,严重的时候会导致系统宕机。针对这个情况,提出了垃圾收集的机制,也就是隐形分配器。垃圾收集可以追溯到John McCarthy在20世纪60年代早期在MIT开发的Lisp系统。

隐式分配器

隐式分配器能够监测一个已分配的内存块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫垃圾收集器(garbage collector)而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection)。如我们使用的Java,Javascript 就是依赖垃圾收集来释放已分配的块。

垃圾收集器

垃圾收集器将内存视为一张有向可达图,如下图所示

该图的节点被分成一组根节点(root node)和一组堆节点(heap node)每个堆节点对应于堆中的一个已分配块。当对于堆中的的任何一个节点,如果从任何一个根节点出发都能到达,那么我们可以说该节点是可达的,那么该节点占据的内存块还有用,能够被再次利用。在任何时候,不可达节点就是垃圾,不能被应用程序再次利用,垃圾回收器会回收掉该节点,清除没有用的对象。垃圾回收器的作用就是一直维护这张可达图,直到应用程序不再运行。

使用垃圾回收器的优势

如果像c语言一样,程序员手动清除分配的内存,很容易忘记清除占用的内存。那些不可达的堆节点,不能自动释放垃圾回收器几乎让我们很少去考虑手动清除分配的内存的问题,让那些不可达的堆节点(一般保存着对象),自动被垃圾回收器释放。  

使用垃圾回收器的缺点

当然使用垃圾回收器,会有线程维护该垃圾回收器,1.这样肯定会占用cpu资源。2。不同算法的实现,有着不同的效率,但是跟优势相比,这个劣势可以忽略不计

那为什么有了垃圾回收机制,还有内存泄露的问题?

有了垃圾回收器,我们很高兴,但是为什么我们还要提内存泄漏这个问题呢?这是怎么回事,让人感觉很是百思不得其解。这个我们有需要把有垃圾回收机制的语言(java,javascript),和没有垃圾回收机制的语言做对比了(c语言)。

  • 对于c语言来说,内存泄漏是指在堆中的一块没有被free()释放的内存,但同时没有任何指针指向这个内存块,它没有主人认领了。
  • 对于JavaScript/java语言来说,上述的这种情况不会存在,GC遍历一下有向可达图,这些无主认领的内存块,直接回收掉。而对应的内存泄漏是指堆中一块没有被释放的内存,同时又存在Roots指向这个堆块(节点),而事实这个堆节点(被引用的对象,应用程序不需要了,重点在这,它无用了,但是我们还是保留了它,它占据着内存,占着茅坑不拉屎), 这个问题就是我们所定义的内存泄漏。 这个就是我们需要考虑的问题。

题外话:(其实在Java/javascript 这个所谓的内存泄漏,我们是指有变量指向该堆节点,尤其是在服务端,有时候有大量的请求处理导致大量无用的变量所引用的对象没有及时释放掉,当请求过多,内存利用率一下子提高,导致应用变慢,接口返回变慢,而对于浏览器端,主要对dom节点的引用,闭包中对变量引用,当没有该节点没有用,或闭包中的变量没有用时,却没有释放掉,这个要考虑带来的影响,是否影响程序快慢,如果没有影响,也不需要考虑)。

 

垃圾回收算法

既然内存动态分配,又有动态回收,回收的策略各种各样,而这些回收内存块的策略,我们称指为算法。不同算法的都有其适用的空间。其实,到目前为止,主要有三种算法,分别为引用计数算法标记清除算法GC复制算法。  很多新的算法都是基于上面的算法做了一些改进。如在javascript V8引擎中,对于VM 堆的内存分为了两部分,一部分分为新生代空间,另一部分问老生代空间。

新生代对象

在应用程序中,有80%的对象,利用一次之后就死去了,我们将新生成的对象,放入新生代空间。但是新生代空间很多对象很快就变成了垃圾,垃圾回收算法需要找一种快速甄别垃圾的算法,将内存腾出来。 这个时候,我们采用了以空间换时间的算法—GC复制算法。

老生代对象

当遍历新生代的垃对象时,发现有些对象还是被引用,那么把这些长时间被引用的对象,标志为老生代垃圾,移入老生代区域,对于老生代区域的对象,垃圾回收就不需要那么高效的性能。V8引擎中采用了标记-清除算法,和标记-压缩算法

这篇文章,瞎扯淡了垃圾回收的一些东西,没有什么系统性可言,下一篇,我们继续垃圾回收的问题,继续挖坑。

参考资料

《深入理解操作系统-第三版》

Node.js垃圾回收机制-基础

Understanding Garbage Collection and hunting Memory Leaks in Node.js

V8的Memory Scheme(内存模型)

内存管理

v8之旅-垃圾回收器

解读V8 GC Log

《垃圾回收的算法与实现    中村成洋 , 相川光 , 竹内郁雄》

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注