JVM GC Collector工作原理及优化

JVM 调优主要是调整GC以及一些执行参数:

目标:

堆不要太大,不然单次GC的时间过长导致服务器无法响应的问题

压力测试的时候TPS平稳

尽量避免full GC

检查是否用了并行的垃圾回收器

 

参数:

-server执行,开启优化

采用并行gc collector, -XX:+UseParallelGC +XX:+UseParallelOldGC +XX:+UseConcMarkSweepGC

-Xmx不要太大,不然单次gc的过程可能太长,大内存机器可以采用多个实例的方式

-Xms不要太小,不然jvm需要多次调整堆的大小,增加gc次数,影响了启动性能。

同理-XX:MaxPermSize

-XX:NewRatio新生代与老年代的比例要合适,这个需要看应用类型,一般新生代的对象很快就会被GC掉了。

线程池的大小要合适,线程上下文切换也是很耗资源的。

不要去调整一些你不完全了解的参数。

还有-Xverify:none可以加快启动速度,但字节码问题查错很麻烦。

 

字节码调优应该避免吧,容易触发一些jvm本身的bug,这些参数缺少实际场景的测试。

CMS垃圾收集器的相关参数,请读取相关资料。貌似CMS不会去移动对象的去使得空间更加紧凑。

G1垃圾收集器的工作原理,G1可以Compact Heap Region以减少内存碎片问题。

 

相关链接:一个Oracle jvm部门里面的员工的blog: https://blogs.oracle.com/jonthecollector/ 里面的文章很有价值。

 

一些命令:

// 监控gc的情况,每隔2秒输出一次gc的信息
jstat -gcutil pid 2000

// 每列的语言如下
S0(Survivor0) S1(Survivor1) E(Eden) O(Old) P(Perment) YGC (Young GC Count) YGCT(Young GC Time) FGC(Full GC Count) FGCT(Full GC Time)

 

 Memory Management In The Java HotSpot Virtual Machine

1、为什么要使用自动内存管理

为了消除手动内存管理带来的复杂性以及内存泄漏,dangling引用,内存碎片等一系列问题

2、动态内存分配是一个比较复杂的工作,因为它要使用内存的分配及释放足够快,同时还要考虑内存碎片问题。

3、垃圾回收器必须尽量少使得程序暂停,同时也需要在 回收耗时、空间大小,回收次数这几个方面取得一个平衡,另外还需要控制内存碎片。

4、垃圾回收器必须是可扩展,它不应该成为程序性能的瓶颈,它应该可以在多线程/多CPU的环境并行得执行。

5、垃圾回收器最好能够并发地进行内存回收工作,并发情况下,堆被划分成几个区域,这些区域会被并发地进行回收,由此减少了回收工作所引起的程序的暂停。

6、Compacting vs Non-Compacting vs Copying

7、垃圾回收器性能评判的几个指标:GC时间占程序运行时间的比重,非GC时间占比,GC引起的暂停时间,回收的频率,检测到垃圾对象的速度,回收器工作消耗的内存大小。

8、内存分代回收,比较流行分为young generation(新生代)及old generation(老年代),大多数分配的对象不会存活得太久,通常只有一小部分对象可以进化到老年代。因此新生代的区域回收得比较频繁,而老年代空间通过占用比较多的空间,因此各个区域会使用不同的算法进行垃圾回收。比如-XX:NewRatio=4表示 young generation : old generation = 1 : 4。在HotSpot Virutial Machine中,内存被分为三个区域,young generation, old generation和permenent generation,大部分对象都是在新生代进行分配的,有些对象会直接在老年代进行分配。新生代的空间由Eden生两个survivor区域来组成,一个survivor用来存放至少存活超过一次young generation GC的对象,而另一个survivor空间是空的,直接下一次回收的时候会被使用。一个suvivor与Eden的大小比值可以用-XX:SurvivorRatio=n来表示(1/n=survivor : Eden)

当old generation因塞满而无法存放young generation升级上来的对象时,将触发full GC,这时大部分的收集器会用老年代的算法去GC整个Heap(除了CMS Collector外, CMS Collector的老年代算法无法回收新生代区域)。

 

快速分配,在大片连接的内存块中进行分配内存的效率是很高的,可以利用bump-the-pointer技术来进行分配,分配器会记住下一次分配的起点。对于多线程的程序来说,内存分配操作需要是线程安全的,如果使用全局锁的话这会降低性能并造成性能瓶颈,相应的HotSpot采用一种叫做Thread-Local Allocation Buffers的技术(线程自己的内存分配缓冲区),使得减少获取全局锁的操作,通常TLAB占用大概1%的Edgen的大小。结合TLAB和bump-the-pointer技术,通常分配一个对象空间只需要10条本地指令。

 

Serial Collector

young generation使用Serial Collector进行回收的过程

