内存回收的一些基本方法

简介:

内存垃圾回收(Garbage Collection)是一个很古老的技术了,最开始在Lisp上出现。如今几乎所有高级语言都有GC,大部分程序员不再需要绞尽脑汁通宵达旦去查找内存泄露的原因了。我以前也不怎么关心垃圾回收这个问题,可是面试时老是被问到智能指针,而我又不会写,因为我对C++不熟。所以决定研究并且总结一下这个问题。
其实智能指针都不能称为GC,就是编译器给你加了delete或free,基于的原理是引用计数(Reference Counting)。GC一般基于一下两个原理

Reference Counting(引用计数): 每个对象都设置一个参数,就是引用它的变量,引用少一个就减1,多一个就加1,为0时回收
Reachability(可达性):有一组基本的对象或变量是可达的,称为root set,这些变量或对象指向的对象也是可达的,同理,一个可达对象指向的对象是可达的。

本文简单的介绍了常用的几种内存回收算法,包括Reference Counting,Mark and Sweep,Semispace, Generation。

Reference Counting

一般没有真正的GC使用Reference Counting。智能指针使用了Reference Counting,在指针析构的时候,将引用数减1,为0时顺便把指向的对象回收了。

一个简单的智能指针的实现(用于应付面试)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
template  < class  T>  class  SmartPointer {
     protected :
     T* ref;
     unsigned  int  * ref_count;
     public :
     SmartPointer(T *ptr)
     {
         ref = ptr;
         ref_count = (unsigned  int *) malloc ( sizeof (unsigned  int ));
         *ref_count = 1;
     }
     SmartPointer(SmartPointer<T> & sptr)
     {
         ref = sptr.ref;
         ref_count = sptr.ref_count;
         ++*ref_count;
     }
     SmartPointer<T> & operator= (SmartPointer<T> &sptr)
     {
         if ( this  != &sptr)
         {
             ref = sptr.ref;
             ref_count = sptr.ref_count;
             ++*ref_count;
         }
         return  * this ;
     }
     ~SmartPointer()
     {
         --*ref_count;
         if (*ref_count == 0)
         {
             delete  ref;
             free (ref_count);
             ref = NULL;
             ref_count = NULL;
         }
     }
     T getValue() { return  *ref;}
}

智能指针是最简单的一种gc方法。甚至,这算不上一种gc,实际上是编译器帮你写了free或者delete,基于的原理就是:对象的作用域结束时都会自动调用析构函数,这个析构函数是编译器在编译时加上的。gc都会有一个触发事件,对于智能指针来说,就是作用域结束。对于其他的,可能是内存不够了,然后会启动gc进行回收。

Mark and Sweep

Mark and Sweep使用的是可达性。在一个程序中,所有的全局变量,静态变量,局部变量都是可达的,这些称为root set。从root出发,找到所有可达的,然后回收不可达的。
基本的过程如下:
每个object都有一个singlebit的标志位,一开始都是0
要回收的时候,扫两遍
第一遍,从root变量开始进行DFS扫描,可达的都将它们的标志位置1
第二遍,搜索所有的object,如果是1,置为0,如果是0,reclaim

这就有一个问题,这个root怎么找呢?比如C语言,怎么确定找出栈上哪些是变量?更不用说要确定哪些是指针了。对于高级的动态语言,虚拟机或者解释器都会维护一个所有符号的表,这样找起来是很容易的。gc可以分为Precise gc和Conservative gc。前者明确知道内存的哪个地方是变量,哪个地方是指针,因此可以精确的进行回收,这种一般适用于高级语言,例如lisp,python,Java等。但是对于C语言,只能假设栈上任何32bit(或者64bit)都是指针,在此基础上可能会有一些检测方法,然后把这些指针当作root,进行扫描。C/C++还有一个问题就是internal pointer。因为在高级语言里,一般所有的地址都指向对象的开头,但是C/C++指针可以指向对象的任何地方,这也导致了扫描的困难。所以C/C++一般不会使用gc。This is the nature of C! 但是也有一些比较好的C/C++的gc,例如Boehm GC,它是一种Conservative GC。

Boehm挺好用的,下面是一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <gc/gc.h>
int  main()
{
     int  i;
     GC_INIT();
     int  *p;
     for (i = 0; i < 1000000; i++)
     {
         p = ( int *)GC_MALLOC(20*1024*1024);
         p[i/400] = 5;
         if (i % 10000 == 0)
             printf ( "Heap size = %d\n" ,GC_get_heap_size());
     }
}

这段代码不会发生内存溢出,如果使用malloc但是不free,很快内存就不够了。
但是如果我把大小从20*1024*1024增加到1024*1024*1024,就有问题了。

内存不够用了。说明它的回收做的不够好。而使用malloc加free,可以一直运行下去。我的内存有2G,是够用的。Boehm GC是最有名的C/C++ GC,而且不少项目也在用它。但是,C语言的本性决定了它不需要GC。

Semispace

在进行内存回收时,内存整理也是必须的。否则内存中充满了碎片。Semispace的方法也是基于可达性,从名字也可以看出,它是要把内存分成两半,只有一半可用,一个FromSpace,一个ToSpace(或者叫Old,New,whatever)。

基本工作过程是:
从root开始扫描,找到可达的,就从FromSpace复制到ToSpace,一直这样找下去,最后可达的都被移到了ToSpace,而且不存在碎片。
这个过程牵涉到一个很严重的问题:指针重定向,称为pointer forward。这是semispace需要解决的最主要问题。这个问题最简单的方法就是查表。

