.3
垃圾回收算法
标记-清除算法(Mark-Sweep)
这是最基础的收集算法,分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记过程完成后统一回收。标记过程详见上篇博文。
缺点
- 1、效率问题:标记和清除的效率不高
- 2、空间问题: 标记清除后会产生大量不连续的内存碎片。
复制算法
为了解决效率问题而产生,它将可用内存按容量划分成为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样每次都是对整个半区进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。
现在的商业虚拟机都用这种算大回收新生代。HotSpot虚拟机中默认Eden 和Survivor From 、Survivor To的大小比例为 8:1:1.
标记-整理算法(Mark-Compact)
标记过程与标记-清除算法一样,然后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
分代收集算法
当前商业虚拟机的垃圾收集都采用分代收集(Generational Collection)算法,根据对象的存活周期不同将内存划分为几块。一般是把堆划分为新生代和老年代,这样样就可以根据各个年代的特点选择适当的收集算法。新生代,每次垃圾收集时都有大批对象死去,只有少量存活则采用复制算法。老年代中对象存活率高,则采用标记清理或者标记整理算法来进行回收。
HotSpot算法实现
- 枚举根节点
从可达性分析中从GCRoots节点中查找引用链为例,由于可作为GCRoots的节点很多,存放的内存区域也很大,如果需要逐个检查必将耗费大量时间,并且在整个分析过程中不可以出现对象引用关系还在变化的情况。这也是导致GC进行时必须停顿所有Java执行线程(Stop The World)的一个重要原因,即使是CMS收集器在枚举根节点是停顿也是必须的。目前的主流Java虚拟机使用的都是准确是GC,因此在停顿是不需要全部检查执行上下文和全局的引用位置而是通过一组称为OopMap的 数据结构存放对象引用的位置。在类加载完成的时候,Hotspot就把对象内什么偏移量是什么类型的数据计算出来,在JIT编译过程中也会在特定的位置记录下栈和寄存器中哪些位置是引用。 - 安全点(SafePoint)
生成OopMap的特定位置称之为安全点。也是程序执行停顿下来开始GC的位置,安全点的选取基本上是以程序是否具有让程序长时间执行的特征为标准选定的,长时间执行最明显的特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
- 抢断式中断(Preemptive Suspension)
在GC发生时,首先停止所有线程,如果线程中断的地方不在安全点,则恢复线程让其跑到安全点 - 主动式中断(Voluntary Suspension)
当GC需要中断线程时,不直接对线程进行操作,仅仅设置一个标志,各个线程执行时主动去轮训这个标志,发现中断标志位真时就自己中断挂起。轮训标志的地方和安全点是重合的,另外加上创建对象需要分配内存的地方。
- 安全区域
安全区域是指在一段代码片段之中,引用关系不会发生变化,这这个区域中的任意地方开始GC都是安全的,可以看做是被扩展了的Safepoint。线程执行到Safe Region中的代码时,首先标识自己已经进入了安全区域,在这段时间里JVM发起GC时就可以不管已经标识自己为Safe Regio状态的线程。当线程要离开安全区域时,它要检查系统是否已经完成了根节点枚举(或者整个GC过程),若是完成了则继续执行,否则将必须等待直到可以安全离开Safe Region的信号为止。
垃圾收集器
JDK1.7 Update 14后的垃圾收集器如下图(连线代表可以搭配使用)
- 并行(Parallel):多条垃圾收集线程并行工作,但用户线程仍处于等待状态。
- 并发(Concurrent):用户线程与垃圾收集线程并行工作,但不一定是并行的,可能是交替执行,用户程序在继续运行而垃圾收集运行在另外一个CPU上。
Serial 收集器
最基本的收集器,单线程的收集器,这里的单线程不仅仅只只会使用一个CPU或者一条收集线程去完成垃圾收集,更重要的是它在进行垃圾收集时,必须暂停其他工作线程,知道它收集结束。
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有参数(例如 --XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlerPromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial 收集器完全一致。
Parallel Scanvenge 收集器
新生代并行的多线程收集器,使用复制算法。关注达到一个可控的吞吐量(Throughout),所谓的吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码/(运行用户代码时间+垃圾收集时间)。停顿时间短适合用户交互程序,高吞吐则则可以高效利用CPU时间,适合后台运算类任务。
Parallel Scanvenge提供了两个参数用于精确控制吞吐量
- -XX:MaxGCPauseMilis 控制最大垃圾收集停顿时间
- -XX:GCTimeRatio 直接设置吞吐量。参数范围1~99的整数 也就是垃圾时间占总总时间的比率,也就是吞吐量的倒数。如果把此参数设置为19,那么允许的最大垃圾收集时间为 1/(1+19) = 20%
另外还有一个参数-XX:+UserAdaptiveSizePolicy 这是一个开关参数,开启后可以不用手动动指定新生代的比例,晋升老年代对象大小等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息动态调整。这种调节方式称之为GC自适应的调节策略(GC Ergonimics)。
Serial Old 收集器
Serial 收集器的老年代版本
Parallel Old 收集器
Parallel Old Scanvenge 收集器的老年代版本
CMS收集器(Concurrent Mark Sweep)
以最短回收停顿时间为目标的收集器。整个过程分为四个步骤
- 初始标记 CMS innitial mart (STW)
进行GC Root Tracing的过程 - 并发标记 CMS concurrent mark
- 重新标记 CMS remark (STW)
修正并发标记期间饮用户程序继续运行而导致标记产生变动的那一部分对象的标记记录 - 并发清除 CMS concurrent sweep
缺点
- CPU资源敏感,CMS默认启动线程数是(CPU数量+3)/4也就是在 4个CPU以上时,垃圾回收线程占用不少于25%的CPU资源。如果在4个CPU下的对应用程序影响较大。
- CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure。浮动垃圾指的是在并发清理阶段产生的,由于出现在标记过程之后,于是需要在下一次GC再清理。由于CMS收集器是并发清理,还要预留一部分内存空间给用户线程使用,因此不能想其他收集器那样等老年代几乎被填满之后再进行收集。如果老年代增长的不是很快,可以通过参数
-XX:CMSInitiatingOccupancyFranction 此参数在JDK1.6中提升至92%。但如果CMS运行期间预留的内存无法满足程序要求,则会出现一次 Concurrent Mode Failure 失败,并临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就会很长了,因此-XX:CMSInitiatingOccupancyFranction 也不宜设置的太大。 - 空间碎片过多。由于CMS是基于标记-清除算法实现的收集器,因此在垃圾收集结束时很有可能出现大量空间碎片产生,空间碎片过多时会给大对像分配带来很大麻烦,即使内存剩余空间很大但是无法找到连续空间进行分配,从而提前出发FullGC。
可以通过参数 -XX:+UseCMSCompactAtFullCollection(默认开启),用于在即将FullGC之前开启内存碎片的整理合并过程,内存整理的过程无法并发,会导致停顿时间变长。
另外还可以通过参数 -XX:CMSFullGCBeforeCompact 控制设置执行多少次不压缩的FullGC 后跟着来一次带压缩的。默认值为0,标识每次FullGC 时都进行碎片整理。
G1收集器(Garbage First)
1.7后的全新垃圾收集器,面向服务端应用的垃圾收集器。相比较与其他收集器有如下特点
- 并发与并行: 充分利多CPU、多核环境下的优势,降低STW的停顿时间,部分收集器原本需要停顿Java线程来进行GC动作,G1收集器仍可以通过并发的方式让Java程序继续运行。
- 分代收集:G1能够独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的对象以获取更好的收集效果
- 空间整理:G1整体上看是基于标记-整理算法实现的,从局部(两个Region之间)上看来是基于复制算法实现的,但无论如何这两种算法都不会有内存碎片产生,因此G1运作期间不会产生内存空间碎片。这种特性有利于程序长时间运行。
- 可预测的停顿:除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小及耗时的经验值)在后台维护一个优先列表,根据每次允许的收集时间,优先回收价值最大的Region。
G1收集器虽然还保留着新生代和老年代的概念,但是Java堆的内存布局和其他收集器有很大差异,他讲整个Java堆划分成多个大小相等的独立区域(Region),因此新生代和老年代不再是物理隔离的,他们都是一部分Region(不需要连续)的集合。
避免可达性分析中的全堆扫描,G1收集器中的对象引用及其他收集器中的新生代和老年代之间的对象引用都是是使用Remembered Set来避免全堆扫描的。G1中每一个Region 都有一个对应的Rememberer Set,虚拟机发现程序在堆Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference 引用的对象是否处于不同的Region 之中(在分代的例子中就是检查老年代中的对象引用了新生代的对象)。如果是则通过CardTable把相关引用信息记录到被引用对象所属Region的Remembered Set中,在内存回收时,在GC Roots的枚举范围中加入Remembered Set 中即可避免不用全堆扫描也可确保不遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作过程可分为如下几步
- 初始标记 (Initial Marking):仅仅标记下GC Roots能够直接关联的对象且修改TAMS(Next Top at Mark Start)让下一阶段用户程序并发运行是能够在整齐可用的Region中创建对象,此阶段需要停顿线程但是耗时很短。
- 并发标记(Concurrent Marking):对堆中对象进行可达性分析
- 最终标记 (Final Marking):修正并发标记期间因用户程序继续运行而导致变动的那一部分标记记录。并将这段时间对象变化记录在Remembered Set Logs 中,并把Remembered Set Logs 的数据合并到Remembered Set中
- 筛选回收 (Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户期待的GC停顿时间来指定回收计划。
GC日志
为了方便用户阅读,即使是不同收集器的日志都是同样的格式。
可以用如下参数打印更详细的GC日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
GC日志示例
2015-05-26T14:45:37.987-0200: 151.126:
[GC (Allocation Failure) 151.126:
[DefNew: 629119K->69888K(629120K), 0.0584157 secs]
1619346K->1273247K(2027264K), 0.0585007 secs]
[Times: user=0.06 sys=0.00, real=0.06 secs]
2015-05-26T14:45:59.690-0200: 172.829:
[GC (Allocation Failure) 172.829:
[DefNew: 629120K->629120K(629120K), 0.0000372 secs]
172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs]
1832479K->755802K(2027264K),
[Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs]
[Times: user=0.18 sys=0.00, real=0.18 secs]
前面的时间代表GC发生的时间,默认为Java虚拟机启动以来经过的秒数,可以通过参数-XX:+PrintGCDateStamps 改为日期格式。
GC日志开头的 GC和Full GC 说明了本次垃圾收集的类型而不是用来区分新生代GC和老年代GC,Full 说明了此次GC是发生了STW的。如果是调用调用System.gc()方法触发的收集,则会显示为[Full GC(System)].
接下来的“DefNew”、“Tenured”、“MetaSpace”标识GC发生的区域。Serial收集器中新生代的名称为 Default New Generation因此显示为DefNew。如果是ParNew 收集器则会变为 ParNew (Parellel New Gennerration).如果是 Parallel Scanvenge 收集器则新生代称为 PSYongGen.
DefNew: 629119K->69888K(629120K) 含义是 新生代GC前该内存区域已使用容量-> GC后该内存已使用容量(该内存区域总容量)
[Tenured: 1203359K->755802K(1398144K), 0.1855567 secs]表示GC前堆内存使用容量->GC后堆已使用容量(堆总容量)
常用参数
通用配置
使用示例
-Xms
设置Java堆的初始大小。当可用的Java堆区内存小于40%时,JVM就会将内存调整到选项-Xmx所允许的最大值
-Xms100M
-Xmx
设置Java堆的最大值。当可用的Java堆区内存大于70%时,JVM就会将内存调整到选项-Xms所指定的初始值。与-Xms设置为一样的话可以避免堆自动扩展带来的性能开销。
-Xmx300M
-Xss
设置栈容量大小。每个线程都拥有一个栈,生命周期与线程相同,每个方法的调用都会创建一个栈帧。在堆容量确定的情况下,栈容量越大意味着能建立的线程越少。
-Xss2M
-Xmn
设置新生代的大小。-Xmn的内存大小为Eden+2个Surivivor空间的值,官方建议配置为整个堆的3/8。
-Xmn100M
-XX:NewSize
设置新生代的初始大小。和-Xmn等价,推荐使用-Xmn,相当于一次性设定了NewSize与MaxNewSize的内存大小。
-XX:NewSize=100M
-XX:MaxNewSize
设置新生代的最大值
-XX:MaxNewSize=100M
-XX:NewRatio
新生代(Eden+2个Surivivor空间)与老年代(不包括永久代)的比值
-XX:NewRatio=4,表示新生代与老年代所占的比值为1:4
-XX:SurivivorRatio
Eden与一个Surivivor的比值大小。默认为8:1,即Eden占8/10。
-XX:SurvivorRatio=8
-XX:PermSize
设置非堆内存(方法区,永久代)的初始大小。方法区主要存放Class相关信息,如类名、访问修饰符、常量池、字段描述等。
-XX:PermSize=10M
-XX:MaxPermSize
设置非堆内存(方法区,永久代)的最大值
-XX:MaxPermSize=50M
-XX:MaxDirectMemorySize
设置本机直接内存大小。一般通过Unsalf类来操作直接内存。
-XX:MaxDirectMemorySize=100M
-XX:PretenureSizeThreshold
设置对象超过指定字节大小时直接分配到老年代
-XX:PretenureSizeThreshold=3145728
-XX:+HandlePromotionFailure
是否允许新生代收集担保失败。
进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留
-XX:+HandlePromotionFailure
-XX:ParallelGCThreads
设置并行GC进行内存回收的线程数
-XX:ParallelGCThreads=4
-XX:MaxTenuringThreshold
晋升到老年代的对象年龄。每次Minor GC之后,存活年龄就加1,当超过这个值时进入老年代。默认为15
-XX:MaxTenuringThreshold=15
-XX:+HeapDumpOnOutOfMemoryError
内存溢出时Dump出当前的内存堆转储快照以便事后进行分析
-XX:+HeapDumpOnOutOfMemoryError
-XX:+PrintGCDetails
发生垃圾收集时打印详细回收日志,
并且在退出的时候输出当前内存区域分配情况。
-XX:+PrintGCDetails
XX:+PrintGCDateStamps
输出GC时的时间戳
XX:+PrintGCDateStamps
XX:+PrintHeapAtGC
在进行GC的前后打印出堆的信息
XX:+PrintHeapAtGC
-XX:+PrintGCApplicationStoppedTime
输出GC造成应用暂停的时间
-XX:+PrintGCApplicationStoppedTime
-Xloggc
日志文件的输出路径
-Xloggc:./logs/gc.log
-XX:+DisableExplicitGC
是否关闭手动System.gc
-XX:+DisableExplicitGC
Serial收集器
使用示例
-XX:+UseSerialGC
开启Serial收集器。
年轻代收集,单线程收集,采用复制算法,默认Client模式默认收集器,对于单核CPU和堆内存小使用效率比较好。
缺点是Stop-the-world。
-XX:+UseSerialGC
ParNew收集器
使用示例
-XX:+UseParNewGC
开启ParNew收集器。
年轻代收集,多线程,采用复制算法,好处是速度快,可开启多个垃圾线程收集,
优点是可并行运行,缺点单核CPU下效率不比Serial收集器快。
除了Serial收集器外,目前只有它能和CMS收集器配合工作。
它也是使用CMS收集器时默认的新生代收集器。
-XX:+UseParNewGC
Parallel Scavenge收集器
选项
描述
使用示例
-XX:+UseParallelGC
开启Parallel Scavenge收集器。
年轻代收集,采用复制算法,可并行多线程收集器,
优点是注重吞吐量,缺点是停顿时间较长,用户体验不太好。
-XX:+UseParallelGC
-XX:MaxGCPauseMillis
最大GC停顿时间(毫秒),虚拟机将尽可能保证回收停顿时间不超过设定值。
-XX:MaxGCPauseMillis=10
-XX:GCTimeRatio
垃圾收集时间占总时间的比值,相当于吞吐量的倒数。
例如设置吞吐量19,那最大GC时间就占总时间5%(1/(1+19)),默认值99。
-XX:GCTimeRatio=99
-XX:+UseAdaptiveSizePolicy
自适应调节策略,当打开这个参数之后,
JVM会根据运行情况收集性能监控信息,
将动态调整这些参数(如-Xmn,-XX:SurivivorRatio,-XX:PretenureSizeThreshold等)
-XX:+UseAdaptiveSizePolicy
Serial Old收集器
使用示例
-XX:+UseSerialOldGC
开启Serial Old收集器。
老年代收集,单线程,采用标记整理算法,优缺点和Serial收集器类似。
-XX:+UseSerialOldGC
Parallel Old收集器
选项
描述
使用示例
-XX:+UseParallelOldGC
开启Parallel Old收集器。
老年代收集,多线程,采用标记整理算法。
优缺点和Parallel Scavenge收集器一样,只是之前老年代收集只有Serial Old收集,
所以Parallel Scavenge收集和Serial Old收集组合一起发挥不了吞吐量的优势,
所以出现了该收集器。
注重吞吐量以及CPU资源敏感的场合,优先考虑Parallel Scavenge+Parallel Old收集器。
-XX:+UseParallelOldGC
CMS收集器
使用示例
-XX:+UseConcMarkSweepGC
开启CMS收集器。
老年代收集,多线程,采用标记清除算法,
优点是可并行并发处理,注重停顿时间,用户体验更快,
缺点是产生浮动垃圾,内存碎片,吞吐量会下降。
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction
由于CMS收集存在浮动垃圾,CMS不能等到老年代用尽才进行回收,
而是使用率达到设定值就触发垃圾回收。不能设置太高,
否则会出现“Concurrent Mode Failure”错误而
临时启用Serial Old收集器导致停顿时间加长。
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly
开启固定老年代使用率的回收阈值,
如果不指定,JVM仅在第一次使用设定值,后续则自动调整。
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+UseCMSCompactAtFullCollection
开启对老年代空间进行压缩整理(默认开启)。
由于CMS收集会产生内存碎片,所以需要对老年代空间进行压缩整理。
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction
设置执行多少次不压缩的Full GC后,紧接着就进行一次压缩整理
(默认为0,每次都进行压缩整理)。
-XX:CMSFullGCsBeforeCompaction=5
-XX:+CMSScavengeBeforeRemark
执行CMS 重新标记(remark)之前进行一次Young GC,
这样能有效降低remark时间。
-XX:+CMSScavengeBeforeRemark
G1 收集器
-XX:+UseG1GC
-XX:+UseG1GC
-XX:MaxGCPauseMillis
设置最大GC停顿时间。这是一个软性指标, JVM 会尽量去达成这个指标。
-XX:MaxGCPauseMillis=10
-XX:InitiatingHeapOccupancyPercent
启动并发GC周期时的堆内存占用百分比。G1用它来触发并发GC周期,基于整个堆的使用率而不只是某一代内存的使用比。
值为 0 则表示"一直执行GC循环", 默认值为 45。
-XX:InitiatingHeapOccupancyPercent=70
-XX:ParallelGCThreads
设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同。
-XX:ParallelGCThreads=4
-XX:ConcGCThreads
并发垃圾收集器使用的线程数量。默认值随JVM运行的平台不同而不同。
-XX:ConcGCThreads=4
-XX:G1ReservePercent
设置预留堆大小百分比,防止晋升失败,默认值是 10。
-XX:G1ReservePercent=20
-XX:G1HeapRegionSize
指定每个heap区的大小,默认值将根据 heap size 算出最优解。
最小值为 1Mb,最大值为 32Mb。
-XX:G1HeapRegionSize=16M
-XX:InitialSurvivorRatio
设置Survivor区的比例,默认为5。
-XX:InitialSurvivorRatio=10
-XX:+PrintAdaptiveSizePolicy
打印自适应收集的大小。默认关闭。
-XX:+PrintAdaptiveSizePolicy
-XX:G1MixedGCCountTarget
混合GC数量,默认为8。
减少该值可以解决晋升失败的问题(代价是混合式GC周期的停顿时间会更长)。
-XX:G1MixedGCCountTarget=8