Java虚拟机札记-垃圾回收与内存分配

目前内存分配和垃圾回收已经实现了“自动化”,为什么我们还要关注它们呢?当排查各种内存溢出、内存泄露问题时,当垃圾回收成为提高系统并发量的瓶颈时,有必要对内存的动态分配和垃圾回收进行监控和调节。GC需要考虑三个问题:哪些内存需要回收?什么时候回收?如何回收?

哪些内存需要回收?

Java虚拟机札记-Java内存区域划分一文中,我们已经学习了Java运行时内存区域的各个部分。其中,程序计数器、Java虚拟机栈和本地方法栈是“线程私有”的,这类内存区域会随着用户线程的启动和结束而建立和销毁,因此这些区域不需要过多考虑内存分配和回收的问题。 Java堆和方法区是“线程共享”的,随着虚拟机的启动而存在,这部分内存的分配和回收是动态的,我们只有在程序运行时才知道创建了哪些对象,需要多少内存,GC针对的就是这部分内存。

什么时候回收?判活算法

垃圾回收器对堆内存进行回收之前,需要判断堆中对象是“活着”还是“死去”。有两种方法可以实现这个功能:引用计数法和可达性分析算法。

引用计数法

定义:给对象添加一个引用计数器,每当有一个地方引用它,计数器就+1,;当引用失效时,计数器就-1;任何时刻计数器都为0的对象就是不能再被使用的。
优点:实现简单,效率很高
缺点:很难解决对象之间循环引用的问题。
由于它的缺点很严重,主流的程序语言都没有使用它判定该对象是否死亡。

可达性分析算法

定义:通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
优点:准确高效
缺点:实现较麻烦
在java中可以作为GC Roots的对象有以下几种:

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

无论是引用计数法还是可达性分析算法,判断是否存活都与“引用”有关。在JDK1.2以前,一个对象只有“被引用”或者“没有被引用”两种状态。它无法描述这样一类对象:当内存空间充足时,可以保存在内存中;如果内存空间在垃圾回收后依然很紧张,则可以抛弃这些对象。所以在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为以下四种引用。强度大小为强引用>软引用>弱引用>虚引用

  • 强引用(Strong Reference)
  • 软引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虚引用(Phantom Reference)
强引用
Object obj =  new  Object();

上述的引用就是强引用。只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。

软引用

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

弱引用

描述非必须的对象,强度比软引用更弱一些,被弱引用关联的对象,只能生存到下一次垃圾收集发生前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。JDK 1.2 之后,提供了 WeakReference 类来实现弱引用。

虚引用

也称幽灵引用或者幻影引用,是最弱的一种引用。一个对象是否有虚引用,完全不会对其生存时间够成影响,也无法通过虚引用来取得一个对象实例。为一个对象关联虚引用的唯一目的就是希望在这个对象被收集器回收时,收到一个系统通知。JDK 1.2 之后,提供了 PhantomReference 类来实现虚引用。

如何回收? 垃圾回收算法
  • 标记-清除算法(mark-sweep)
  • 复制算法 (copying)
  • 标记-整理算法 (mark-compact)
  • 分代收集算法(Generational Collection)
标记-清除算法

定义:分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。
地位:是最基础的垃圾回收算法,后面的几种垃圾回收算法都是基于这种思路并对其不足进行改进而得到的。
缺点:

  • 效率问题。标记和清除过程的效率都不高。
  • 空间问题。标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致,程序分配较大对象时无法找到足够的连续内存,不得不提前出发另一次垃圾收集动作。
    MarkdownPhotos/master/CSDNBlogs/JVM/mark-sweep.png
复制算法

定义:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。
优点:解决了标记-清除算法的效率问题。复制算法使得每次都是针对其中的可用内存的半区进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:

  • 浪费空间。将内存缩小为原来的一半,代价太高昂。
  • 效率问题。在对象存活率较高时,需要执行较多的复制操作,效率会变低。
    应用:回收新生代
    现在的商用虚拟机都采用这种算法来回收新生代。因为新生代中的对象大部分很快就死亡,所以并不需要按照1:1的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间。每次使用 Eden 和其中的一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性拷贝到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。Hotspot 虚拟机默认 Eden 和 Survivor 的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80% + 10%),只有10%的内存是会被“浪费”的。当然,无法保证每次回收都只有不多于10%的对象存活,如果10%的Survivor 空间不够用,可以临时使用老年代的内存。
    MarkdownPhotos/master/CSDNBlogs/JVM/copying.png
标记-整理算法

定义:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。
优点:解决了复制算法的空间和效率问题。
应用:现在的商用虚拟机都采用这种算法来回收老年代
MarkdownPhotos/master/CSDNBlogs/JVM/mark-compact.png

