C/C++ 这样的编程语言中,申请内存的时候,是需要用完了,进行手动释放的
C 申请内存
1)局部变量(不需要手动释放)
2)全局变量(不需要手动释放)
3)动态申请 malloc(通过 free 进行释放的)
C++ 申请内存
1)局部变量
2)全局变量/静态变量
3)动态申请 new 通过 delete 进行释放
上面的释放操作,是很容易遗忘的(执行不到),就会导致内存泄漏
很多编程语言,引入了 垃圾回收机制
不需要程序员写代码手动释放,会有专门的逻辑,帮助自动释放
垃圾回收,大大的解放了程序员,提高了开发效率
Java, Python,Go,PHP,JS 大多数主流语言都包含了GC功能
为啥C/C++ 没有引入 GC 呢?
C++ 的设计核心理念,有两个(C++的红线)
1)和 C 兼容(C语言写的代码,用 C++ 的编译器可以正常编译运行的)
2)把性能发挥到极致
引入 GC 会影响性能,引入了额外的运行时开销
GC 会影响执行效率
触发 GC 的时候,可能会涉及到 STW 问题,(stop the world 世界都停止),(GC 这样的"扫地"操作,就需要其他工作线程"抬脚",抬脚就无法继续工作了)
1.GC 回收的内存区域是哪个部分呢?
JVM
程序计数器
元数据区
栈
堆 => 主要是回收这个区域
2.GC 的目的是为了释放内存,是字节为单位"释放"的吗?
不是的,以"对象为单位"

按照对象为维度进行回收,更简单方便
如果是按照"字节维度",就可能针对每个对象都得描述出那部分需要回收,那部分不需要回收,比较麻烦了
堆上的内存 => new 的对象
3.如何回收?
1)找出垃圾,区分哪些对象是垃圾(后续代码不在使用)
2)释放这些垃圾对象的内存
如何"找出垃圾"(后续代码不在使用)
在 Java 使用对象,都是通过"引用"来进行的
使用对象,无非是使用的对象的属性/方法,都要通过 . 来进行 . 前面的部分就是指向对象的引用
如果一个对象以及没有任何引用指向它了,此时这个对象就注定无法使用了
判定一个对象是否是垃圾(抽象)
判定一个对象是是否有引用指向这个对象(具体,JVM 内部是有些办法可以做到的)
1.引用计数(Java 中没有使用,Python,PHP.. 采用的方案)
简单粗暴的方案
给每个对象都分配一个"计数器"

Python / PHP 等语言会搭配其他的算法机制,识别当前的引用是否构成循环引用
两个弊端
1)消耗额外的内存空间较大
如果对象本身就很小(就 4 个字节)
计数器占俩字节,相当于额外的内存空间多了 50%
2)循环引用问题(类似于死锁)
java
class Test {
Test t;
}
Test a = new Test();
Test b = new Test();

java
class Test {
Test t;
}
Test a = new Test();
Test b = new Test();
a.t = b;
b.t = a;
a = null;
b = null;

此时,这俩对象的引用计数是 1,不能释放.
但是,这俩对象却无法通过如何引用来访问到( A B 相互证明对方不是垃圾,实际上 AB 都是垃圾)
2.可达性分析(Java 的使用方案)
在 Java 代码中,每个"可访问的对象"一定是可以通过引用操作访问到的
JVM 安排专门的线程,负责上述的"扫描"的过程,会从一些特殊的引用开始扫描(GC roots)
1.栈上的局部变量(引用类型)
2.常量池里面指向的对象(final 修饰的,引用类型)
3.元数据区 (静态成员,引用类型)
这三组里面可能有很多变量,以这些变量为起点,尽可能的往里访问所有可能被访问到的对象
但凡是可以被访问到的对象,都"标记为可达"
JVM 又能知道所有的对象列表,去掉"标记为可达的",剩下的就是垃圾了
java
Node build() {
Node a = new Node();
Node b = new Node();
Node c = new Node();
Node d = new Node();
Node e = new Node();
Node f = new Node();
Node g = new Node();
a.left = b;
a.right = c;
b.left = d;
b.right = e;
e.left = g;
c.right = f;
return a;
}
Node root = build();
此时通过 root 这个引用,是可以访问到这个树上的如何一个对象的
假设,写 root.right.right = null;
这样的代码会使 f 无法被访问到 (f 已经没有引用指向了)
假设,写 root.right = null
这样的代码会使 c 不可达,由于 f 必须依赖 c, f 也一起不可达
这种方法,不需要引入额外的内存空间,但是需要消耗较多的时间,进行上述的扫描过程,这些扫描过程中也是容易触发 STW 的(时间换了空间),这里也不会涉及到"循环引用"
如何释放垃圾(回收内存)
关于内存回收,涉及到几种算法
1.标记 - 清除
标记,就是可达性分析,找到垃圾的过程
清除,直接释放这部分的内存(相当于直接调用 free / delete 释放这个内存给操作系统)
存在内存碎片问题
总的空闲内存空间,是比较多的 (一共3MB),但是这些空闲的内存空间,不连续
在申请内存的时候,都是在申请连续的内存空间,当尝试申请 2MB 的内存时候,就会申请失败
2.复制算法,解决内存碎片

被红钩选择中的是"垃圾"
把不是垃圾的对象,复制到另外一侧,把整个空间都释放掉

很好的解决了内存碎片的问题
弊端
1.内存浪费比较多
2.如果存活的对象比较多/比较大,复制的开销是非常明显的
3.标记 - 整理
类似于顺序表删除元素 - 搬运元素

这个方案,能够解决内存碎片,也能避免负责算法的内存浪费,但是,搬运对象的成本也可能会比较高
4.分代回收(综合方案),把上述的几个方案,结合起来,扬长避短
整个堆空间,分成"新生代","老年代"
年龄:一个对象经过垃圾回收扫描线程的轮次
可达性分析中,JVM 会不停地使用线程扫描这些对象是否是垃圾,每隔一定的时间,就会扫描一次,如果一个对象扫描一次,不是垃圾,年龄就 + 1,一般来说年龄超过 15(可以配)的就进入老年代
对于年轻对象来说,是容易成为垃圾的,年老对象,着不容易成为垃圾
"要死早死了"
比如 C 语言,已经存在了50年了,还可以预见这个 C 语言还有很大的希望在活50年,和 C 语言同时期的语言,都死的差不多了
针对新新生代,老年代,不同的特点,就可以采用不同的方案了

刚创建的新对象,放到伊甸区
如果对象活过一轮 GC,进入幸存区(通过复杂算法)
新对象,大多数是生命周期非常低的"朝生夕死"
这俩幸存区,同一时刻使用一个(相当于是复杂算法,分出两个部分)
每次经过一轮 GC 都会淘汰掉幸存区中的一大部分对象,把存活的对象和伊甸区中新存活下来的对象,负责算法拷贝到另一个幸存区(新生代是非常适合负责算法的)
如果这个对象在新生代中存活多轮之后,就会进入老老年代
老年代的对象由于生命周期大概率是很长的,没有必要频繁扫描
如果这个对象是非常大的,不适合使用负责算法,直接进入老年代
老年代回收内存采取的是 标记 - 清除 / 标记 - 整理(取决于垃圾回收的具体算法实现了)