Java垃圾收集机制

本文是《深入理解Java虚拟机》一书中第三章的读书总结。

前面介绍了Java内存结构和HotSpot虚拟机在堆内存中管理对象的过程。不过,在Java程序中对象的创建是非常频繁的,而内存的大小又是有限的,为了内存的重复利用,就需要对内存中的对象进行垃圾收集。其实,这也是Java和C++的一个区别,在Java中可以进行自动的垃圾收集,而C和C++中需要程序员手动回收不再使用的对象。

Java中的垃圾收集是虚拟机要考虑的问题。那么以虚拟机的角度考虑,如果要收集虚拟机内存中的垃圾,需要考虑哪些问题呢?

  • Java虚拟机中的内存分为程序计数器、虚拟机栈、本地方法栈、Java堆和方法区等几部分,在哪些部分回收内存呢?
  • 确定了要回收的内存,内存中必然存在着很多内容,如何判定这些内容就是不需要的垃圾了呢?
  • 程序不断运行,垃圾收集不可能也随着程序一直运行,那什么时候进行垃圾收集操作呢?
  • 最重要的问题是,怎么回收?

Java的垃圾收集机制是一个挺复杂的过程,涉及到的内容也很多,上面的问题一个一个解决。

1、回收区域

在前面几篇中可以知道,Java内存中的程序计数器、虚拟机栈和本地方法栈是线程私有的,线程结束也就没了。其中程序计数器负责指示下一条指令,栈中的栈帧随着方法的进入和退出不停的入栈出栈。每一个栈帧的大小在编译时就基本已经确定。所以这几个区域就不需要考虑内存回收,因为方法结束或线程停止,内存就回收了。

和上述三个区域不同的是,Java堆和方法区是线程共享的。在Java堆中存放着所有线程在运行时创建的对象,在方法区中存放着关于类的元数据信息。我们在程序运行时才能确定需要加载哪些类的元数据信息到方法区,创建哪些对象到堆中,也就是说,这部分的内存分配和回收都是动态的。也因为这样,这两个部分是垃圾收集器所关注的地方。

2、谁才是垃圾?

首先考虑一下存放对象的Java堆。

程序中创建的对象绝大多数都会在Java堆中,而程序的运行也会创建大量的对象。但这些对象并不总是使用,这样就产生了一些不会再使用的垃圾。这些垃圾占据着宝贵的内存空间,所以需要回收这些空间。不过,怎么才能确定堆中的对象是垃圾呢?

一种常见的算法是引用计数算法,它基于这样的考虑,给对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用实效时,计数器的值就减1。当计数器的值为0时,对象就不可能再被使用。

引用计数算法实现简单,判定效率也高。不过,主流的Java虚拟机中并没有使用引用计数算法来管理内存,因为这个算法很难解决对象之间相互循环引用的问题。

考虑下面的代码:

public class ReferenceCountingGC {
	public Object instance=null;
	private static final int _1mb=1024*1024;
	@SuppressWarnings("unused")
	private byte[] bigSize=new byte[2*_1mb];
	public static void testGC() {
		ReferenceCountingGC objA=new ReferenceCountingGC();
		ReferenceCountingGC objB=new ReferenceCountingGC();
		objA.instance=objB;
		objB.instance=objA;
		objA=null;
		objB=null;
		System.gc();
	}
	public static void main(String[] args) {
		ReferenceCountingGC.testGC();
	}
}

ReferenceCountingGC类中有一个实例对象,在测试代码中构造的两个对象objA和objB在instance实例对象上互相引用。这样,每个对象的引用计数都是2,当两个对象都赋值为null时,引用计数减1变为1,按照引用计数算法这不是垃圾。但是,很明显这两个对象已经不能再被访问到了,这就是垃圾。实际上,运行之后Java虚拟机将这两个对象作为垃圾回收掉了。

那么Java中使用的是什么方法呢?是可达性分析算法(Reachability Analysis)。

可达性分析算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,即从GC Roots到这个对象不可达,就说明这个对象是不可用的。如下图,左面的四个对象都有引用链到GC Roots,因此是可用的;右面的三个对象到GC Roots不可达,所以是不可用的。

Java垃圾收集机制_第1张图片

在Java中,下面几种对象可以作为GC Roots:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI(即Native方法)引用的对象;