1
2
3
4
5
6
7
8
9
10
copy(p):
     if (content of p is already copy to ToSpace)
          p = forwarding_address(p)
          ret
     if (content of p is not copied to ToSpace)
          copy content of p to ToSpace
          forwarding_address(p) =  ToSpacePtr;
          ToSpacePtr +=  sizeof (p)
     foreach pointer x in content of p:
           copy(x)

如果回收的时候堆里大部分都是garbage,那么semispace的方法特别好,如果大部分都是可达的,那么效率就很低了。

Generation Garbage Collection

如果你在程序里读入了一些静态的数据,很大,而且需要常驻内存,而且里面确定没有指针。你肯定不希望GC一直去扫描它或者一直移来移去。Java和.Net采用的方法称为Generation Garbage Collection,将对象分成几个generation,新创建的对象在 Generation 0(Java使用Young,Old,Permenant,Eden,Survior,Tenured,.Net使用0,1,2),逃过第一次扫荡(Sweep)的被挪到Generation 1,逃过两次的被挪到Generation 2,.Net就到2,就是你逃过回收的次数越多,就越年老,GC就越不管你。
基本的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
if (G0 is almost full)
{
      scan and reclaim G0
      if (G1 is almost full)
      {
           scan and reclaim G1
           if (G2 is almost full)
                scan and reclaim G2
           move survivors to G2
      }
      move survivors to G1
}

这张图是Java使用的方法,先分了Young,Old,Permanent,然后里面又细分,挺复杂的,但是思想就是上面所叙述的。

总结

本文只是简单的介绍了垃圾回收的一些基本思想方法,实际上GC特别复杂。自动回收的代价就是性能的下降,在有些情况下自动回收可能会比手动释放性能更好。即使性能差点,能摆脱内存泄露这样的问题,还是非常值得的。



本文转自nxlhero 51CTO博客,原文链接:http://blog.51cto.com/nxlhero/1293433,如需转载请自行联系原作者

相关文章
|
2月前
|
弹性计算 定位技术 数据中心
阿里云服务器配置选择方法:付费类型、地域及CPU内存配置全解析
阿里云服务器怎么选?2025最新指南:就近选择地域,降低延迟;长期使用选包年包月,短期灵活选按量付费;企业选2核4G5M仅199元/年,个人选2核2G3M低至99元/年,高性价比爆款推荐,轻松上云。
168 11
|
11月前
|
存储 安全 iOS开发
内存卡怎么格式化?6个格式化方法供你选
随着使用时间的增加,内存卡可能会因为数据积累、兼容性或是文件系统损坏等原因需要进行格式化。那么怎样正确格式化内存卡呢?格式化内存卡的时候需要注意什么呢?本文会给大家提供详细的步骤,帮助大家轻松完成格式化内存卡的操作。
|
6月前
|
存储 Windows
内存卡坏了还能修吗?4种常见修复方法
内存卡出现“无法保存”或“存储异常”等问题时,不一定是硬件损坏,可能是系统错误或文件系统异常导致。本文介绍几种亲测有效的修复方法:1) 更换读卡设备排除接触问题;2) 格式化修复文件系统(需先备份数据);3) 使用DiskGenius检测坏道;4) 借助厂商工具深度修复。同时提供日常保养建议,如避免高温环境、养成数据备份习惯,延长内存卡使用寿命。通过这些方法,多数问题可轻松解决,无需更换硬件。
|
监控 JavaScript Java
Node.js中内存泄漏的检测方法
检测内存泄漏需要综合运用多种方法,并结合实际的应用场景和代码特点进行分析。及时发现和解决内存泄漏问题,可以提高应用的稳定性和性能,避免潜在的风险和故障。同时,不断学习和掌握内存管理的知识,也是有效预防内存泄漏的重要途径。
726 62
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
308 6
|
程序员 开发者
分代回收和手动内存管理相比有何优势
分代回收和手动内存管理相比有何优势
|
机器学习/深度学习 算法 物联网
大模型进阶微调篇(一):以定制化3B模型为例,各种微调方法对比-选LoRA还是PPO,所需显存内存资源为多少?
本文介绍了两种大模型微调方法——LoRA(低秩适应)和PPO(近端策略优化)。LoRA通过引入低秩矩阵微调部分权重,适合资源受限环境,具有资源节省和训练速度快的优势,适用于监督学习和简单交互场景。PPO基于策略优化,适合需要用户交互反馈的场景,能够适应复杂反馈并动态调整策略,适用于强化学习和复杂用户交互。文章还对比了两者的资源消耗和适用数据规模,帮助读者根据具体需求选择最合适的微调策略。
3756 5
|
缓存 监控 Java
在使用 Glide 加载 Gif 动画时避免内存泄漏的方法
【10月更文挑战第20天】在使用 Glide 加载 Gif 动画时,避免内存泄漏是非常重要的。通过及时取消加载请求、正确处理生命周期、使用弱引用、清理缓存和避免重复加载等方法,可以有效地避免内存泄漏问题。同时,定期进行监控和检测,确保应用的性能和稳定性。需要在实际开发中不断积累经验,根据具体情况灵活运用这些方法,以保障应用的良好运行。
|
算法 Java 程序员
内存回收
【10月更文挑战第9天】
473 5
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
140 2

热门文章

最新文章