分代收集算法

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

Java堆分区

下面来详细介绍下堆内存的垃圾回收。

为什么要将堆内存分区?
分区是为了对堆中对象进行模块化,以提高 JVM 的执行效率。

堆内存分为哪几块?

  • 新生代(Young Generation Space)
  • 老年代(Tenure Generation Space)

在Java8中,HotSpot中的永久代(Permanent Space)被元空间(Metaspace)取代。

新生代

主要用来存储新创建的对象,内存较小,MinorGC频繁。新生代又分为三个区域:Eden、ServivorFrom、ServivorTo。
Eden:当对象在堆创建时,一般进入Eden。如果新创建的对象占用内存很大,则直接分配到老年代。当Eden区内存不够时会触发MinorGC,对新生代区进行一次垃圾回收。
ServivorFrom:上一次GC的幸存者,作为这一次MinorGC的被扫描者。
ServivorTo:保留了这一次MinorGC后的幸存者。
MinorGC:采用复制算法。

  1. 扫描Eden和ServivorFrom,将存活的对象复制到ServivorTo,并将这些对象的年龄+1。(如果ServivorTo已经满,则复制到老年代。)
  2. 扫描ServivorFrom时,如果对象已经经过了几次的扫描仍然存活,达到了老年代的标准,JVM会将其移到老年代。
  3. 扫描完毕后,清空Eden和ServivorFrom,然后交换ServivorFrom和ServivorTo,即ServicorTo成为下一次GC时的ServicorFrom区。
老年代

主要用来存储长时间被引用的对象。它里面存放的是经过几次在新生代进行扫描仍存活的对象。因为老年代对象比较稳定,所以MajorGC频率较小。
MajorGC:采用标记-整理算法。

永久代

主要用来存储类元数据信息,如类定义、字节码和常量等。GC不会在主程序运行期对永久代进行清理。所以这也导致了永久代会随着加载的Class的增多而胀满,最终抛出OOM异常。在Java8中,永久代已被元空间取代。再启动JVM时,如果JVM设置了PermSize 和 MaxPermSize 两个参数,参数会被忽略并给出警告。

元空间

元空间并不在虚拟机中,而是使用本地内存来存储类元数据信息。下面是两个与元空间相关的参数

  • XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。
MarkdownPhotos/master/CSDNBlogs/JVM/GCM.png
图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。接下来介绍这些收集器的特性、基本原理和使用场景,并重点分析CMS和G1这两种收集器。还有一点要强调,没有最好的收集器,只有对具体场景最合适的收集器

Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。
MarkdownPhotos/master/CSDNBlogs/JVM/SerialAndSerial-Old.png
特性

  • 针对新生代;
  • 采用复制算法;
  • 单线程收集:这个收集器是一个单线程的收集器。“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

应用场景
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。

优势
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

参数
-XX:+UseSerialGC。Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收。

ParNew收集器

ParNew垃圾收集器是Serial收集器的多线程版本。
MarkdownPhotos/master/CSDNBlogs/JVM/ParNewAndSerialOld.png

特性
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。

应用场景
1.ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。
除了Serial收集器外,目前只有它能与CMS收集器配合工作。
在JDK 1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。

优势

  • 多线程
    -除了Serial收集器外,目前只有它能与CMS收集器配合工作。

参数

  • -XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器
  • -XX:+UseParNewGC:强制指定使用ParNew
  • -XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同

Serial收集器与ParNew收集器
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。

Parallel Scavenge收集器

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。
MarkdownPhotos/master/CSDNBlogs/JVM/ParallelScavengcOrOld.png
特性

  • 新生代收集器;
  • 采用复制算法;
  • 多线程收集;

看上去和ParNew收集器一样,它有什么特别之处呢?
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

应用场景
高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务

优势
参考特性和应用场景。

参数

  • -XX:+UseParallelGC:Jvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收;
  • -XX:+UseParallelOldGC:使用Parallel Scavenge + Parallel Old的收集器组合进行回收;

Parallel Scavenge收集器提供两个参数用于精确控制吞吐量:

  • -XX:MaxGCPauseMillis。用于控制最大垃圾收集停顿时间。
  • -XX:GCTimeRatio。设置吞吐量大小
  • -XX:+UseAdaptiveSizePolicy。开启自适应调节策略。

GC自适应的调节策略
Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
如果对垃圾收集器不太了解,手动优化困难,这是一种值得推荐的方式。

  1. 只需要把基本的内存数据设置好(如-Xmx设置最大堆);
  2. 然后使用-XX:MaxGCPauseMillis或-XX:GCTimeRatio给JVM设置一个优化目标;