其实这两种方法都涉及到了对象的引用,也就是说对象是否是垃圾都与引用有关,因此有必要全面的理解一下Java中的引用。

其实Java中的引用一共有四种。这是JDK 1.2 之后对引用概念的扩充,分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference),这四种引用的强度依次逐渐减弱。

(1)强引用

强引用就是程序中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾回收器就不会回收被引用的对象。

(2)软引用

软引用用来描述一些还有用但不是必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。SoftReference类来实现软引用。

(3)弱引用

弱引用也用来描述非必须的对象,但是强度比软引用还弱,被引用的对象只能存活到下一次垃圾收集之前。当下一次垃圾收集器工作时,不论内存是否足够,都会回收这些对象。WeakReference类实现了弱引用。

(4)虚引用

虚引用是最弱的一种引用,也叫幽灵引用或幻影引用。一个对象是否有虚引用存在不会对其生存时间产生影响,也无法通过虚引用来取得一个对象实例。虚引用的唯一目的就是当被虚引用关联的对象被收集器收集时收到一个系统通知。PhantomReference类实现了虚引用。

3、垃圾也有可能变废为宝

垃圾也有可能变废为宝然后再利用呢。其实,即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,要真正认为一个对象是垃圾要收集,至少要经过两次标记过程:如果对象在进行可达性分析后发现不可达,那么就将它进行第一标记并进行一次筛选,筛选的条件是这个对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,或finalize方法已经被虚拟机执行过了,虚拟机任何没有必要执行finalize方法。

如果这个对象被判定为有必要执行finalize方法,那么这个对象会放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行。不过虚拟机只是会触发这个方法,但不承诺会等待执行完毕,这是因为,如果一个对象的finalize方法执行缓慢,或发生了死循环,就会导致F-Queue对象中的其他对象处于等待,甚至整个垃圾收集系统崩溃。稍后GC会在F-Queue中的对象进行第二次小规模的标记,如果这时标记为可达,就可以不被收集;如果仍然不可达,那么就被标记为垃圾了。具体的流程图如下:

Java垃圾收集机制_第2张图片

下面的代码演示了上面所说的内容。

public class FinalizeEscapeGC {
	public static FinalizeEscapeGC SAVE_HOOK=null;
	public void isAlive(){
		System.out.println("yes,i am still alive.");
	}
	protected void finalize()throws Throwable{
		super.finalize();
		System.out.println("finalize method executed!");
		FinalizeEscapeGC.SAVE_HOOK=this;
	}
	public static void main(String[] args) throws InterruptedException {
		SAVE_HOOK=new FinalizeEscapeGC();
		SAVE_HOOK=null;
		System.gc();
		Thread.sleep(500);
		if(SAVE_HOOK!=null){
			SAVE_HOOK.isAlive();
		}else{
			System.out.println("no,i am dead.");
		}
		
		SAVE_HOOK=null;
		System.gc();
		Thread.sleep(500);
		if(SAVE_HOOK!=null){
			SAVE_HOOK.isAlive();
		}else{
			System.out.println("no,i am dead.");
		}
	}
}

结果如下:


FinalizeEscapeGC类覆盖了finalize方法,所以在GC将SAVE_HOOK第一次标记为垃圾后的筛选中认为finalize有必要执行。在覆盖的finalize方法中,将自己赋值给了类的变量SAVE_HOOK,成功拯救自己,第一次没有被收集。但是第二次虽然代码相同,但是由于虚拟机已经执行过finalize方法了,GC不认为有必要执行,在第二次标记中也标记为垃圾,所以没有能拯救自己,被当做垃圾收集了。

4、回收方法区

除了Java堆,方法区中也存在垃圾收集。只不过这里的收集效率比较低。

方法区,在HotSpot虚拟机中叫永久代,GC收集两部分内容,废弃常量和无用的类。收集废弃常量与收集Java堆中的对象类似。以常量池中字面量的收集为例,假如一个字符串“ABC”已经在常量池中,但是当前系统中没有任何一个String对象是“ABC”,即没有对象引用常量池中的“ABC”,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,“ABC”就会被清理出常量池。常量池中的其他类(接口)、方法和字段的符号引用也类似。