当使用Serial Collector时候,young generation和old generation的回收工作都是使用单个CPU线性执行的,回收过程中将stop-the-world。下图展示了Serial Collector对young generation进行回收时过程,Eden区域存活的对象被复制到那个空的Survivor块(图中用to标记),如果对象太大超出了Survivor的大小,那么它将直接被copy到old generation区域,而非空的survivor(图中用from标记)中仍然年轻的对象也被复制到空的survivor中(图中用to标记),而相对比较“老”的对象则被复制到old generation区域中。

注意:如果"To" survivor被塞满了话,Eden和"From"Survivor区域还没有被copy的对象将直接被复制到old generation中(无论它们存活多少了多少代)。

在完成了对young generation的回收之后,young generation中Eden区域和"From" survivor都被被清空,只有"To" survivor中有存活的对象。这时,"From"和"To" Survivor将对换,如下图所示:

注意,存在old generation引用young generation对象的情况,为了避免进行young gc的时候扫描old generation,老年代对new generation的引用被记录在一个叫做card table的cache中。

old generation使用Serial Collector进行回收的过程:

当使用Serial Collector对old generation和permenent generation进行回收的时候,它将使用一种mark-sweep-compact的回收算法:

Mark阶段:Collector检测那些对象仍然存活。

Sweep阶段:Collector扫描整个old generation或者permenent generation,

Compact阶段:Collector将存活的对象“滑动”到old generation的首部,所有连续的空间放在old generation的尾部,这样方便利用bump-the-pointer来实现快速地分配对象。如下图所示:

什么时候应该使用Serial Collector。

大部分以Client-Style(java -client)运行的程序都使用这种收集器,这类程序对回收引种的程序暂停时候不敏感。在现在的硬件条件下,Serial Collector能够管理64MB大小的堆空间(现在应该可以256MB了吧)。

在JavaSE5中,运行非server-class的机器上默认使用Serial Collector,但用户可以使用-XX:+UseSerialGC命令参数来指定使用Serial Collector。

Parallel Collector(也被称为Throughput Collector)

如今,很多程序运行在拥有大内存多CPU的机器上面,Parallel Collector就是为了在垃圾回收过程中充分利用CPU资源而开发的。

使用Parallel Collector对young generation进行垃圾回收的过程:

Parallel Collector使用一种类似于Serial Collector对young generation回收的算法的并行版本,回收时它仍然会stop-the-world,但在回收的过程中它并行地使用多个cpu并行地执行,由些来减少垃圾回收所占用的时候并提升程序运行时间的占比。

使用Parallel Collector对old generation进行回收

Parallel Collector使用了与Serial Collector同样的mark-sweep-compact的回收算法。

什么时候使用Parallel Collector

运行在多cpu以及对停止时间不敏感的程序可以从使用parallel collector中受益,不频繁,耗时较长的针对old generation区域回收的仍然会发生, 批量处理,计费,工资以及科学计算这类程序比较适合使用Parallel Collector。

JavaSE5中,运行server-class的机器上默认使用Parallel Collector,用户可以使用-XX:UseParallelGC命令参数来显示指定使用Parallel Collector

 

Parallel Compacting Collector

Parallel Compacting Collector在JavaSE5.0 update 6被引入,与Parallel Collector不同的是,它使用了一种新的算法来对old generation进行回收,最终Parallel Compacting Collector将取代Parallel Collector

回收young generation时,Parallel Compacting Collector使用了与Parallel Collector同样的算法。

回收old generation和permenent generation时,Parallel Compacting Collector仍然会stop-the-world,但在整理存活对象的时候大部分是并行的。Parallel Compacting Collector使用三个阶段进行,

1、young/old/permenent generation区域被划分成几个固定大小的区域

2、marking阶段,程序中仍然可以引用到的存活的对象被划分给几个garbage collection threads中,然后mark工作是并发执行地,当一个对象被标记为存活的时候,它所在的regioin的大小将会被更新。

3、Summary阶段,通过前几次的收集,generation空间的首部会存活的对象会比较密集,通过compacting能回收的空间比较少,因此不值得在上面进行compacting,所以summary阶段所做的第一件事就是检验regions的密度,从最左边开始,直到碰到一个密度比较小,值得花时间去compacting的region,然后从这个region开始,compacting右边的region。summary阶段计算并存放被compacting region的新的首地址(这个阶段并没有真正地去Compacting)。注意:summary段是单线程执行的,尽管它可以实现为并发执行。但事实表明并发执行的

4、compacting阶段,在这个阶段中,利用上一阶计算出来的Compacting信息,各个线程可以独立地往region移动对象。Compacting完成之后,堆空间的后部将释放出一片连续的空间。

什么时候使用Parallel Compacting Collector

在Parallel Collector的基础上,Parallel Compacting Collector进一步减少了由于回收old generation所消耗的时候,进一步满足对垃圾回收引起的暂停时间 比较敏感的程序。但需要注意的是,Parallel Compacting Collector 可以不适合那些运行在大型机/刀片机的程序,这种机器上是不允许单独一个程序占用几个cpu过长时间,在这种环境下可以考虑利用参数-XX:ParallelGCThread=n,或者选择另外的垃圾回收器。

需要使用-XX:+UseParallelOldGC来显式指定使用Parallel Compacting Collector,这个参数的名字有点奇怪,这里的"Old"是指old generation。

 

Concurrent Mark-Sweep (CMS) Collector

对于很多应用来说,应用运行时间占比(throughput)没有响应时间这么重要,young generation区域的回收通常不会造成太长时间的暂停。但是old generation的回收,尽管不是很频繁,但通常会强制应用暂停比较长的时间。为了解决这个问题,HotSpot JVM 引入了一个叫做concurrent mark-sweep (CMS)的收集器,也叫做low-latency collector(低延迟回收器)

CMS回收young generation的过程:

CMS与Parallel Collector使用同样的方式回收young generation

CMS回收old generation的过程:

CMS在回收old generation大多数时候都是在程序运行时并发地执行,在开始一次完整的回收之前,CMS需要暂停一下程序(stop-the-world),这个过程叫做初始标记(Initial Mark),这个过程中查找程序代码中可以直接引用到的对象(通常是线程栈上的对象引用),然后在并发标记阶段,CMS去标记所有可达的对象,因为这个工作是并发进行的,应用同时也在更新一些字段的引用,所以在并发标记之后需要来一个stop-the-world,将新产生的对象标记完整,这个过程被称为remark,remark比initial mark更加耗时,所以一般使用多个线程并发地执行来提交效率。

在remark结束之后,所有的可达的对象都被标记了,然后接下来的并发扫描阶段将回收垃圾,下图阐述了CMS Collector与Serial  mark-sweep-compact Collector之间的差别:

 因为有一些工作,比如在remark阶段重新遍历对象,增加了collector的工作量,所以CMS回收时占用的CPU和内存资源也更多,但它减少了应用的停止时间。

需要注意的是,CMS collector是唯一一个不会去compact(整理)内存的收集器,如下图所示,这节省了一些时间,但因为这些空间并不连接,bump-the-pointer也不奏效了,因此它需要使用一个free列表去记录可使用的空间,然后在分配时去查找这个list。因此分配空间的操作效率相对要低一些,同时,这也会造成回收young generation的一些负担,因此回收young generation时需要从young generation里面复制一些存活的对象到old generation中,内存碎片的风险也增加了。

另外,与其它收集器不同的是,当old generation满了之后,它并不会发起一个old generation的回收,相反,它会尝试在还没有满的时候发起一次回收(因为在concurrent mark阶段程序是并发运行地),以便能够用完之前能够完成回收,否则它将转为使用与parallel collector 和serial collector一样stop-the-world方法去回收这部分空间。为了避免这一点,CMS Collector定时记录了一些垃圾回收的数据,比如回收的速率,然后在恰当的时候触发回收操作,避免在用完的时候再进行回收。另外,当old generation占用超过一定程度之后,CMS Collector也会去发起一次回收操作,可以用-XX:CMSInitiatingOccupanyFraction=n,n是old generation大小的占比,默认是68。

总之,CMS能够减少大部分程序由于回收工作而被暂停的时间 ,但结果的代价是:回收young generation变慢了,程序运行时间占比下降,以及更大的堆空间消耗。

增量式的回收

CMS Collector可以运行在增量回收的模式下,这种模式下,young generation回收过程的时候被分为几个小块的时间段。以减少stop-the-world时间。

CMS Collector适合那些对暂时时间比较敏感,允许GC操作并发地使用CPU,而且有大量存活对象的应用,比如web server。

必须使用-XX:+UseConcMarkSweepGC来显示指定使用CMS Collector,如果需要运行在增量模式下,必须使用-XX:+CMSIncrementalMode参数。

 

 默认堆大小

在-server模式下,jvm的默认初始heap大小是1/64的物理内存(最多1GB),默认最大堆大小是1/4物理内存大小(最大1GB)

-client模式下,默认4MB初始heap大小及64MB的最大堆大小。当然这些都可以通过命令参数进行覆盖。

 

其它参数

另外还可以使用-XX:MaxGCPauseMillis=n来指定GC造成的最大停顿时间,这个时间不一定能完成,不能完成的话,Collector会在堆未占满的情况触发回收操作。

另外也可以使用-XX:GCTimeRatio=n来设置GC时间占比(GC:程序运行时间),比如GCTimeRatio=4的情况下,GC时间将最大占用20%的时间。和-XX:MaxGCPauseMillis一样,如果不能满足要求,Collector将在geneartion占满之前触发回收操作。

 

posted on 2017-09-01 17:13  mosmith  阅读(1360)  评论(0编辑  收藏  举报