那些具体细节参数的调节就由JVM自适应完成。

Parallel Scavenge收集器与CMS等收集器:
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。

Parallel Scavenge收集器与ParNew收集器
Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。

Serial Old收集器

Serial Old是 Serial收集器的老年代版本。
MarkdownPhotos/master/CSDNBlogs/JVM/SerialAndSerial-Old.png
特性

  • 针对老年代;
  • 采用"标记-整理"算法;
  • 单线程收集;

应用场景

  • Client模式。主要用于Client模式。
  • Server模式。Server模式有两大用途:
    • 在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用
    • 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

优势
嗯。。。待补充

参数
-XX:+UseSerialGC。Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收。

Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本,JDK1.6中才开始提供。
MarkdownPhotos/master/CSDNBlogs/JVM/ParallelScavengcOrOld.png
特性

  • 老年代收集器;
  • 采用复制算法;
  • 多线程收集;

应用场景
注重吞吐量以及CPU资源敏感的场景,可以使用Parallel Scavenge加Parallel Old收集器的组合。

优势
参考应用场景。

参数
-XX:+UseParallelOldGC。使用Parallel Scavenge + Parallel Old的收集器组合进行回收

JDK1.6之前Parallel Scavenge的尴尬
JDK1.6之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器外别无选择(Parallel Scavenge收集器无法与CMS收集器配合工作)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器。

运作过程

  • 初始标记(CMS initial mark)
    • 需要"Stop The World";
    • 仅标记一下GC Roots能直接关联到的对象,速度很快;
  • 并发标记(CMS concurrent mark)
    • 进行GC Roots Tracing;
    • 刚才产生的集合中标记出存活对象,并不能保证可以标记出所有的存活对象;
    • 和用户线程一起并发执行;
  • 重新标记(CMS remark)
    • 需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
    • 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
    • 采用多线程并行执行来提升效率;
  • 并发清除(CMS concurrent sweep)
    • 回收所有的垃圾对象;
    • 和用户线程一起并发执行;

MarkdownPhotos/master/CSDNBlogs/JVM/CMS.png
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作,所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行。

特性

  • 老年代收集器;
  • 基于"标记-清除"算法;
  • 以获取最短回收停顿时间为目标;
  • 并发收集;
  • 低停顿;
  • 需要更多的内存;

应用场景
尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验的服务端应用

优势

  • 并发收集;
  • 低停顿;

劣势

  • CMS收集器对CPU资源非常敏感
    其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低
    CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。
    针对这种情况,曾出现了"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS)。类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间。但效果并不理想,JDK1.6后就官方不再提倡用户使用。

  • CMS收集器无法处理浮动垃圾
    CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
    浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
    CMS需要更多的内存:也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败。
    Concurrent Mode Failure解决方案:出现“Concurrent Mode Failure”后,虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

  • CMS收集器会产生大量空间碎片
    问题原因:CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
    解决方法:
    1.“-XX:+UseCMSCompactAtFullCollection”,默认开启,使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程。但合并整理过程无法并发,停顿时间会变长
    2."-XX:+CMSFullGCsBeforeCompaction"设置执行多少次不压缩的Full GC后,来一次压缩整理, 为减少合并整理过程的停顿时间。默认为0,也就是说每次执行Full GC都会进行压缩整理。

参数

  • -XX:+UseConcMarkSweepGC:使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。
  • -XX:CMSInitiatingOccupancyFraction:CMS收集器的启动阀值。当老年代使用了超过阀值,CMS就会启动。不能设置地太高,否则可能导致大量“Concurrent Mode Failure”失败。
G1收集器

G1(Garbage-First)是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK 1.5中发布的CMS收集器。

与其他GC收集器相比,G1具备如下特点。
特性

  • 并行与并发。
    • G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间。
    • 部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集
    • 与其他收集器一样,分代概念在G1中依然得以保留。
    • 虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合
    • 与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的
    • 这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿
    • 这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点。但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

Java堆内存布局的改变
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

G1可预测的停顿的实现原理
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

运作过程
不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。

  1. 初始标记(Initial Marking)
    • 仅标记一下GC Roots能直接关联到的对象;
    • 且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;
    • 需要"Stop The World",但耗时很短;
  2. 并发标记(Concurrent Marking)
    • 从GC Root开始对堆中对象进行可达性分析,找出存活的对象;
    • 这阶段耗时较长,但可与用户程序并发执行。
  3. 最终标记(Final Marking)
    • 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
    • 虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中;
    • 需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
    • 采用多线程并行执行来提升效率;
  4. 筛选回收(Live Data Counting and Evacuation)
    • 首先对各个Region的回收价值和成本进行排序;
    • 然后根据用户期望的GC停顿时间来制定回收计划;
    • 最后按计划回收一些价值高的Region中垃圾对象,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率;
    • 可以并发进行,降低停顿时间,并增加吞吐量;