不过要判断一个类是否无用就麻烦很多了。要同时满足如下三个条件一个类才是无用的类:

  1. 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何的实例;
  2. 加载该类的ClassLoader已经被回收;
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问到该类的方法。

满足上面的三个条件,虚拟机就可以回收。不过,对于HotSpot虚拟机来说,是否回收通过-Xnoclassgc参数来设置。

5、垃圾收集算法

现在我们知道了在哪里收集垃圾以及如何判定一个对象是否是垃圾。接下来就要考虑如何收集垃圾,即垃圾收集算法。不过由于垃圾收集算法涉及到大量的程序细节,所以这里仅仅介绍算法的基本思想及其发展过程。

(1)标记-清除算法

标记-清除(Mark-Sweep)算法是最基础的收集算法,算法名字表明这个算法的垃圾收集过程包括两步:标记和清除。前面介绍的判定垃圾的过程就是标记过程,在标记过后的清除过程中会清理标记为垃圾的对象。后序的垃圾收集算法都是在这个算法的基础上改进而成的。这个算法有两个不足:一个就是标记和清除的效率不高;第二个是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多的话可能导致以后分配大块内存时失败的问题,这样就会触发另一次垃圾收集操作。算法的执行过程如下图:

Java垃圾收集机制_第3张图片

(2)复制算法

复制算法是为了解决标记-清除算法效率不高的问题的,它将可用内存按照容量分为大小相等的两部分,每次只使用其中的一块。当一块的内存用完了,就将还存活的对象复制到另一块,然后再把已经使用过的内存空间一次性清理掉。这样使得每次是对整个半区进行内存回收,内存分配时也不需要考虑内存碎片的问题,只要移动堆顶指针,按顺序进行分配就好。算法的执行过程如下图:

Java垃圾收集机制_第4张图片

不过这个算法使得内存只能一半能用,代价太高了。现在的虚拟机都采用这种方法来回收新生代,不过不是1:1分配的,而是将堆内存分为以块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和一个Survivor空间。当回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor中,然后清理Eden和使用过的Survivor空间。HotSpot虚拟机默认的Eden和Survivor比例是8:1,即Eden占堆的80%空间,Survivor占10%的空间,每次只能使用90%的堆空间。

不过,我们并不能保证每次回收只有不多于10%的对象存活,当Survivor空间不够时,需要使用其他内存空间(老年代)进行分配担保,即如果Survivor空间不够,存活的对象直接进入老年代。

(3)标记-整理算法

复制收集算法在对象存活率较高时就需要进行较多的复制操作,效率就会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都存活的极端情况,所以在老年代中一般不使用这种算法。

根据老年代的特点,可以使用另一种标记-整理(Mark-Compact)算法,标记过程和标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是整理存活的对象,将存活的对象都向一端移动,然后直接清理掉边界外的内存。算法的执行过程如下:

Java垃圾收集机制_第5张图片

这样,也没有了内存碎片的问题。

(4)分代收集算法

现在的虚拟机都使用“分代收集”算法,这种算法只是根据对象的存活周期的不同将内存划分为几块。一般把Java堆空间分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。在新生代,每次垃圾收集都会有大量的对象死去,只有少量存活,这样就可以选择复制算法,只需复制少量存活的对象就可以完成垃圾收集。在老年代中,对象的存活率高、没有额外的空间对它进行分配担保,就必须采用标记-清除或标记-整理算法来进行回收。

6、HotSpot的算法实现

前面从理论的角度介绍了对象存活判定和垃圾收集算法,接下来介绍下HotSpot虚拟机的实现。

(1)枚举根节点

在对象存活判定中使用的是GC Roots可达性分析算法,可作为GC Roots的节点主要在全局的引用(例如常量和类静态属性)与执行上下文(例如栈帧中的本地变量表)中,不过现在很多应用仅仅方法区就有数百兆,如果逐个检查这里的引用,就必然会消耗很多时间。

另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项工作必须在一个能确保一致性的快照中进行,就是说在整个分析过程中执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不停变化的情况,否则分析的结果就不能保证准确。这是导致GC进行时必须停顿所有Java执行线程的一个重要原因,Sun将这个事件叫做“Stop the World”。

