基本概念
在谈垃圾收集器之前,我们先来捋一捋哪些内存会被当成垃圾回收。虚拟机的内存区域可以分为 程序计数器,虚拟机栈,本地方法栈,堆,方法区,直接内存。
程序计数器 和 虚拟机栈 和 本地方法栈 都为线程私有的,当线程结束之后其内存会被销毁,所以这部分不存在垃圾回收。
直接内存 是使用本地内存,所以垃圾收集器无法管理。
堆 是垃圾收集的主要区域。存储着几乎所有的对象 (现在已经有 栈上分配 等新技术可以在 堆 以外的内存区域分配对象,但还不太成熟,应用的地方较少)。
方法区 是垃圾回收的次要区域,主要是常量池的回收和无用类的卸载。常量池的回收比较容易,但类的卸载很难,因为要判断一个类是否无用很复杂。所以垃圾回收在方法区的回收效率很低。
ps 判断类是否无用:
- 在虚拟机中不存在该类的实例对象
- 该类的 Class 对象没有在任何地方上被使用(反射)
- 该类的 ClassLoader 已经被回收了
从上面已经可以知道垃圾回收在哪里会起作用,接下来我们再来看一看垃圾回收的标准(哪些内存区域会被当成垃圾)。因为回收的一般是堆里面的对象,所以用对象的角度来讲。判断对象是否应该被回收有两种经典算法:
- 引用计数法
在对象中维护一个计数器来记录对象的被引用数,每当有地方引用这个对象时,计数器 + 1,否则 - 1 ,当计数器为 0 时说明对象已死。Python,COM (微软的组件对象模型)等应用这种算法。但是在商用领域中一般不被使用。因为引用计数法不能处理对象相互引用的情况。
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
虽然 objA 与 objB 虽然都被设为 null,但其内部都含有对方的引用,所以两个对象的计数器都为 1。如果垃圾回收应用这种标准,则无法回收无用的对象。
- 可达性分析算法
通过一系列被称为 GC Root 的对象,向下遍历,所有不可达的对象都会被当成废弃对象。商用程序如 Java 等都是用这种算法。
ps GC Root 的选取:
- 虚拟机栈或本地方法栈中引用的对象(栈帧中的本地变量表)
- 方法区中的类静态属性或常量引用的对象
ps 虚拟机对引用进行了四种分类,与 GC 息息相关
- 强引用,对象如果含有一个强引用,那么就绝对不会被回收
- 软引用,用于描述那些有用但非必需的对象,当内存充足的时候不会被回收,当内存不足时才会被回收
- 弱引用,每次垃圾回收时都会被回收
- 虚引用,唯一的作用就是在被回收的时候会产生一条日志
垃圾收集器是虚拟机的具体实现,不同的虚拟机中的垃圾收集器会有所不同,但是基本都是围绕着类似的垃圾收集算法来进行的,接下来介绍三种常见的垃圾收集算法:
- 标记-清除算法 (Mark-Sweep)
先标记要回收的对象,然后统一回收。简单高效,但是会产生很多内存碎片,可能导致在分配大对象的时候找不到足够的内存区域,以致于再发动一次 GC。
- 复制算法
把一块内存区域分成两半(A 和 B),只在 A 中分配对象,垃圾回收时把 A 中有用的对象复制到 B,然后把 A 清空。
优点是不会产生内存碎片,复制对象的时候会把对象复制到一块去;缺点是只能使用一半的空间,太浪费资源了
- 标记-整理算法
标记-清除算法的升级,在回收对象的时候随带着整理对象,保证不会产生内存碎片。
- 分代收集算法
准确来说这不算一种算法,它只是把内存分成几个区域,有的使用这个算法回收垃圾,有的时候那个。但是这能根据具体情况来使用最适合的算法,效率很高。
比如堆被分为新生代和老年代,新生代中的对象有 98% 左右的对象是朝生夕死,就可以使用变种的复制算法 (将新生代分为 eden ,from survivor 和 to survivor,比例为 8:1:1,eden 和 from survivor 负责分配对象,垃圾回收时把有用的对象复制到 to survivor 中);而老年代中的对象一般存活率很高,所以可以使用标记-清除或标记-整理算法来实现。
以上内容为阅读 深入理解Java虚拟机(第2版)后的笔记及对 JDK8 的实践补充。看完这本书后最大的感觉就是,,,再看一遍,很多原来理解不了的知识点就可以看懂了,因为很多内容是前后呼应的。有兴趣的可以去阅读这本书,强推。