JVM—垃圾收集器
什么是垃圾
没有被引用的对象就是垃圾。
怎么找到垃圾
引用计数法
当对象引用消失,对象就称为垃圾。
对象消失一个引用,计数减去一,当引用都消失了,计数就会变为0.此时这个对象就会变成垃圾。
在堆内存中主要的引用关系有如下三种:单一引用、循环引用、无引用:
引用计数法性能比较低。
引用计数算法不能解决循环引用问题,为了解决这个问题,JVM使用了根可达分析算法。
根可达算法
根可达算法的基本思路就是通过一系列的名为GCRoot的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GCRoot没有任何引用链相连时,则证明此对象是不可用的,也就是不可达的。
可作GCRoots的对象:
- 虚拟机栈中,栈帧的本地变量表引用的对象。
- 方法区中,类静态属性引用的对象。
- 方法区中,常量引用的对象。
- 本地方法栈中,JNI引用的对象。
回收过程
即使在可达性分析算法中不可达的对象,也并非是"非死不可"。被判定不可达的对象处于"缓刑"阶段。要真正宣告死亡,至少要经历两次标记过程:
- 第一次标记:如果对象可达性分析后,发现没有与GC Roots相连接的引用链,那它将会被第一次标记;
- 第二次标记:第一次标记后,接着会进行一次筛选。筛选条件:此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。
第二次标记成功的对象将真的会被回收,如果失败则继续存活。
对象引用
在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)四种,这四种引用强度依次逐渐减弱。
引用类型 | 被垃圾回收的时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止时终止 |
软引用 | 内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 正常GC | 对象缓存 | GC后终止 |
虚引用 | 正常GC | 类似事件回调机制 | GC后终止 |
无引用 | 正常GC | 对象的一般状态 | GC后终止 |
强引用
代码中普遍存在,只要强引用还在,就不会被GC。
Object obj = new Object();
软引用
非必须引用,内存溢出之前进行回收,如内存还不够,才会抛异常。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
Object o = sf.get();//有时会返回null
System.out.println("o = " + o);
应用场景:软引用可用来实现内存敏感的高速缓存。
举例如下:
- 应用需要读取大量本地文件,如果每次读取都从硬盘读取会严重影响性能,如果一次性全部加载到内存,内存可能会溢出。
- 可以使用软引用解决这个问题,使用一个HashMap来保存文件路径和文件对象管理的软引用之间的映射关系。
- 内存不足时,JVM会自动回收缓存文件对象的占用空间,有效地避免了OOM问题。
弱引用
非必须引用,只要有GC,就会被回收。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
//System.gc();
Object o = wf.get();//有时候会返回null
boolean enqueued = wf.isEnqueued();//返回是否被垃圾回收器标记为即将回收的垃圾
System.out.println("o = " + o);
System.out.println("enqueued = " + enqueued);
- 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
- 作用:监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
虚引用
- 虚引用是最弱的一种引用关系。垃圾回收时直接回收
- 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, newReferenceQueue<>());
obj=null;
Object o = pf.get();//永远返回null
boolean enqueued = pf.isEnqueued();//返回是否从内存中已经删除
System.out.println("o = " + o);
System.out.println("enqueued = " + enqueued);
- 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
- 作用:跟踪戏象被垃圾回收的状态,仅仅是提供一种确保对象被回收后,做某些事情的机制。类似事件监听机制
如何清除垃圾
JVM提供3种方法,清除垃圾对象:
- Mark-Sweep标记清除算法
- Copyirvg拷贝算法
- Mark-Compact标记压缩算法
标记清除算法(Mark-Sweep)
最基本的算法,主要分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
缺点:
- 效率不高,标记和清除过程的效率都不高;
- 空间碎片,会产生大量不连续的内存碎片,会导致大对象可能无法分配,提前触发GC。
拷贝算法(Copying)
为解决效率。它将可用内存按容量划分为相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
一般采用这种收集算法来回收新生代,当回收时,将Eden和Survivor中还存活着的对象拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1
,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
- 优点:没有碎片化,所有的有用的空间都连接在一起,所有的空闲空间都连接在一起;
- 缺点:存在空间浪费;
标记-整理算法(Mark-Compact)
老年代没有人担保,不能用复制回收算法。可以用标记-整理算法,标记过程仍然与“标记-清除”算法一样,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
缺点:性能较低,因为除了拷贝对象以外,还需要对象内存空间进行压缩,所以性能较低。
分代回收(Generational Collection)
当前商业虚拟机都是采用这种算法。根据对象的存活周期的不同将内存划分为几块。
- 新生代,每次垃圾回收都有大量对象失去,选择复制算法。
- 老年代,对象存活率高,无人进行分配担保,就必须采用标记清除或者标记整理算法。
用什么清除垃圾
有 8 种不同的垃圾回收器,它们分别用于不同分代的垃圾回收。
- 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1、ZGC