一. 垃圾回收所针对的区域
首先Java虚拟机中共有五大内存区域:
程序计数器,Java虚拟机栈,本地方法栈,java堆,方法区
为什么不针对前三个区域: 对于前三种内存区域,他们的生存周期都是与线程相同,并且这些线程在运行之前就已经可以从类结构中确定他们所需内存,因此不需要过多的考虑他们的内存回收。
为什么针对后两个区域: 而对于java堆和方法区则不同,比如,一个泛型数组 new ArrayList(),指向的是一个父类接口,具体存储的子类由终端交互获得,然后才创建,这种情况在编译器就不可能知道具体需要分配多大的内存了。
因此垃圾回收机制主要针对的是: java堆和方法区
由于方法区的垃圾回收在java虚拟机规范中并没有强制要求,因此下面主要讨论的是在堆中的垃圾回收机制,而方法区将在稍后单独提及。
👇
二. 垃圾回收过程
先回答三个问题:
- 什么时候回收?
- 哪些需要回收?
- 如何回收?
什么时候? 当需要内存,但是内存又不够的时候,将会触发回收机制。
哪些需要回收? 有两种算法判断一个对象是否需要回收:
- 引用计数算法: 简单说就是给每个对象一个计数器,如果有引用就递增,引用失效就递减,当这个值任意时刻都为0,则此对象不可用。
优点:简单,效率高。
缺点:很难解决对象之间的相互循环引用。 - 根搜索算法: 使一些对象当做树的根(GC Roots),如何没有到根的引用路径,则此对象不可用。
可作为根的对象有:
a. Java虚拟机栈中引用的对象
b. 方法区中的类静态属性引用的对象
c. 方法区中常量引用的对象
d. 本地方法栈中Native方法引用的对象
如何回收? 回收的方式有三种:
- 标记——清楚算法: 就是标记好需要回收的内存之后,直接清理,这样做的缺点在于:效率低,产生大量额空间碎片。
- 复制算法: 此算法和下面的算法一样,都是在标记——清楚算法的基础上,提高他的效率,减少空间碎片做努力。复制算法的思想将内存分为两类区域,一类用来实际使用,一类用来在回收时作为中转空间,既将在回收后还存活的对象放入这个中转空间,然后直接清楚掉另一类空间的所有内容。
- 标记——整理算法: 这个算法是将标记之后依然存活的的对象依次重新从一端开始放入,然后清楚调用后面所有的内存。
- 分代收集算法: 这个算法将内存区域分为新生代和老年代。故名思意,新生代代表这个区域类的对象流动快,更新快。而老年代则与此对立,流动慢,更新慢。而根据他们各自的特点,新生代主要使用的是复制算法,老年代主要使用的是标记——整理算法。
分代 | 特点 | 主要使用算法 |
---|---|---|
新生代 | 对象更新快 | 复制算法 |
老年代 | 对象更新慢 | 标记——整理 |
接下来就是垃圾回收的具体过程👇
其中对finalize()方法的判断是因为,利用引用计数器或根搜索算法标记出来的对象并不是一定会被清楚,而是还需要接下来的两次判断。也就是说,只有当覆盖了finalize()方法,并且是第一次调用,才有机会在这个方法中让这个被判死刑的对象起死回生!
方法区的回收: 方法区中存放了,类的信息,常量,静态变量,即时编译器生成的代码和数据,以及运行时常量池。 而方法区的回收,主要针对的就是常量池和类的卸载。
常量池回收与堆相似,如果常量池中的常量没有任何一个对象引用,那么就会被回收。
而对于类的卸载就麻烦许多,它需要满足下面三个条件才能回收:
- 该类所有的实例都已经被回收,也就是Java堆中不存在任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
三. 七种垃圾收集器对比
收集器 | 算法 | 应用代 | 线程支持 | 特点 |
---|---|---|---|---|
Serial收集器 | 复制算法 | 新生代 | 单线程 | 简单高效,但GC过程中会停止所有用户线程 |
ParNew收集器 | 复制算法 | 新生代 | 多线程 | 与Serial差不多,能与CMS配合工作 |
Parallel Scavenge收集器 | 复制算法 | 新生代 | 并行多线程 | 注重吞吐量,具有自适应调节策略 |
Serial Old收集器 | 标记——整理 | 老年代 | 单线程 | Serial老年代版本 |
Parallel Old 收集器 | 标记——整理 | 老年代 | 并行多线程 | Parallel Scavenge老年代版本 |
CMS收集器 | 标记——清除 | 老年代 | 并发收集 | 并发手机,低停顿,但是对CPU资源敏感,无法处理浮动垃圾,由于使用标记——清除算法所以会产生大量空间碎片。 |
G1收集器 | 标记——整理 | 非分代方式 | 高吞吐量,低停顿 |