由于目前的主流虚拟机使用的都是准确式GC,所以当执行系统停顿后,并不需要检查所有的引用和执行上下文,虚拟机有办法直接得知哪些地方存放着对象的引用。在HotSpot的实现中,使用一组OopMap的数据结构来达到这个目的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。

(2)安全点

在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots的枚举,但是,这样可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果每一条指令都生成对应的OopMap,将会消耗大量的空间。

实际上,HotSpot只是在特定的位置记录OopMap的信息,这些位置称为“安全点”(Safe Point),即程序执行时并非在所有地方都能停下来开始GC,只有到达安全点时才能停止。SafePoint的选定不能太少也不能太多,所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特点”为标准进行选定。因为每条指令执行的时间都非常短,程序不太可能因为指令流长度太长而长时间执行,长时间执行最明显的特征就是指令序列复用,比如方法调用、循环跳转和异常跳转等,具有这些特征的指令才会产生安全点。

对于SafePoint来说,还有一个问题,就是如何在GC发生时让所有的线程(不包括执行JNI调度的线程)都跑到最近的安全点停下来,这里有两种方案:抢占式中断和主动式中断。抢占式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有的线程全部中断,如果有线程中断的地方不在安全点上,就恢复线程执行到安全点。不过几乎没有虚拟机采用这种方法。

另一个方法就是主动式中断,就是说当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现为真时就自己中断挂起。轮询标志的地方和安全点重合,此外还有创建对象需要分配内存的地方。

(3)安全区域

SafePoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的SafePoint。但如果程序由于没有分配CPU时间或者线程处于sleep或blocked状态而没有执行呢?这时线程无法响应JVM的中断请求,无法跑到安全点然后挂起,JVM也不可能等着线程重新执行。这时就需要安全区域(Safe Region)了。

安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任何地方开始GC都是安全的,也就是说,安全区域是扩展了的安全点。

在线程执行到安全区域中的代码时,首先标识自己已经进入安全区域,这样,当在这段时间JVM要发起GC时,就不用管已经标识进入安全区域的线程了。在线程要离开安全区域时,要检查系统是否完成了GC Roots的枚举或者整个GC过程,如果完成了,那线程就继续执行,否则就等待可以安全离开安全区域的信号。

7、HotSpot虚拟机中的垃圾收集器

上面只是介绍了垃圾收集算法的原理,而由于HotSpot虚拟机的垃圾收集是按照分代完成的,所以虚拟机中实现了多个垃圾收集器。这里依据的是JDK 1.7之后的实现,垃圾收集器如下图所示:

Java垃圾收集机制_第6张图片

这张图体现了HotSpot虚拟机中各个垃圾收集器的关系,其中上面的三个垃圾收集器工作在新生代,下面的三个收集器工作在老年代,而G1收集器在两个部分都可以工作。收集器之间的连线表明两个收集器可以协同工作。下面来分别介绍一下各个垃圾收集器的原理、特性和使用场景。

(1)Serial收集器

Serial收集器是最基本的、历史最悠久的收集器,曾经是JDK 1.3.1之前虚拟机的新生代收集的唯一选择。Serial这个名字揭示了这是一个单线程的垃圾收集器,特点如下:

  • 仅仅使用一个线程完成垃圾收集工作;
  • 在垃圾收集时必须暂停其他所有的工作线程,知道垃圾收集结束;
  • Stop the World是在用户不可见的情况下执行的,会造成某些应用响应变慢;
  • 使用复制算法;

Serial收集器的工作流程如下图:

Java垃圾收集机制_第7张图片

虽然如此,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器。它的优点同样明显:简单而高效(单个线程相比),并且由于没有线程交互的开销,专心做垃圾收集自然课获得最高的单线程效率。在一般情况下,垃圾收集造成的停顿时间可以控制在几十毫秒甚至一百多毫秒以内,还是可以接受的。

(2)ParNew收集器

ParNew收集器其实是Serial收集器的多线程版本,与Serial不同的地方就是在垃圾收集过程中使用多个线程,剩下的所有行为包括控制参数、收集算法、Stop the World、对象分配规则和回收策略等都一样。ParNew收集器也使用复制算法。ParNew收集器的工作流程如下图:

Java垃圾收集机制_第8张图片

