一、垃圾收集算法
(1)标记-清除算法:最基础的收集算法“标记--清除”(Mark-sweep)算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,对象的标记过程是采用“可达性分析算法”来进行的。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而来的。
主要缺点:a、效率问题,标记和清除两个过程的效率都不高。
b、空间问题,标记清除后会产生大量的不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续空间而不得不提前触发另一次垃圾收集活动。执行过程如下图:
(2)复制算法:为了解决效率问题,一种称为“复制”的收集算法出现了,他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,当然再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这个算法的代价是将内存缩小为了原来的一半,未免太高了点。如下图:
(3)标记--整理算法:复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另一种“标记--整理”(Mark--Compact)算法,标记过程仍然与“标记--清除”算法一样,但后续步骤不是直接对回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存,“标记--整理”算法的示意图如下:
(4)分代收集算法:当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法只是根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记--清理”或者“标记--整理”算法来进行回收。
二、HotSpot的垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器则是内存回收的具体实现,JDK1.7 update14之后的Hotspot虚拟机的所有收集器如下如:
(1)Serial收集器:他是最基本、发展历史最悠久的收集器,这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明他只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。如下图:
(2)ParNew收集器:他是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop the world、相当多的代码、回收策略等都与Serial收集器完全一样。如下图:
注意:
a:并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
b:并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行哎另一个CPU上。
(3)Parallel Scavenge收集器:是一个新生代收集器,他也是使用复制算法的收集器,又是并行的多线程收集器。看上去和ParNew都一样,但他的特点是他的关注点与其他收集器不同,CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
(4)Serial Old收集器:它是Serial收集器的老年代版,它同样是一个单线程收集器,使用“标记--整理”算法。这个收集器的意义在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要有两大用途:一种是在jdk1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后预案,在并发收集发生Concurrent Mode Failure时使用。工作流程图如下:
(5)Parallel Old 收集器:是Parallel Scavenge收集器的老年代版,使用多线程与“标记--整理”算法。这个收集器在jdk1.6中才开始提供的,直到Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加 Parallel Old收集器。工作过程如下图:
(6)CMS收集器(Concurrent Mark Sweep):是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
CMS收集器是基于“标记--清除”算法实现的, 他的运作过程相对于前几种收集器来说更复杂一些,整个过程分为4个步骤:
a、初始标记(CMS inital mark):需要“stop the world”,但只标记一下GC Roots能直接关联的对象,速度很快。
b、并发标记(CMS concurrent mark):是GC Roots Tracing的过程,花费时间长
c、重新标记(CMS remark):是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
d、并发清除(CMS concurrent sweep):是并发清除无用对象。
CMS收集器的运作步骤如下图:
(7)G1收集器(Garbage-First):是当今收集器技术发展的最前沿的成果之一,G1是一款面向服务器端应用的垃圾收集器。与其他GC收集器相比,G1具备如下特点:
a、并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
b、分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能够独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
c、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记--整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。这个特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前出发下一次GC。
d、可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时java(RTSJ)的垃圾收集器的特性了。
使用G1收集器时,java堆的内存布局就与其他收集器有很大差别,它将真个java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代与老年代的概念,但新生代与老年代不再试物理隔离的了,他们都是一部分Region(不需要连续)的集合。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下几个步骤:
a、初始标记(Initial Marking)
b、并发标记(Concurrent Marking)
c、最终标记(Final Marking)
d、筛选回收(Live Data Counting and Evacuation)
G1收集器运行示意图如下:
三、理解GC日志
通过以下两段典型的GC日志进行分析:
33.125:[GC [DefNew:3324K->152K(3712K),0.0025925secs] 3324K->152K(11904K),0.0031680 secs]
100.667:[FullGC [Tenured:0K->210K(10240K),0.0149142secs] 4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs]
[Times:user=0.01 sys=0.00,real=0.02 secs]
(1)最前面的数字“33.125:”和“100.667:”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。
(2)GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的,例如下面这段新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。如果是调用System.gc()方法所触发的收集,那么在这里将显示“[Full GC(System)”。
[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]
(3)接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。GC发生区域日志与GC收集器对照列表如下:
GC日志区域名 |
对应GC收集器名 |
[DefNew (Default New Generation) |
Serial收集器 |
[ParNew (Parallel New Generation) |
ParNew收集器 |
[PSYoungGen |
Parallel Scavenge收集器 |
[ParOldGen |
Parallel Old收集器 |
(4)后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”。
而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”。
(5)再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。
有的收集器会给出更具体的时间数据,如“[Times:user=0.01 sys=0.00,real=0.02 secs]”,这里面的user、sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。
CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以读者看到user或sys时间超过real时间是完全正常的。
四、GC收集器参数
注意,这里的参数分类不是指只能在指定收集器类型配置。
1、 Serial串行收集器相关的参数
-XX:+UseSerialGC
|
虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收
|
-XX:SurvivorRatio
|
新生代中设置eden区大小和survivior区大小的比例。默认为8,代表Eden:Survivor =8:1
|
-XX:PretenureSizeThreshold
|
设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配。
|
-XX:MaxTenuringThreshold
|
晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄增加1,当超过这个参数值时就进入老年代
|
注意:当GC发生在新生代时,称为Minor GC,次收集;当GC发生在年老代时,称为Major GC,主收集。 一般的,Minor GC的发生频率要比Major GC高很多。
2、 ParNew并行收集器相关的参数
-XX:+UseParNewGC
|
打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收
|
-XX:+UseParallelGC
|
虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old(PS Mark Sweep)的收集器组合进行内存回收
|
-XX:+UseParallelOldGC
|
打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收
|
-XX:ParallelGCThreads
|
设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等,但在CPU数量比较多的情况下,设置相·对较小的数值也是合理的。
|
-XX:MaxGCPauseMills
|
设置最大垃圾收集停顿时间。它的值是一个大于0的正数。收集器在工作时,会调整java堆大小或者其他一些参数,尽可能地把停顿时间控制在MaxGCPauseMills以内。
仅在Parallel Scavenge收集器时生效 |
-XX:GCTimeRatio
|
设置吞吐量大小,即GC时间占总时间的比率。它的值是一个0到100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。默认值为99,即允许1%的GC时间,仅在Parallel Scavenge收集器时生效
|
-XX:+UseAdaptiveSizePolicy
|
打开自适应GC策略。在这种模式下,新生代的大小、eden和survivior的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
|
-XX:HandlePromotionFailure |
是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
3、 CMS 收集器相关的参数
-XX:+UseConcMarkSweepGC
|
打开此开关后,使用ParNew + CMS —— Serial Old的收集器组合进行内存回收。Serial Old收集器是CMS收集器出现Concurrent Mode Failure失败后的备用收集器
|
-XX:ParallelCMSThreads
|
设定CMS的线程数量。
|
-XX:CMSInitiatingOccupancyFraction
|
设置CMS收集器在老年代空间被使用多少后触发,默认为68%。
仅在CMS收集器时生效
|
-XX:+UseCMSCompactAtFullCollection
|
设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。
仅在CMS收集器时生效 |
-XX:CMSFullGCsBeforeCompaction
|
设定进行多少次CMS垃圾回收后,进行一次内存压缩(碎片整理)。
仅在CMS收集器时生效 |
-XX:+CMSClassUnloadingEnabled
|
允许对类元数据区进行回收。
|
-XX:CMSInitiatingPermOccupancyFraction
|
当永久区占用率达到这一百分比时,启动CMS回收(前提是-XX:+CMSClassUnloadingEnabled激活了)。
|
-XX:CMSInitiatingPermOccupancyOnly
|
表示只在到达阈值的时候才进行CMS回收。
|
-XX:+CMSIncrementalMode
|
使用增量模式,比较适合单CPU。增量模式在JDK8中标记为废弃,并且将在JDK9中彻底移除。
|
4、 G1收集器相关的参数
-XX:+UseG1GC
|
使用G1回收器。
|
-XX:MaxGCPauseMillis
|
设置最大垃圾收集停顿时间。
|
-XX:GCPauseIntervalMillis
|
设置停顿间隔时间。
|