zhaoyu@home:~$

jvm-垃圾收集器

java有着自动的内存管理,就是我们通常所说的GC,GC的首要任务就是分配内存,在内存中管理引用对象,从不再引用的对象中恢复内存。

从java5开始,HotSpot虚拟机执行自我优化(self-tuning),这个过程包括在启动的时候基于平台信息选择最合适的GC及相关的设置,并进行持续的自我优化。

尽管GC会自动优化相关的设置,但是有时候我们依然需要基于下面的指标优化GC:

  1. 最大暂停时间,即GC暂停应用来回收内存的时间,也就是stop the world的时间。
  2. 吞吐量,即应用占用的时间,即非GC所占用的时间。

Serial收集器

名称中有serial,意味着是一个单线程的收集器,它在一个CPU上运行单个线程进行收集,在垃圾回收时,必须停止其他所有的工作线程,“Stop The World”, 所以Serial GC的暂停更加明显。

这个收集器最好使用在数据集小于100M的应用中,并且对低暂停时间不敏感,客户端程序一般都是低内存,所以Hot Spot客户端程序默认使用Serial Gc。

在命令行中通过下面命令指定Serial GC

-XX:+UseSerialGC

SerialGC中一个GC线程打断所有的应用线程。

Parallel收集器

Parallel是一种并行的stop-the-world收集器,意思是,当GC发生时,它停止所有的应用线程,使用多线程执行GC工作。Parallel收集器也称为吞吐量收 集器,可以在多cpu下执行多线程收集,使用多线程收集,极大地加快了GC的速度,但是个别gc引起的应用程序的暂停时间会很长,和serial GC面临着一样的 问题。

在parallel刚引入的时候,只有yong代使用parallel stop-the-world收集器,old代使用一个单线程的stop-the-world收集器。那时的延迟需求较宽松, 一个web程序可以容忍一秒的延迟,甚至3到5秒。启用parallel的命令如下:

-xx:+ UseParallelGC

但是随着heap的越来越大和old区的对象越来越多,收集需要的时间越来越多,Parallel对old带也使用了parallel的收集器。这个改变是在java6更新的。 通过使用

-xx:+ UseParallelOldGC

在java 7u4后,UseParallelOldGC默认被启用, 指定UseParallelGC会启用UseParallelOldGC,相应地指定UseParallelOldGC也会开启 UseParallelGC,即两个选项是等效的。

parallel GC在下面的情况下是一个好的选择:

  1. 吞吐量需求远比延迟需求重要。如一个批处理应用就是一个好的例子,因为它是非交互式的。当你运行一个批处理是,你只希望它尽快完成。
  2. 在最坏情况下,如果应用的延迟需求能够被满足,那么paralle GC将提供最好的吞吐量。最坏的延迟需求包括了暂停时间,和暂停发生的频率。 如,“超过500ms的暂停每两小时不能超过一次,所有的暂停不能超过3秒。”

拥有足够小的使用数据的交互应用,如一个Parallel GC的full GC 能够不超过最坏情况下GC导致的延迟,也适用于parallel收集器。然而由于数据大小 和heap的大小有关,这类应用适合本策略的情况是否有限。

	多个Parallel GC线程打断所有的应用线程,一般应用线程多于GC线程。

CMS(Concurrent Mark Sweep)

CMS也称为低延迟收集器,是一种以获取最短停顿时间为目的的收集器。它牺牲了一些应用的吞吐量用来减少GC暂停的时间。
CMS收集器中,yong代的GC类似于Parallel GC,他们是并行地 stop-the-world,意味着在多线程进行gc时,所有的java应用程序会暂停。
CMS和Parallel的主要区别是old代的收集。为了获得更小的暂停,CMS在应用线程运行的同时做了大部分工作,除了一些短暂的GC同步暂停的工作。
可以通过下面的命令启用CMS GC:

-XX:+UseConcurrentMarkSweepGC

很有可能,在一个yong代的GC发生的同时,一个old代的GC正在进行,当这种情况发生时,old代的GC会被yong代的GC打断,并在yong代GC完成后继续。 CMS的yong代GC被称为ParNew。

适用于处理长时间的大批量收集工作。适用于响应时间优于吞吐量和暂停时间好几倍的程序中。很多互联网或者B/S系统的服务器采用这种收集器。
CMS基于标记清除算法,整个步骤较前面的收集器复杂些,大体4步。

  1. 初次标记(initial mark), 这是CMS的第一个stop-the-world操作,一是标记old代中所有的GCRoot;二是标记被yong代中活着的对象引用的对象。

  2. 并发标记(concurrent mark) 遍历整个old代,并标记所有存活对象,和应用线程并行,不会关心应用程序改变的对象。

  3. 并发预清除(concurrent preclean) 和应用线程并行,标记前一个阶段应用程序改变的对象,并做一些必要的清扫工作。还会做一些final remark阶段需要的准备工作。

  4. 最终标记(final remark) 这是CMS的第二个stop-the-world的操作,该阶段的任务是完成标记整个年老代的所有的存活对象。由于之前的预处理是并发的,它可能跟不上应用程序改变的 速度,这个时候,STW是非常需要的来完成这个严酷考验的阶段。

  5. 并发清除(concurrent sweep) 这个阶段的目的就是移除那些不用的对象,回收他们占用的空间并且为将来使用。

其中,初次标记和最终标记两个步骤任然需要“Stop the World”。重新标记是为了修正并发标记期因用户程序运作而导致标记变动的那一部分对象的记录, 这个阶段一般会比初始标记要长一些,但是远比并发标记短。

由于整个过程中,时间最长的并发标记和并发清除都可以和用户线程一起工作,所以整体上,CMS是和用户线程一起并发执行的。

CMS的调优方法

  1. 由于是并发程序,所以要占用cpu资源,导致应用程序变慢。CMS默认启动的回收线程数是(CPU数量+3)/4,当cpu大于4个时,收集器线程占用不少于 25%的cpu资源,cpu在4个以下时,影响越大,可以通过以下参数设置CPU个数:
    -XX:ParallelCMSThreads=2
    

年轻代的并行收集线程数默认是(cpu <= 8) ? cpu : 3 + ((cpu * 5) / 8),如果你希望降低这个线程数,可以通过下面选项调整。

-XX:ParallelGCThreads= N 
  1. 会产生浮动垃圾,由于并发清理阶段,用户线程还在运行,这时可能会有新的对象产生。这一部分出现在标记之后,所以清理线程当次无法处理他们, 只能留到下一次GC,这一部分被称为“浮动垃圾”。

浮动垃圾导致收集器需要提前在老年代预留一部分空间,所以CMS不能像其他收集器一样等到老年代几乎被填满才收集。老年代达到一定阈值时,就需要触发。 可以通过-XX:CMSInitialingOccupancyFraction适当调节CMS的出发百分比。在JDK1.6后,CMS的启动阈值提升到92%。要是CMS运行期间预留的 内存无法满足“浮动垃圾”的分配需求,就会出现“Cocurrent Mode Failure”。这时虚拟机会启动后备方案,临时启动 Serial Old收集器回收老年代垃圾。 停顿时间就会很长,降低性能。

  1. CMS采用标记-清除算法,会产生大量的空间碎片。碎片整理无法并行,CMS提供了-XX:+UseCMSCompactAtFullCollection参数,用于收集器需要 进行FullGC时开启内存碎片的合并整理过程。另一个参数-XX:CMSFullGCsBeforeCompact设置执行多少次不压缩的FullGC后跟一次压缩FullGC。
  2. 为了减少第二次暂停的时间,开启并行remark: -XX:+CMSParallelRemarkEnabled。如果remark还是过长的话,可以开启 -XX:+CMSScavengeBeforeRemark选项,强制remark之前开始一次minor gc,减少remark的暂停时间,但是在remark之后也将立即开始又一次 minor gc。
  3. 为了避免Perm区满引起的full gc,建议开启CMS回收Perm区选项:
    -XX:+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled
    

G1收集器

G1 GC是一种基于“优先收集最多垃圾”策略的整理(compacting)收集器。目标主要是取代CMS收集器。G1用于用于大内存的多处理器计算机。
G1抛弃了传统的连续的代的heap布局。G1将heap分为一系列不连续的region,由这些region组成young和old代。
G1在收集暂停期间,收集大部分heap region。还有一部分会在多阶段并发标记循环中的cleanup阶段回收。在cleanup阶段,如果G1碰到纯粹的垃圾填充 region,它会直接回收这些region。
G1有三种类型的GC循环:一个young GC循环,一个多阶段的并发标记循环(multistage concurrent marking cycle),和一个混合收集循环 (mixed collection cycle)。还有一个单线程的full GC循环,这是当G1 发生evacuation fail时的故障安全机制。evacuation failure 又被称为晋升失败,或者to空间耗尽,通常在没有空闲空间晋升对象时发生。

young 代

G1的分代由young代和old代组成。young代的大小会在初始和最大的yong代值中计算。jdk8u45默认的是5%(通过-XX:G1NewSizePercent设置) 和60%(通过-XX:G1MaxNewSizePercent设置)。暂停时间指标可以通过(-XX:MaxGCPauseMillis)参数设定

对象的年龄和old代

每次young GC,GC都会对每一个对象维护一个年龄,GC会将这些年龄信息维护到一张“age table”中,可以通过(-XX:TargetSurvivorRatio (default = 50))定义survivor的容量。通过(-XX:MaxTenuringThreshold (default = 15))定义young区对象的年龄阈值。一旦对象超过 tenuring threshold,他们会被晋升到old 代的region中。

G1的设计

G1将heap分为多个region。region的大小会基于heap的大小,但是最小是1M,最大时32M,但必须是2的幂。所有的region大小相同,在JVM中不会改变。 大约会有2000个region平分heap的大小。如果heap太小,那么会少于2000个,heap太大,会比2000个多。

每一个区域都关联一个remembered set,被称为Rset,Rset的大小很大程度依赖于应用的行为,最低时可能占heap的1%,最高时可能占20%。

一个region一次只能用于一个目的(如yong,或者old代),但是被GC收集后,它完全清空对象,并释放为一个新的可用region。

除了可用region,在G1中有三种类型的region,第一种为yong 代region,有eden region和survivor region。eden region组成了eden space, survivor region组成了survivor space。eden和survivor region的数量在不同的GC后都可以改变,这些GC的类型有young,mix,Full。第二种为 old代region,组成了大部分的old代。最后一种为humongous region,存放着大小大于一个region一半或者更大的对象。humongous region被认为是 old代的一部分,jdk8u40,humongou region才作为old代的一部分,但是某些humongous region会作为yong 代的一部分。

一个region可以用于任何目的,即没必要把heap划分为连接的yong代和old代。G1会自动评估在一个给定的GC暂停时间内能够处理多少yong代的region。 当程序开始分配对象时,G1会选择一块可用区域,指定为eden region,使用该region处理内存,一旦这个region满了,另一个可用区域被指定为 eden region。这种处理过程会持续,直到达到最大的eden region的限制。此时一个young GC开始运行。

在一个young GC中,所有的young region,eden和survivor region都会被回收。一些可用区域会被标记为survivor或者old代region,然后所有的 存活对象被转移到这个survivor region或者old 代region。

当old代空间占用达到或者超过初始的heap占用阈值时,G1启动一个Concurrent Marking cycle。占用阈值可以通过下面参数设置,默认是heap的45%。

-XX:InitiatingHeapOccupancyPercent

当G1在mark阶段显示一个region没有存活对象时,它可以重新定义该region为一个可用region。包含了存活对象的old代region会被一个未来的mixed GC 处理。

G1使用多线程并发标记,他们在一个给定的时间间隙内尽可能多的完成工作,并暂停一会,暂停时允许应用线程的执行。

humongous对象

当一个humongous 对象分配内存时,G1分配一个足够大的连续的可用region来容纳大对象。如果没有足够的可用region,G1会做一次Full GC。 Humongous region只包含一个对象,这让G1在并发标记的时候更早地发现这个对象是否存活,如果不再存活,立即重新处理这个region。

Collection Sets

Collection Sets 即Cset是一个region集合,这些region在GC暂停期间会被GC回收。这些region中的存活对象将会被清空。region将会被重新设置 为可用。young 收集中,CSet只包含了young region。在一个mixed GC中,CSet不仅包含young region,还包含了一些old region。在mixed GC中, 有两个重要参数帮助CSet对old region的选取。

-XX:G1MixedGCLiveThresholdPercent

这个参数默认为85%,这是一个存活率阈值,任何old region的存活率低于该阈值,都会被包括进mixed collection的CSet中。

-XX:G1OldCSetRegionThresholdPercent

该值默认为java总heap的10%,设置了每一次mixed collection pause中能够被收集的old region数目的上限。这个阈值依赖于java heap对于 JVM进程的可用大小。

Remembered Sets

G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用是使用Remembered Set来避免扫描全堆。G1中每个Region都有一个 与之对应的Remembered Set,虚拟机发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的 对象是否处于不同的Region之间(在分代中例子中就是检查是否老年代中的对象引用了新生代的对象),如果是便通过CardTable把相关引用信息记录到被引用 对象所属的Region的Remembered Set中。当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。

Young GC

Young GC和Serial类似,将所有存活对象移动到survivor region中,或者晋升到old region中,或者两者都有进行。会在JVM在eden region中分配 内存失败时触发。

在每个Young GC暂停的时候,G1基于当前收集花费的时间计算出yong代需要扩大或缩减的尺寸;remember set的尺寸;目前,最大, 最小时young代的容量;以及暂停时间目标。所以,在young GC暂停完成后,young代会被重新调整大小。重新调整大小前后的尺寸可以用个GC日志查看。

Mixed GC

mixed GC和young GC类似,一个mixed GC除了young GC外,还关联一部分的old region的GC,这个old region GC将存活对象从某些old region (拥有很多垃圾,最可能被回收的)移动到另一些old region中。

一个mixed GC 在InitiatingHeapOccupancyPercent的阈值被超过,并且在一个Concurrent marking cycle完成后。

当old代空间占用达到或者超过初始的heap占用阈值时,G1启动一个Concurrent marking cycle。占用阈值可以通过下面参数设置,默认是heap的45%。

-XX:InitiatingHeapOccupancyPercent

有下面两个参数控制一个mixed collection cycle中的mixed collection总数。

-XX:G1MixedGCCountTarget

该参数设置在concurrent marking cycle完成后,mixed collection执行的次数。G1将需要收集的old代的region根据G1MixedGCCountTarget分割, 那么每次mixed collection pause需要收集的最小region数目为:需要收集的old代的region总数/ G1MixedGCCountTarget

-XX:G1HeapWastePercent

该参数默认为java heap的5%,每次mixed collection pause的时候,G1会基于可回收的死亡对象标识出可回收heap的数量。只要G1达到该参数设置的 阈值,G1会停止mixed collection pause。结束mixed collection cycle。这个设置浪费一定的heap空间,加快mixed GC的效率。

Full GC

G1的full GC和Serial GC使用同样的算法。当一个Full GC发生时,对整个heap进行一次整理。这样保证了heap拥有最大的可用空闲内存。G1的full GC 是单线程的,可能会导致一个很长的暂停时间。然而G1如此设计Full GC 是因为它不是必需的,G1希望不进行GC就能满足应用的性能。

Concurrent Cycle

一个G1的并发循环包括了几个阶段的活动:

  • initial marking,收集所有的GC root节点,应用线程必须停止,所以是stop-the-world。 在G1中,initial marking是作为yong GC pause的一部分,yong GC中会收集所有的GC root。
  • concurrent root region scanning, 标记操作必须扫描和跟踪所有survivor region中对象的所有引用,这个阶段,应用线程可以运行。
  • concurrent marking,这个阶段,多线程协同标记处所有存活的对象。同时允许应用线程运行。由于有并发操作占用资源,应用的吞吐量会有降低。
  • remarking, remark阶段,主要标记应用线程在前面的阶段中改变的对象。会进行较短的stop-the-world暂停。
  • cleanup,找到不含任何存活对象的region,重置为可用region。

所有的这些步骤除了cleanup,都可以认为是“标记活动对象”动作的一部分。

由于mixGC是G1释放内存的主要方式,所以标记阶段需要在G1用完可用region之前完成显得十分重要,否则,G1会启动一次Full GC释放内存。

Heap Size

G1可以像其他GC一样自动在-Xms和-Xmx之间调整heap的大小。
G1会在下列情况下增加heap的大小

  1. 一次Full GC中,基于heap大小的计算。
  2. 一次yong或者mixed GC中,计算GC使用的时间和应用花费的时间比,如果这个比超过了-XX:GCTimeRatio的设置,那么heap size就会增加, G1中GCTimeRatio默认是9,其他的GC默认是99,这个值越大,越容易增加heap size。
  3. 对象分配失败,导致的FullGC,G1会优先增加heap size。
  4. 一个GC需要新的可用空间时,G1会优先增加heap size,而不是进行一次full GC。