ParNew收集器看似没有多大的创新之处,但却是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为,除了Serial收集器外,目前只有ParNew收集器能够与CMS收集器配合工作,而CMS收集器是HotSpot在JDK 1.5时期推出的具有划时代意义的垃圾收集器(后面会介绍到)。

ParNew收集器在单个线程的情况下由于线程交互的开销没有Serial收集器的效果好。不过,随着CPU个数的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同。可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

(3)Parallel Scavenge收集器

Parallel Scavenge收集器和ParNew类似,是一个新生代收集器,使用复制算法,又是并行的多项成收集器。不过和ParNew不同的是,Parallel Scavenge收集器的关注点不同。

CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的则是达到一个可控制的吞吐量。吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)。如果虚拟机一共运行100分钟,垃圾收集运行了1分钟,那么吞吐量就是99%。

停顿时间越短就越适合与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供了两个参数来精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能在给定时间内完成垃圾收集。不过垃圾收集时间的缩短是以牺牲吞吐量和新生代空间为代价的,短的垃圾收集时间会导致更加频繁的垃圾收集行为,从而导致吞吐量的降低。

GCTimeRatio参数的值是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。如果设置为19,那允许的最大GC时间就是总时间的5%(1/(1+19))。默认是99,也就是允许最大1%的垃圾收集时间。

Parallel Scavenge收集器也叫吞吐量优先收集器,它还有一个参数-XX:UseAdaptiveSizePolicy,这是一个开关参数,当这个参数打开后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或最大的吞吐量,这叫GC自适应的调节策略。这也是Parallel Scavenge收集器和ParNew收集器的一个重要区别。

(4)Serial Old收集器

Serial Old是Serial的老年版本,在Serial的工作流程图中可以看到,Serial Old收集器也是一个单线程收集器,使用“标记-整理”算法。这个收集器主要给Client模式下的虚拟机使用。如果在Serve模式下,它有两个用途:一个是在JDK 1.5之前的版本中与Parallel Scavenge收集器搭配使用;另一个就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。这个收集器的工作流程在Serial的后半部分有所体现。

(5)Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年版本,它也使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6开始提供。

在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器的组合。Parallel Old收集器的工作流程如下:

Java垃圾收集机制_第9张图片

(6)CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。在重视响应速度和用户体验的应用中,CMS应用很多。

CMS收集器使用“标记-清除”算法,运作过程比较复杂,分为4个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS Concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS Concurrent Sweep)

其中,初始标记和并发标记仍然需要Stop the World、初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC RootsTracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段长,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以整体上说,CMS收集器的内存回收过程是与用户线程一共并发执行的。下图是流程图:

Java垃圾收集机制_第10张图片

CMS的优点就是并发收集、低停顿,是一款优秀的收集器。不过,CMS也有缺点,如下:

  • CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程数是(CPU数量+3)/4,当CPU个数大于4时,垃圾收集线程使用不少于25%的CPU资源,当CPU个数不足时,CMS对用户程序的影响很大;
  • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC;
  • CMS使用标记-清除算法,会产生内存碎片;

(7)G1收集器

G1(Garbage first)收集器是最先进的收集器之一,是面向服务端的垃圾收集器。与其他收集器相比,G1收集器有如下优点:

  • 并行与并发:有些收集器需要停顿的过程G1仍然可以通过并发的方式让用户程序继续执行;
  • 分代收集:可以不使用其他收集器配合管理整个Java堆;
  • 空间整合:使用标记-整理算法,不产生内存碎片;
  • 可预测的停顿:G1除了降低停顿外,还能建立可预测的停顿时间模型;

G1中也有分代的概念,不过使用G1收集器时,Java堆的内存布局与其他收集器有很大的差别,它将整个Java堆划分为多个大小相等的独立区域(Region),G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次优先收集价值最大的那个Region。这样就保证了在有限的时间内尽可能提高效率。

G1收集器的大致步骤如下:

  • 初始标记(Initial mark)
  • 并发标记(Concurrent mark)
  • 最终标记(Final mark)
  • 筛选回收(Live Data Counting and Evacuation)

收集器的流程如下图:

Java垃圾收集机制_第11张图片

添加公众号Machairodus,我会不时分享一些平时学到的东西~

Java垃圾收集机制_第12张图片

你可能感兴趣的:(Java,Java虚拟机)