G1收集器运行示意图如下:
MarkdownPhotos/master/CSDNBlogs/JVM/G1.png

应用场景
和CMS相同。尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验的服务端应用

CMS和G1该如何选择呢?
如果现在采用的收集器没有出现问题,不用急着去选择G1。如果应用程序追求低停顿,可以尝试选择G1。

优势

  • 并发收集;
  • 低停顿;
  • 分代收集
  • 空间整合
  • 除了追求低停顿外,还能建立可预测的停顿时间模型

参数

  • -XX:+UseG1GC:指定使用G1收集器;
    -XX:InitiatingHeapOccupancyPercent:当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
    -XX:MaxGCPauseMillis:为G1设置暂停时间目标,默认值为200毫秒;
    -XX:G1HeapRegionSize:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个;
垃圾收集器比较

| 垃圾收集器 | 类型 | 算法 |优点 | 缺点 | 使用场景 | 参数 |
| :-------- | :----- |:----- |:----- |:----- |:----- |
| Serial | 新生代 | 复制算法 | 没有线程交互(切换)开销 |Stop The World | HotSpot在Client模式下默认的新生代收集器 | -XX:+UseSerialGC |
| ParNew| 新生代、并行 | 同上 | 多线程+使用场景 |有线程交互(切换)开销 | 除Serial外,目前只有它能与CMS收集器配合工作 |-XX:+UseConcMarkSweepGC、-XX:+UseParNewGC |
| Parallel Scavenge | 新生代、并行 | 同上 |参考使用场景 | ? | 主要适合在后台运算而不需要太多交互的任务。 | - -XX:+UseParallelGC、-XX:+UseParallelOldGC|
| Serial Old | 老年代 | 标记-整理算法 | ?| ? | 主要用于Client模式。 Server模式有两大用途:在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用;作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。| -XX:+UseSerialGC |
| Parallel Old | 老年代、并行 | 标记-整理算法 | ? | ? | 注重吞吐量以及CPU资源敏感的场景,可以使用
Parallel Scavenge加Parallel Old
收集器的组合。| -XX:+UseParallelOldGC |
| CMS | 老年代、并发 | 标记-清除算法 | 并发收集、低停顿 | 1.对CPU资源非常敏感;2.无法处理浮动垃圾;3.会产生大量空间碎片 | 尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验的服务端应用 | -XX:+UseConcMarkSweepGC |
| G1 | 整堆收集器、并发 | 从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法; | 并发收集;低停顿;分代收集;空间整合; 除了追求低停顿外,还能建立可预测的停顿时间模型 | ? | 和CMS相同。 | -XX:+UseG1GC |

垃圾收集器参数总结
  • -XX:+<option> 启用选项
  • -XX:-<option> 不启用选项
  • -XX:<option>=<number>
  • -XX:<option>=<string>
参数描述
-XX:+UseSerialGCJvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收
-XX:+UseParNewGC打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收
-XX:+UseConcMarkSweepGC使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。
-XX:+UseParallelGCJvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收
-XX:+UseParallelOldGC使用Parallel Scavenge + Parallel Old的收集器组合进行回收
-XX:SurvivorRatio新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1
-XX:PretenureSizeThreshold直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
-XX:MaxTenuringThreshold晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代
-XX:UseAdaptiveSizePolicy动态调整java堆中各个区域的大小以及进入老年代的年龄
-XX:+HandlePromotionFailure是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留
-XX:ParallelGCThreads设置并行GC进行内存回收的线程数
-XX:GCTimeRatioGC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效
-XX:MaxGCPauseMillis设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效
-XX:CMSInitiatingOccupancyFraction设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSCompactAtFullCollection由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效
-XX:+CMSFullGCBeforeCompaction设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用
-XX:+UseG1GC指定使用G1收集器
-XX:InitiatingHeapOccupancyPercent当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45
-XX:MaxGCPauseMillis为G1设置暂停时间目标,默认值为200毫秒
-XX:G1HeapRegionSize设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个
内存分配策略

前面已经介绍了垃圾回收,了解了内存回收策略,接下来介绍下内存分配的策略。
内存分配有哪些原则?

  • 对象优先分配在 Eden
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
  • 动态对象年龄判定
  • 空间分配担保

本文已收录于Java并发编程札记专栏
本文内容摘录或总结自《深入理解Java虚拟机 JVM高级特性与最佳实践》。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值