JVM的垃圾回收主要针对 “堆” 和 “方法区”,主要解决以下问题:
- 哪些内存需要回收?(对象存活判定算法)
- 触发垃圾回收的条件?
- 如何回收?(垃圾收集算法)
- 用什么回收?(垃圾收集器)
本文先介绍 对象存活判定算法、垃圾收集算法,然后介绍常见的 JVM 垃圾收集器。
1. 对象存活判定算法
一般来说,判定对象是否可以被回收有许多算法,目前主流的算法有 引用计数法、可达性分析法,本节对这两种算法进行简要介绍。
1.1 引用计数法
每个对象维护一个引用计数器。当有一个新的引用指向该对象时,引用计数器就+1;当指向该引用对象失效时,计数器就-1;当引用计数器值为0时,说明该对象可以被回收。
JVM 没有选用 引用计数法 来管理内存,其主要原因就是引用计数法很难解决对象之间的循环引用问题。
循环引用示例:
public class MyObject {
public Object ref = null;
public static void main(String[] args) {
MyObject myObject1 = new MyObject();
MyObject myObject2 = new MyObject();
myObject1.ref = myObject2;
myObject2.ref = myObject1;
myObject1 = null;
myObject2 = null;
}
}
如上图所示,白色框代表在堆中开辟了一个内存空间,黄色和蓝色线代表引用,各位两条,也就代表计数器各为2,myObject1 和 myObject2 分别指向 null,线各自消失一条,计数器减1,但还剩1,也就如图中ref的指向,没有恢复0,所以不会被回收。
1.2 可达性分析法
目前主流的JVM都采用可达性分析算法来判定对象是否存活。
可达性分析法:通过"GC Roots"对象(必须活跃的引用)作为起点,从起点开始进行图的遍历,所走过的路径称为引用链。当一个对象不在引用链上时,则该对象被判定为可回收对象。
上图 Object 5 、6 、7 会被回收。
可以作为“GC Roots”的对象包括以下几种:
- 虚拟机栈 中引用的对象;
比如正在运行的线程的虚拟机栈上的引用变量。 - 本地方法栈中JNI(Native方法)的引用的对象;
- 方法区中的类静态属性引用的对象;
- 方法区中的常量引用的对象。
1.3 Java引用类型
怎么样的对象算没有被引用?Java语言其实一共定义了4种引用,按强度从大到小依次是:
强引用(Strong Reference)
强引用是使用最普遍的引用,GC永远不会回收被强引用的对象。
例:Object obj=new Object();
软引用(Soft Reference)
软引用的对象,只有在内存不足时,GC会对其回收。
也就是说,只有发生 OOM 之前的 GC,才会对软引用的对象进行回收
软引用有时被用来实现内存敏感的高速缓存。
例:
// 获取对象并缓存
SoftReference softRef = new SoftReference(new SomeClass());
// 从软引用中获取对象
Object cache = softRef.get();
if (cache == null) {
// 当软引用被回收后重新获取对象
cache = new SomeClass();
}
return (SomeClass)cache;
弱引用(Weak Reference)
弱引用的对象拥有更短暂的生命周期。弱引用与软引用的区别在于:在GC进行存活对象判定时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它。
Car car = new Car();
WeakReference<Car> weakCar = new WeakReference<Car>(car);
//假设此处进行了一次GC
System.gc()
//打印true
System.out.println(weakCar.get()==null);
虚引用(Phantom Reference)
虚引用是最弱的一种引用关系。
2. 垃圾回收触发条件
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。对于一个拥有终结方法的对象,在垃圾收集器释放对象前必须执行终结方法。但是当垃圾收集器第二次收集这个对象时便不会再次调用终结方法。
Minor GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区,然后整理Survivor的两个区。这种方式的GC是对新生代的Eden区进行,不会影响到老年代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。
Full GC
对整个堆进行整理,包括年轻代、老年代 和 永久代。Full GC因为需要对整个对进行回收,所以比 Scavenge GC 要慢,因此应该尽可能减少 Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于 Full GC 的调节。有如下原因可能导致Full GC:
老年代(Tenured)被写满
永久代(Perm)被写满
System.gc() 被 显示 调用
3. 垃圾收集算法
3.1 标记-清除算法
标记-清除(Mark-Sweep)算法,分为 标记 和 清除 阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点:
- 效率问题;
标记和清除两个过程的效率都不高 - 空间问题;
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
3.2 复制算法
复制(Copying)算法,将内存分为相等的两块,每次只使用一块。当这块内存用光,就将还存活的对象复制到另外一块,然后再把已经使用过的空间一次清理掉。
优点:内存分配/清除的效率都很高,不用考虑内存碎片等情况。
缺点:内存缩小为原来的一半,空间利用率低。
现代商用JVM一般采用复制算法来回收新生代,因为新生代中大部分对象都是朝生夕死的。实际一般按照8:1:1的比例将内存空间划分为一块Eden区(80%)和两块Survivor区(各10%)。
3.3 标记整理算法
标记-整理(Mark-Compact)算法,标记过程和标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
现代商用JVM一般采用标记-整理算法回收老年代。因为老年代对象存活率高,一般只需要清除少量对象。
3.4 分代收集算法
目前大部分JVM都采用的分代收集(Generational Collection)算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域,不同区域采用不同的垃圾回收算法。
一般情况下将堆区划分为老年代(Tenured Generation)、新生代(Young Generation)、永久代。
新生代:对象朝生暮死,存活率低,一般选用“复制算法”;
老年代:对象存活率高、没有额外空间对对象进行分配担保,一般采用“标记-整理”算法。
4、JVM垃圾收集器
收集器名称 | 介绍 | 使用时期 | 算法 |
---|---|---|---|
Serial | 单线程收集器,暂停所有线程 | 新生代 | 复制算法 |
Serial Old | 单线程收集器,暂停所有线程 | 老年代 | 标记-整理算法 |
ParNew | 多线程收集器,与用户线程并发 | 新生代 | 复制算法 |
Parallel Scavenge | 多线程收集器,与用户线程并发,可控制吞吐量 | 新生代 | 复制算法 |
Parallel Old | 多线程收集器,与用户线程并发 | 老年代 | 标记-整理算法 |
CMS | 多线程收集器,与用户线程并发;对CPU资源敏感;标记阶段,用户线程依旧运行 | 老年代 | 标记-清除算法 |
G1 | 技术前沿成果;空间整合;可预测停顿,消耗时间短 | 没有限制 | 标记-整理算法 |
参考
https://www.jianshu.com/p/667e0d876666
https://www.jianshu.com/p/3d9484f266ae