Skip to content

Kilokiyiu/LeakDetector

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 

Repository files navigation

项目介绍


1.内存基础知识

下面的列表总结了重要的 CLR 内存概念:

  • 每个进程都有其自己单独的虚拟地址空间。 同一台计算机上的所有进程共享相同的物理内存和页文件(如果有)。

  • 默认情况下,32 位计算机上的每个进程都具有 2 GB 的用户模式虚拟地址空间。

  • 作为一名应用程序开发人员,你只能使用虚拟地址空间,请勿直接操控物理内存。 垃圾回收器为你分配和释放托管堆上的虚拟内存。

  • 如果你编写的是本机代码,请使用 Windows 函数处理虚拟地址空间。 这些函数为你分配和释放本机堆上的虚拟内存。

  • 虚拟内存有三种状态:

状态 描述
免费 该内存块没有引用关系,可用于分配。
保留 内存块可供你使用,不能用于任何其他分配请求。 但是,在该内存块提交之前,你无法将数据存储到其中。
已提交 内存块已分配给物理存储。
  • 虚拟地址空间可能会出现碎片,这意味着地址空间中存在一些称为缺口的空闲块。 当请求虚拟内存分配时,虚拟内存管理器必须找到满足该分配请求的足够大的单个可用块。 即使有 2 GB 可用空间,2 GB 分配请求也会失败,除非所有这些可用空间都位于一个地址块中。

  • 在物理内存压力(对于物理内存的需求)较低的情况下,页文件也会被使用。 首次出现物理内存压力较高的情况时,操作系统必须在物理内存中腾出空间来存储数据,并将物理内存中的部分数据备份到页文件中。 该数据仅在需要时才会分页,因此即使在物理内存压力较低的情况下,也有可能会出现分页。


2.实现垃圾回收的原理:

实现垃圾回收主要涉及两个算法:分别是:标记压缩算法和升代回收算法

1. Mark-Compact标记压缩算法

  • Mark阶段:假设所有对象都可以回收,则找到不可以被回收的部分,打上标记
  • Compact阶段:移动不连续内存,从heap上基址开始排列,即进行碎片内存整理

内存经过整理压缩后,栈上的地址此时并没有被更新,所以在compact过程中,还需要修复引用对象的栈上的引用地址、CPU寄存器变量和堆上的其他引用

2. 升代回收算法

  • 我们将内存中的对象根据存在时间分为三个代:第0代,第1代,第2代。并分别给这三个代的对象分配了三段内存用于存储对象,比如第0代分配5m,第1代分配15m,第2代分配2g
  • 如果一个对象第一次进入内存(托管堆)中,不超过一定大小(一般指小于85000个字节), 那么它就是第0代,并进入给第0代分配的内存空间中,当第0代内存已经被占满,后续有新的对象需要引入,此时触发第0代GC,清理不需要引用的对象,如果第0代经过一轮GC后,还有对象存在,则升为第1代,并将它存放到第1代分配的内存中,一样,如果第1代的内存占满,触发第1代的GC,还有对象存在,则升级为第2代,分配到第2代的内存中

升代回收算法是一个统计学的方法,我们通常给第0代和第1代分配较少的内存空间,每次内从空间被占满而需要新向托管堆中推入新的对象时,会触发GC,从而确保第0代和第1代可以及时清理不需要的垃圾


3.Memcheck的机制

3.1 使用影子内存来模拟字节数据的访问情况

在影子内存中,一个字节有9位数据,分别是8个V位和1个A位:

  • A 位 (Addressability):1位,表示这个地址能不能访问。
  • V 位 (Validity):8位,表示这个字节里的8个比特,哪些是已初始化的。

但是,如果直接用这种方式来运行一个影子内存,你会发现影子内存的开销比原本程序运行使用的内存开销还要大,所以我们得想办法减少影子内存的开销

工程师经过统计发现:影子内存各个字节的核心V位,一般都只有两种极端的状态:

  • 全是0:要么全是已经初始化的数据
  • 全是1:要么全是未初始化的数据

所以我们可以将影子内存映射为只有两位的状态机:

状态 状态名 状态描述
00 NOACCESS 地址不可访问。既然不可访问,其V位状态无意义。
01 UNDEFINED 地址可访问,但值完全未定义。即A=1,8个V位全为1。
10 DEFINED 地址可访问,且值完全定义。即A=1,8个V位全为0。
11 PARTDEFINED 地址可访问,但值部分定义。即A=1,8个V位不全相同。需查次级表。

注意:这里状态机的两位数据,它的每一位并不代表A位或者V位,只是用与描述四种状态,比如,我们将地址可访问 (A=1),但值全未定义 (8个V位全为1)的情况下定义位01状态

3.2 影子内存使用两级页表结构来映射整个内存

我们使用影子内存有一个核心矛盾:需要为4GB的内存空间快速提供一个对应的影子内存,但是为了保证不过度消耗资源,绝对不能使影子内存的占用超过4GB。所以我们参考操作系统的两级页表来实现,这两个表叫做:Primary Map(一级表)和Secondary Map(二级表)

我们以32位系统为例:

将系统的32位拆分成两个部分:
  • 我们定义页大小:64K。(这里的页大小是只影子内存的管理粒度,不是CPU世界的页大小)
  • 高16位:作为一级表的索引,表示有2^16 = 65536个条目。
  • 低16位:作为二级表内的偏移,一个二级表正好覆盖2^16 = 65536个字节,总共64Kb
一级表是一个拥有65536个元素的数组,每一个元素都是一个指向二级表的指针,如果本地内存中有64K的空间没有被使用,则对应的指针就是空白,也就不会指向二级表。
二级表是一个由一级表映射出来的,管理范围覆盖本地内存64K的影子内存,我们使用二级表来表示本地内存的状态。因为我们使用2bit的大小来表示,所以本地64K的内存空间,映射到二级表就需要64K x 2bit = 16K(一个bit为8个字节,1K为1024个bit),也就是说,对于64K的本地内存,我们只需要16K的影子内存来表示。
总结起来就是:一级表(前16位)用来映射(说简单点就是指向了二级表)出二级表(后16位),而这个二级表,就是影子内存,用来存储本地内存通过一级表映射出来的状态,通过这种方法,我们4GB的本地内存,理论上就只需要1GB的影子内存,整体的比例是4:1.

截止到目前为止,我们会发现一个和前文有巨大矛盾的地方,就是:我们不使用二级表,单纯位4GB提供的影子空间是4GB x 2bit = 1GB,不也是4:1吗?使用二级表也是4:1分配,为何二级表会更省内存呢?请继续往下看

3.3 二级表的优势

前文提到了,影子内存用来存储本地内存的状态,而工程师将其简化成了2位数的状态机来表示,总共有四种状态。聪明的你一定想到了,那么多数据,却只有四种状态,而且我们用一级表作为指向二级表的指针,那假如有很多本地内存都都是同一个状态,那么通过一级表指向二级表的时候,都指向同一个状态不就行了?

没错,这就是我们能节省大量内存的关键。我们可以提前在影子内存中设定两个模板二级表:

  • 一个全00的模板:代表地址不可访问,状态无意义
  • 一个全10的模板:代表地址可访问,状态已定义

那么所有未映射的区域,他的指针都指向同一个00的影子内存,程序加载时,已经有的代码段和数据段都指向同一个10的影子内存,这样就节省了大量的内存消耗,而当程序需要给一个原本指向模板的地址写入新的状态时,他的状态只需要从00编程01,此时表示这是一个可访问的,可读写的内存段。实现这些操作的步骤就是:

  • 1.我们需要写入新的数据
  • 2.Memcheck发现此时一级表上的指针指向一个只读的共享模板,这时指向的影子内存是不可被写入的
  • 3.Memcheck立刻从影子内存上分配一段16K的新的可读写的私有二级表,此时表中是空的,没有保存任何状态
  • 4.在分配新的空间后,Memcheck再将原模版(00)复制一份到刚分配的可读写的表上,然后再将此表的状态改成01,表示此表现在可读写

这样就分配了一块新的,可读写的影子内存用来表示本地内存的状态了


4. C++ 内存泄漏的本质

4.1 泄漏的定义

4.2 泄漏的分类(对标 Valgrind mc_leakcheck.c 的 9 种 case)

4.3 C++ 中常见的泄漏场景

4.4 new/delete 的各种变体

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages