在调优Java应用程序时,重点一般放在两个主要目标上:响应性 或 吞吐量。html
响应性Responsiveness
是指应用程序对请求的数据作出响应的速度:java
吞吐量Throughput
专一于最大程度地提升应用程序在特定时间段内的工做量:算法
较长的暂停时间Pause Time
对于注重响应性的应用程序是不可接受的,但对于注重吞吐量的应用程序来讲能够接受的。前者重点是在短期内作出响应,后者则侧重与长时间运行的处理效率。数据库
可达性分析是 Java GC 算法的基础,基本思路就是以一系列名为 GC Roots
对象做为起始点,经过引用关系遍历对象图,若是一个对象到 GC Roots
间没有任何可达路径相连时,则说明此对象能够被回收。segmentfault
能够做为 GC Roots
的对象:缓存
可达性分析中重要的一环就是遍历整个堆,并标记其中的存活对象。一种经常使用的标记算法是 三色标记法tri-color marking
:服务器
每一个对象可能为如下 3 种颜色之一:数据结构
标记算法从 GC Roots 出发遍历堆,可达对象先标记 gray,而后再标记 为 black。并发
遍历完成以后全部可达对象都是 black 的,此时全部标记为 white 的对象都是能够回收的。oracle
当实现并发标记算法时,必须防止 white 对象被漏标,不然可能致使不应回收的对象被回收。
传统垃圾收集器将堆分红三个部分:年轻代YoungGen = Eden + Survivor
,老年代OldGen
和永久代PermGen
,每一个区域内存连续且大小固定。
将堆内存进行划分后,能够按照对象生命周期长短,在不一样区域使用不一样的回收算法,提升 GC 的效率。
标记-清除
用一个空闲列表free-list
记录失效对象占用的内存区域,方便后续从新分配给新对象。
标记-整理
将全部存活对象移动到内存区域的开头,剩余的连续内存区域都是可用的空闲空间。
标记-复制
将内存划分为活动区间与空闲区间,前者用于动态分配对象,后者用于容纳 GC 存活对象。
GC 时只需将存活对象从前者复制到后者,而后交换二者的角色便可。
CMS Concurrent Mark-Sweep
是一个采用 标记-清除 算法的老年代收集器。
它经过与应用程序线程并发执行大多数垃圾回收工做,来最大程度地减小因为 GC 致使的暂停。
一般状况下,CMS 收集器不会复制或压缩活动对象,这意味着无需移动活动对象便可完成垃圾回收。
然而过多的内存碎片可能形成分配失败,最终致使 FullGC。能够经过分配更大的堆来规避这一问题。
CMS 对老年代的回收能够分为如下几个步骤:
Initial Mark (STW) 初始标记
Concurrent Mark 并发标记
GC 线程遍历 Initial Mark 阶段标记出来存活的老年代对象,而后递归标记这些可达的对象。
该阶段与应用线程并发运行,期间会发生新生代对象晋升、老年代对象引用关系更新,须要对这些对象进行从新标记,避免发生遗漏。
CMS 用一个card-table
管理老年代,并发标记过程当中,某个对象的引用关系发生了变化,则将对象所在的内存块标记为 Dirty Card。
CMS 使用增量更新incremental update
解决并发修改致使的漏标问题:把 black 对象从新标记为 grey,下次从新扫描其引用。
Preclean 预清理
这一阶段主要是处理 Concurrent Mark 阶段中引用关系改变,致使没有标记到的存活对象的。经过并发地从新扫描这些对象,预清理阶段能够减小 Remark 阶段的 STW。
这个阶段会处理前一个阶段被标记为 Dirty Card 的部分,将其中变化了的对象做为 GC Root 再进行扫描并从新标记。
Abortable Preclean 可终止的预清理
这个阶段做用与 Preclean 相似,但能够经过设置 扫描时长(默认5秒)或 Eden 区使用占比(默认50%)控制本阶段的结束时机。
增长这一阶段的缘由,是期待这期间能发生一次 YoungGC 清理无效的年轻代对象,减小 Remark 阶段扫描年轻代的时间。
Remark (STW) 从新标记
:
这个阶段同时扫描 YoungGen 与 OldGen,从新标记整个老年代中全部存活对象。
因为以前的 Concurrent Mark 与 Preclean 阶段是与用户线程并发执行的,年轻代对老年代的引用可能已经发生了改变,Remark 要花不少时间处理这些改变,会致使长时间的 STW。
此外,即便新生代的对象已经不可达了,CMS 也会使用这些不可达的对象当作的 GC Roots 来扫描老年代,致使部分失效的老年代对象没法被及时回收。
能够加入参数 -XX:+CMSScavengeBeforeRemark,在从新标记以前,先执行一次 YoungGC,回收掉年轻代的对象无用的对象。这样进行年轻代扫描时,只须要扫描 Survivor 区的对象便可,通常 Survivor 区很是小,这大大减小了扫描时间。
Concurrent Sweep 并发清理
至此,老年代全部存活的对象已经被标记完成。这个阶段主要是清除那些没有标记的对象而且回收空间。
被回收的空间会被添加到 空闲列表中,以供之后分配。这一过程可能会对空闲空间进行合并,可是不会移动存活对象。
因为该阶段是与应用线程并发运行的,天然就还会有新的垃圾不断产生,这一部分垃圾出如今标记过程以后,没法在当次收集中处理掉它们。只好留待下一次GC时再清理掉。这一部分垃圾就称为 浮动垃圾。
Resetting 重置
清除数据结构,并重置定时器,为下一轮 GC 作准备。
G1 Garbage-First
是一种服务器端的垃圾收集器:
G1 可以在大内存的多处理器计算机上,保证 GC 暂停时间可控,并实现高吞吐量。
其最终目的是取代 CMS 成为服务端 GC 更好的解决方案:
incremental collecting
算法,其 GC 暂停时间比 CMS 更具可预测性,并容许用户指按期望的暂停时间。G1 将堆划分为一组大小相等的且连续的堆区域Region
:
G1 中新生代与老年代再也不连续,每一个区域能够在 Eden、Survivor 与 Old 之间切换角色。此外,还有一类被称为 Humongous 的巨型区域,用于容纳体积 ≥ 标准区域大小的50%的对象。JVM 一般会将内存划分为 2000个区域,每一个大小从 1 到 32Mb 不等,由 JVM 在启动时经过 -XX:G1HeapRegionSize 指定。
每一个区域会被进一步细分红多个卡片Card
,每一个大小为 512Kb,用于实现细粒度的引用统计。
分区设计能够避免一次收集整个堆,每次 GC 只收集区域的一个子集 CSetcollection set
,其中必然包含全部 Young 区域,同时可能包括部分 Old 区域:
根据回收区域的不一样,能够将 GC 分为:
G1 根据存活对象的字节数统计每一个区域的 活跃度liveness
,而后根据指望停顿时间来肯定该 CSet 的大小,并保证那些垃圾多(活跃度低)的区域会被优先回收,故此得名 垃圾优先。
G1 的执行过程能够表示为由 3 个阶段组成的循环:
堆中一开始只有 YoungGen,所以只会触发 YoungGC,将 Eden 与 Survivor 区域中的活动对象复制到另外一个空闲的 Survivor 区域。
G1 中将 将存活对象复制到其余区域 的过程称为 疏散Evacuation
。为了减小停顿时间,疏散工做由多个 GC 线程并行完成。
YoungGC 过程当中会根据预期目标停顿时间 -XX:MaxGCPauseMillis 动态调整新生代的大小,经过 -XX:G1NewSizePercent 参数能够人为干预这一过程,但会让预期停顿时间参数失效。
当堆的总体占用空间足够大时(超过45%),就会进入 Concurrent Marking 阶段。经过 -XX:InitiatingHeapOccupancyPercent 选项能够配置这一行为。
与 CMS 相似,G1 中的并发标记包括多个阶段,其中一些阶段是并发的,另外一些阶段则会 STW。
Initial Mark (STW) 初始标记
扫描并标记 GC Root 对象直接可达的老年代存活对象。
Initial Mark 并无独立的执行阶段,而是嵌入 YoungGC 中执行的,其停顿时间会被分摊,所以实际的开销很是低。
Root Region Scan 扫描根区域
扫描 Root Region 并标记全部可达的老年代存活对象。
此处的 Root Region 就是先前 YoungGC 中生成的 Survivor 区域,其包含的对象都会被视为 GC Root。
为了不移动对象对标记产生影响,该过程必须在下次 YongGC 启动前完成。
Concurrent Mark 并发标记
启动并发标记线程,扫描并标记整个堆中的存活对象(线程数能够经过 -XX:ConcGCThread 进行配置)。
为了不重复标记,G1 使用 SATBsnapshot-at-the-beginning
算法解决漏标问题:
该约束是经过预写屏障pre-write barrier
实现:
log buffers
中,而后交由并发标记线程处理。
为了不移动对象对标记产生影响,该过程必须在下次 YoungGC 启动前完成。全部的标记任务必须在堆满前完成,若是堆满前没有完成标记任务,则会触发担保机制,经历一次长时间的串行 FullGC。
Remark (STW) 从新标记
启动并行标记线程,完成对整个堆中存活对象的标记(线程数能够经过 -XX:ParallelGCThread 进行配置)。
该阶段会暂停全部应用线程,避免发生引用更新,并完成对SATB 日志缓冲区中剩余对象的标记,找出全部未被访问的存活对象。
该阶段还执行一些额外的清理操做,例如:
Cleanup 清理垃圾
整理统计信息并识别出高收益的老年代分区,为 MixedGC 作准备。
主要工做有:
此外还会执行一些清理工做,为下一次 Concurrent Marking 作好准备。
MixedGC 主要流程与 YoungGC 相似,不一样的地方在于 CSet 中包含了 Old 区域。
须要注意的是,Concurrent Marking 结束后,并不必定会当即触发 MixedGC,中间可能会穿插屡次的 YoungGC。
当收集某个区域时,咱们必须知道是否有来自非收集区域引用,来肯定它们的活动性:
但查找整个堆很是耗时,同时也失去了增量收集的优点。为了解决这一问题,G1 为每一个区域维护了一个 RSetremembered set
,用于记忆从其余区域指向本身的引用。
在执行收集时,RSet 中引用信息会扮演局部 GC Roots 的角色,避免耗时的引用查找,保证每一个区域的 GC 可以独立进行:
注意,象若是 Old 区域中对在 Concurrent Marking 阶段被肯定为垃圾,即便有外部引用,该对象也会被做为垃圾回收。
接下来发生的事情与其余收集器所作的相同:多个并行GC线程找出哪些对象是活动的,哪些对象是垃圾:
最后,释放空闲区域,将活动对象移到 Survivor 区域,并在必要时建立新对象:
为了维护 RSet,在应用线程对字段执行写操做时,会触发写后屏障post-write barrier
:
为了减小写屏障带来的开销,该过程是异步的:
Dirty Card Queue
,而后由 Refine 线程将其拾取并将信息传播到被引用区域的 RSet。
若是应用线程插入速度过快,会致使 Refine 线程来不及处理,那么应用线程将接管 RSet 更新的任务,从而致使性能降低。
并发标记 与 增量收集 是 G1 实现高性能与可预测回收的关键。
对于 CPU 资源充足且对延迟敏感的服务端应用来讲,G1 算法可以在大堆上提供良好的响应速度。
做为代价,额外的写屏障与更活跃GC线程,会对应用的吞吐量产生负面影响。