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/ 里面的文章很有价值。

 

一些命令:

1// 监控gc的情况,每隔2秒输出一次gc的信息 2jstat -gcutil pid 2000 3 4// 每列的语言如下 5S0(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) 6

 

 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占满之前触发回收操作。

 

代码交流 2021