谈一谈JVM垃圾回收
前言
如果要问Java与其他编程语言最大的不同是什么,我第一个想到的一定就是Java所运行的JVM所自带的自动垃圾回收机制,以下是我学习JVM垃圾回收机制整理的笔记,希望能对读者有一些帮助。
<!-- more -->
哪些内存需要回收?what?
如何判断对象已死?有两种算法
引用计数算法
给对象添加一个计数器,每当有一个地方引用它时,计数器的值就加一,当引用失效的时候,计数器就减一 ,任何时刻计数器为0的对象就是不可能再被使用的时候。
这个算法看似不错而且简单,不过存在这一个致命伤(当两个对象互相引用的时候,就永远不会被回收)
public class Obj{ public Object instance=null; } Obj a=new Obj(); Obj b=new Obj(); a.instance=b; b.instance=a; a=null; b=null;
于是引用计数算法就永远回收不了这两个对象,下面介绍另一种算法。
可达性分析算法
通过一系列被称为“GC Roots”的对象作为起始点,从这些接点向下搜索,搜索所走过的路径称为引用链,当一个对象与任何一个引用链没有关联的时候则可以被回收。
Java中,可作为GC Roots的对象包括下面集中
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
浅谈引用
无论是计数器算法还是可达性分析算法,都与“引用”有关,下面是Java中4种引用,强度依次减弱
强引用
强引用就是我们平时最熟悉的
Object obj=new Object();
只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用
发生gc的时候,如果JVM内存充足则不回收,用SoftReference类来实现软引用。展示一个例子
SoftReference<Object> softReference=new SoftReference<>(new Object()); System.out.println("before gc "+softReference.get()); System.gc(); System.out.println("after gc "+softReference.get());
以下是输出
before gc java.lang.Object@2752f6e2 after gc java.lang.Object@2752f6e2
可以看到软引用的对象依然还在。
弱引用
一旦发生gc,无论JVM内存充足与否,都会回收掉,用WeakReference类来实现弱引用。展示一个例子
WeakReference<Object> softReference=new WeakReference<>(new Object()); System.out.println("before gc "+softReference.get()); System.gc(); System.out.println("after gc "+softReference.get());
以下是输出
before gc java.lang.Object@2752f6e2 after gc null
弱引用的对象已经被回收!
虚引用
虚引用也称为幽灵引用或者幻影引用,是最弱的一种引用,无法通过虚引用来取得一个对象实例,拥有虚引用的对象可以在任何时候被垃圾回收器回收。唯一的目的就是能在当这个对象被回收的时候收到一个系统通知,可用PhantomReference类实现虚引用。
回收方法区
垃圾回收大多发生在Heap(堆区),因为在方法区进行垃圾回收效率较低,要判定一个类是否是“无用的类”条件比较苛刻,类需要同时满足下面3个条件才能算是无用的类
- 该类所有实例已经被回收,堆中不存在该类任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
什么时候回收?when?
首先,GC又分为minor GC 和 Full Gc(也称为Major GC)。Java 堆内存分为新生代和老年代,新生代中又分为1个Eden区域 和两个 Survivor区域。
那么对于 Minor GC 的触发条件:大多数情况下,直接在 Eden 区中进行分配。如果 Eden区域没有足够的空间,那么就会发起一次 Minor GC;对于 Full GC(Major GC)的触发条件:也是如果老年代没有足够空间的话,那么就会进行一次 Full GC。
注意:上面所说的只是一般情况,实际上,需要考虑一个空间分配担保的问题:
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor GC,如果小于则看HandlePromotionFailure设置是否允许担保失败(不允许则直接Full GC)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor GC(如果尝试失败也会触发Full GC),如果小于则进行Full GC。
System.gc()就是指的Full GC
但是,具体程序执行到什么位置才会自动gc,这儿提两个概念Safepoint和SafeRegion。
安全点(Safepoint)
安全点的选定是以程序“是否具有让程序长时间执行的特征”为标准选定的,“长时间执行”最明显的特征就是
- 指令序列复用
- 循环跳转
- 异常跳转等
对于Safepoint如何在GC发生时让所有线程都跑到最近的安全点上停下来,有两种方案抢先式中断和主动式中断。
抢先式中断:不需要线程执行的代码主动配合,GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑到”安全点上。(这种方案几乎很少使用)
主动式中断 :当GC需要中断线程的时候,不直接对线程操作,仅仅简单设置一个标志,各个线程执行时主动去轮询这个标志,发现为true就自动挂起,轮询的地方和安全点是重合的。
安全区域(Saferegion)
当线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间,这时候就需要安全区域来解决。
当线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,当JVM在发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,他要检查系统是否完成了整个GC过程,如果完成了,线程就继续执行,否则必须等待直到收到可以安全离开Safe Region的信号为止。
如何回收?how?
标记-清除 算法
算法分为标记和清除两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。他的不足主要有两个:
- 效率问题:标记和清除两个过程的效率都不高
- 空间问题:标记清除之后,会产生大量不连续的内存碎片,空间碎片太多可能会导致之后分配大对象时,无法找到足够的连续内存而提前触发GC
复制算法
复制算法可以将容量划分为大小相等的两块,每次只使用其中的一块,当一块内存被用完了就将还存活的对象一次复制到另一块内存上,然后把已经使用过的内存一次清理掉。不过因此内存缩小为原来的一半,代价过高。
现在的商业虚拟机普遍采用这种算法来回收新生代,将新生代分为较大的一块Eden空间和两块较小的Survivor空间,HotSpot默认其比例为8:1:1,使用时,每次使用Eden加上其中一块Survivor,回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor中,最后清理掉Eden和刚才用过的Survivor区。
标记-整理 算法
标记过程与“标记-清除”算法一样,然后让所有存活的对象都向同一端移动,然后直接清理端边界以外的内存。
分代收集算法
将Java堆分为新生代和老年代,在新生代中每次垃圾回收都有大批对象死去,少量存活,可以使用复制算法,而老年代中对象存活率高没有额外的空间对它进行分配担保,必须使用“标记-整理”或者“标记-清理”算法来回收。
谈谈G1垃圾收集器
在最近更新的JDK9中,JVM默认的垃圾收集器切换成了G1,那么G1有什么特点呢?总结以下最大的四个特点
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU,(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器需要停顿Java线程执行的GC动作,G1可以通过并发的方式让Java继续执行。
- 分代收集:G1不需要其他收集器配合就能独立管理整个GC堆,G1采用不同的方式去处理新创建的对象、已经存活一段时间的对象、熬过多次GC的旧对象以获取更好的收集效果。
- 空间整合:G1从整体来看是基于“标记-整理”算法,局部上来看是基于复制算法,这两种算法都不会产生内存碎片,有利于程序长时间运行。
- 可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。