GC是什么?
在计算机科学中,垃圾回收(英语:Garbage Collection,缩写为GC)是指一种自动的存储器管理机制。当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。垃圾回收器可以减轻程序员的负担,也减少程序中的错误。
——来自维基百科
通俗来讲就是,你家里有 100 平方,你会买各种生活用品和家居回来,这些物品都会占用空间。过了一段时间,你会通过各种情况去判断这些物品还需不需要,然后再去进行清除和整理。
其中:
- 家里100 平方:指的是总内存空间大小
- 生活用品和家具所占用的空间:指的是程序所所占用的内存空间
- 判断物品是否需要:指的是垃圾回收算法
- 清除和整理:指的是内存回收,以及对内存进行管理
问题来了,无房人士,“家”在哪?
GC在哪运行?
首先,我们先来了解下 class 文件的运行流程:
其中,Java 堆主要用来存放在运行时创建的 Java 对象,当剩下的内存空间不足以存放新建的 Java 对象,这时,就需要 GC 进行内存管理。
GC运行机制
GC 需要进行对象内存回收的之前,需要知道哪些对象的内存可以进行回收。
判断对象内存是否能回收
目前较为流行有两种方式:
引用计数法:
每个 Java 堆中存储的 Java 对象内部都维护着一个 counter 计数器,每当新增一个对象引用指向该对象时,该 counter 则 +1,否则 -1。当 counter 值为 0 的时候,说明该对象可回收。
这种做法的优点是能快速判断该对象是否能回收。但是却有一个致命的缺点,那就是当出现循环的对象引用时,这时就无法回收该对象内存。如下图:
对象 A、B、C 都有一个对象引用指向,他们的 counter 都为 1,但是实际上已经不再使用这三个对象了,但是还是会被判断这三个对象不可回收。
可达性分析算法
JVM 使用该算法。
首先,先确认某些对象是不允许被回收的,也就是一系列的 GC Root 对象,然后再看下 GC Root 对象引用了哪些对象 A,再看这些对象 A 引用了哪些对象 B,以此类推,就会出现一个 GC Root 引用链:
这条 GC Root 引用链上的对象都设定为不可回收,反之,不在 GC Root 引用链上的对象则为可回收。
虽然这个算法在理论上是没有问题,但是在实际开发中,很容易出现该被回收的对象,由于被 GC Root 对象直接或间接引用,无法被回收,导致内存泄漏。
既然有可能会出现内存泄漏,那么我们就要知道什么会成为 GC Root,从而在适当时机释放 GC Root,避免内存泄漏。
可以作为 GC Root 的对象:
- Java 虚拟机栈(局部变量表)中引用的对象
- 方法区中静态引用指向的对象
- 仍存活的线程对象
- Native 方法中 JNI 引用的对象
前三种为应用开发中最常见的对象,但是仅靠描述,可能基础较差的同学还是有点蒙,对此,我再进行相应举例:(至于第四种,我对于 JNI 不够了解,还是不装这逼了🤣)
- Java 虚拟机栈(局部变量表)中引用的对象
- 正在执行的方法里面所引用的对象。这个很容易理解,就是代码执行到了哪个方法,这个方法里面的对象都是。
- 方法区中静态引用指向的对象
- Java 文件中的 static 对象。所以使用单例直接存储持有 Activity Context 的对象,就会导致该 Activity 资源无法释放。
- 仍存活的线程对象
- new Thread().start() 后的对象。由此可以延伸到 AsyncTask、Handler等,只要内部新建并启动线程的都算。使用内部类方式创建线程对象,当宿主对象想要销毁的时候,但由于内部类默认持有外部引用,导致宿主对象无法被回收。
GC回收内存
当标记了哪些对象内存可以被回收,剩下的就是回收操作了。当然,并不是直接回收就行了,毕竟内存就像一张白纸,直接回收内存就像在白纸上挖洞,这些洞大大小小,分部不均,导致后续想再从这些洞中分配大内存就变得十分困难,会频繁触发 GC。因此,GC 回收内存其实也有相应的算法的:
标记清除算法
这个就是最简单粗暴的方式,标记完后直接回收。优点是速度快,但缺点就非常明显:内存碎片化严重,后续想分配大内存困难。
复制算法
把当前的内存空间一分为二,每次只用其中一半,当 GC 的时候,将存活的对象拷贝到另一半内存空间,原有的内存空间直接清除。
缺点就是太浪费内存空间了,相当于可用内存空间减半!
标记压缩算法
将所有存活的对象压缩到内存的一端,然后把剩下的内存空间全部清除。
这种清理完的结果十分优秀,不过就是要频繁移动对象内存,需要消耗过多性能。
分代算法
大佬总是最后登场。
首先,我们先看下其模型:
将内存分为新生代、老年代,其中新生代里面包含 Eden、Survivor0、Survivor1。
当分配对象内存的时候,优先分配到 Eden 区域。
第一次 Monitor GC,将 Eden 存活的对象复制到 Survivor0 中,剩下空间清除:(复制算法)
第二次 Monitor GC,将 Eden 和 Survivor0 存活的对象复制到 Survivor1 中,剩下空间清除:(复制算法)
第三次 Monitor GC,将 Eden 和 Survivor1 存活的对象复制到 Survivor0 中,剩下空间清除:(复制算法)
依次不断循环第二次和第三次 Monitor GC 的操作,直到达到 15 次的操作,或者其中区域到达阈值,这时就会把存活的对象 Copy 到老年代中。(复制算法)
等老年代也到达阈值的时候,就会触发 Full GC,Full GC 包含 Monitor GC 的操作。(老年代区域使用标记压缩算法)
特殊情况
当然,也有特殊情况:
由于老年代的内存空间一般比新生代的大,所以有时申请对象内存空间是,假如新生代无法存放,这时就会直接存储到老年代中。(出道即巅峰🤣)
还有另外一种情况:
老年代中的对象引用了新生代的对象,这时候就出问题了。em…什么问题?可能有些同学还没反应过来,我稍微解释下:
之所以分代算法效率比较高,那是因为它把对象分成了几块区域,像一些比较长命的对象就存放在老年代中,比较短命的对象存放在新生代,这样 GC 的时候,几乎可以忽略老年代的对象,直接清理新生代的对象即可。但是,由于老年代引用了新生代,导致 Monitor GC 的时候需要去扫描老年代,不然怎么知道哪些对象需要保留下来给老年代引用?
其实解决的方式也很简单:
把老年代区域划分为多个区域,并且使用 card table 记录这些需要有没有引用到新生代,有的话,Monitor GC 的时候也要扫描对应的老年代区域。
具体模型如下:
这是我的公众号,欢迎关注支持下,谢谢!