文章目录
- 概述
- 垃圾对象的判定
- 引用计数
- 可达性分析
- 回收垃圾
- 标记清除
- 复制算法
- 标记整理
- 分代回收
概述
垃圾回收就是帮我们把不用的内存垃圾自动释放掉
什么是垃圾呢?就是指不再使用的垃圾
如果不进行垃圾回收就会导致一个严重的问题,内存泄漏
内存泄漏: GC无法及时识别可以回收的内存进行释放,也就是内存占着不用也释放不了,导致内存的浪费,以至于后续的内存申请都会操作失败
如果内存泄漏的越多就会导致内存溢出
内存溢出: 程序申请内存时,没有足够的内存供申请者使用,导致数据无法正常存储到内存中
内存泄漏是一个很严重的问题,所以大佬们就想了一些办法来解决这个问题,而GC
就是其中最主流的一种方式
- GC的好处:让程序猿写代码简单点,不容易出错
- GC的坏处:需要消耗额外的系统资源,也有额外的性能开销
GC还有一个比较关键的问题
STW(Stop The World)
问题
有时候垃圾很多,触发一次GC,开销会非常大,把系统资源吃掉很多,另一方面,GC回收垃圾的时候会涉及一些锁的操作,导致业务代码无法正常执行
JVM中存在许多内存区域,像堆,栈,元数据区等,GC主要针对堆进行释放的,GC是以
“对象”
为基本单位进行回收的,回收的是整个对象都不再使用的这种情况,而不会回收半个对象(一部分使用,一部分不再使用)
GC的工作过程分为两个步骤:
- 找/判定垃圾:找到内存中不再使用的对象,判定其为垃圾
- 释放垃圾:找到不再使用的对象后,释放对象
垃圾对象的判定
判定对象是垃圾的关键点在于是否有引用指向它
引用计数
注意: 引用计数不是Java中采用的方法,而是Python,PHP采用的方法
引用计数就是给每个对象分配了一个计数器,如果每次创建一个引用指向该对象,则计数器+1,如果每次销毁一个引用,该计数器-1,如果计数器为0,则判定为垃圾
缺点:
- 内存空间利用率低,因为每个对象都要分配空间给计数器使用
- 存在循环引用问题
public class Test {
Test t = null;
public static void main(String[] args) {
Test t1 = new Test(); //对象1引用计数为1
Test t2 = new Test(); //对象2引用计数为1
t1.t = t2; //对戏2引用计数为2
t2.t = t1; //对象1引用计数为2
}
}
说明: 如果将t1和t2销毁,那么对象1和对象2的引用计数都为1,但是此时引用计数不为0,不能判定为垃圾,但实际上这两个对象已经不能被访问到了
所以Java没有采用引用计数这种方法,Python,PHP使用引用计数需搭配其它的机制来避免这种循环引用的问题
可达性分析
Java中的对象都是通过引用
来指向并访问的,并且一个引用指向的对象,该对象的成员通常又指向别的对象
可达性分析就是把所有这些对象被组织的结构视为是树,从树的
根节点(GCRoots)
出发遍历树,所有能被访问到的对象就标记为“可达”,不能访问到的对象就是不可达
JVM有一个所有对象的名单,通过上述遍历,把能访问到的标记为可达,剩下未标记的就是不可达的就会判定为垃圾
遍历的起点称为GCRoots
,通常可作为GCroots有以下几类:
- 栈上的局部变量
- 常量池中的对象
- 静态成员变量
回收垃圾
标记清除
该种方法简单粗暴,直接清除垃圾
缺点: 存在内存碎片
问题,因为释放的空闲空间不是连续的,但是申请内存时要求是连续空间,需要申请大一点的内存时,总的内存是够的,但都是零散的,零散的内存不够,此时就会申请失败
复制算法
-
该方法是将内存分成两半,每次只用一半
-
把不是垃圾的对象复制到另一半,然后把整个空间删掉
-
每次触发复制算法都是把内存中的数据往另外一侧复制
此方法解决了内存碎片问题
缺点: 空间利用率低,如果垃圾少,有效的对象多,复制成本就很大
标记整理
此方法类似顺序表删除中间元素,会有元素搬运的操作
让存活的对象往一端移动,直接清除存活对象边界之外的内存
保证了空间利用率,也解决了内存碎片问题
缺点: 效率低,元素搬运的空间比较大那开销也大
标记整理
只解决了复制算法中空间利用率低
的问题,没有解决复制成本大
的问题
分代回收
上述的回收策略都不完美,所以基于上述策略搞了一个复合策略分代回收
那分代是咋分的呢?
- 基于经验规律,如果一个对象存活时间长,那么还会长时间的存活下去
- Java对象的生命周期要么特别短,要么特别长
- 根据生命周期的长短,分别使用不同的算法
给对象引入一个年龄,单位为经历
GC的次数
,如果经历GC的次数越长,说明该对象存活的时间越长
JVM将堆划分为一系列区域
- 刚new出来的对象放在伊甸区(伊甸区大,幸存区小)
- 熬过一轮GC,对象就要被放到幸存区(基于复制算法)了(Java中的大部分对象生命周期都很多,熬不过第一轮GC)
- 在幸存区的对象,如果不是垃圾就会被拷贝到另一个幸存区(幸存区只会使用一个),然后直接清除现在的幸存区空间,后序的几轮GC都在这两个幸存区之间来回拷贝(复制算法)
- 如果对象在幸存区来回拷贝许多次了,那么该对象就会进入老年代
- 针对老年到,也要进行GC,只是GC的频率降低了,如果老年代的对象是垃圾了,使用标记整理的方式释放