JVM系列二:垃圾回收
什么时候回收对象
引用计数法
1、原理:为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
2、缺点:无法解决循环引用问题
可达性分析
1、原理:以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
2、可作为GC Root的对象:
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 JNI 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
方法区回收
1、主要是对类的卸载和对常量池的回收,对于大量引用动态代理和反射的场景下,类的卸载是具有重要意义的
2、类的卸载需满足以下三个条件,但是就算三个条件都满足也不一定就被卸载
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
finalize方法
1、类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
2、当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。
自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法
四种引用
不管是引用计数法还是可达性分析,引用的判断与计数都是很重要的
强引用
1、特点:不会被回收
2、构造方式:new
1 Object object = new Object();
软应用
1、特点:只有在内存不够的时候才会被回收
2、构造方式:SoftReference
1 Object object = new Object(); 2 SoftReference<Object> sf = new SoftReference<Object>(object); 3 object = null;//使对象只能被软引用关联
弱引用
1、特点:下一次内存回收的时候一定会被回收
2、构造方式:WeakReference
1 Object object = new Object(); 2 WeakReference<Object> sf = new WeakReference<Object>(object); 3 object = null;
虚引用
1、特点:没有办法得到一个虚引用对象,设置它的唯一目的就是在系统回收它的时候得到一个通知
2、构造方式:PhantomReference
1 Object object = new Object(); 2 PhantomReference<Object> sf = new PhantomReference<Object>(object); 3 object = null;
垃圾收集算法
标记-清除算法
1、过程
(1)标记阶段:程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。
(2)清除阶段:会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。
回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。
在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;
如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。
2、图解
3、缺点
- 标记和清除的过程效率都不高
- 可能产生大量内存碎片
标记-整理算法
1、过程
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
2、图解
3、缺点
每次都要移动存活的对象
复制算法
1、过程
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
2、图解
3、缺点
每次只能用到局部的内存
分代收集算法
1、按照对象生存周期将内存分为不同的区域主要分成新生代和老年代,不同的区域采取不同的收集算法
2、新生代:采取复制算法
老年代:采取标记-清理算法或者标记-整理算法
垃圾收集器
新生代收集器
Serial收集器
ParNew收集器
Parallel Scavenge收集器
与 ParNew 一样是多线程收集器。
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
老年代收集器
Serial Old收集器
Parallel Old收集器
CMS收集器
G1收集器
搭配关系
比较总结
收集器 | 收集算法 | 新生代/老年代 | 备注 |
Serial | 复制算法 | 新生代 | ![]() |
ParNew | 复制算法 | 新生代 | ![]() |
Parallel Scavenge | 复制算法 | 新生代 | ![]() |
Serial Old | 标记-整理算法 | 老年代 | ![]() |
Parallel Old | 标记-整理算法 | 老年代 | ![]() |
CMS | 标记-清除算法 | 老年代 | ![]() |
G1 | 标记-整理算法 | 新生代+老年代 | ![]() |