GC参数

本博客是炼数成金JVM课程的第五课

目录

  1. 堆的回顾
  2. 串行收集器
  3. 并行收集器
  4. CMS收集器
  5. Tomcat实例演示

堆的回顾

堆的结构

堆总体上可以分为两类,新生代和老年代
老年代存放着老年对象,新生代存放着年级较轻的对象
一般来说,对象在创建出来的时候,绝大部分是会存放到eden区的。除此之外,对象一般都在新生代产生。
如果在新生代进行收集之后这个对象还能幸存,那么就会被放到幸存带,也就是s0,s1(from, to)。 这两个大小相等,完全对称,使用的是复制算法,所以空间浪费比较厉害。
当对象在内存中不断地进行垃圾收集,对象的年龄到了一定的阶段,就会被放入到老年代。

串行收集器

最古老,最稳定
效率高
可能会产生较长的停顿
-XX:UseSerialGC
- 新生代,老年代使用串行回收
- 新生代复制算法
- 老年代标记-压缩

整体来说,串行收集器效率相对而言是最高的,且bug较少。但他可能会产生较长的停顿,并且串行收集器只使用一个线程去回收对象,所以在多核的服务器上不能发挥最好的性能。

串行收集器

应用程序线程会有多个,但是一旦回收开始,应用线程停止,GC线程开始。但是在串行收集器中,GC线程只有一个。当GC完成之后,应用线程恢复,开始运行。

并行收集器

ParNew

-XX:+UseParNewGC
新生代并行,老年代串行
这个是串行收集器新生代的并行版本
在新生代回收时,使用的是复制算法
-XX:ParallelGCThreads: 限制回收的数量

并行收集器

可见,并行收集器与串行收集器的区别在于,GC线程是多进行并发的,但是应用程序线程也都是暂停的。
多线程并不一定会比单线程快。在多核CPU上,设置合理的线程数,一般会比较快,如果在单CPU上,建议使用串行收集器。

0.834: [GC 0.834: [ParNew: 13184K->1600K(14784K), 0.0092203 secs] 13184K->1921K(63936K), 0.0093401 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

ParNew 并行收集器的日子中会有ParNew字样的输出。

Parallel收集器

类似于ParNew收集器
新生代采用复制算法
老年代 采用 标记-压缩算法
更加关注吞吐量
-XX:UseParallelGC
使用Parallel 收集器 + 老年代串行 + 新生代并行
-XX:+UseParallerOldGC
使用Parallel收集器 + 老年代并行 + 新生代并行

这里写图片描述

与ParNew是相同的,GC线程多线程并发,应用程序会被暂停。

1.500: [Full GC [PSYoungGen: 2682K->0K(19136K)] [ParOldGen: 28035K->30437K(43712K)] 30717K->30437K(62848K) [PSPermGen: 10943K->10928K(32768K)], 0.2902791 secs] [Times: user=1.44 sys=0.03, real=0.30 secs]

可以在日志中看到PSYoungGen 字样和 ParOldGen 字样的输出记录

并行收集器的参数

-XX:MaxGCPauseMills
最大停顿时间(GC回收时,应用程序停顿的时间),单位毫秒
GC尽力保证回收时间不超过设定值,但不保证一定可以达到,只作为目标值设定
-XX:GCTimeRatio
0-100的取值范围
垃圾收集时间占总时间的百分比,公式为1/(1+n)
默认99,即最大允许1%时间做GC

这两个时间是矛盾的,因为停顿时间和吞吐量不可能同时调优。
一般我们希望停顿时间最短,垃圾回收占用的时间最少。但是在GC的时候,垃圾回收的工作总量是一定的。就是一定要花这么多时间做这么多事情,我们可以安排什么时候做这些事情。我们也可以把GC频度提高,那每一次GC所花的时间就比较短,因为每次的GC的量比较少,但因此系统的性能会受到影响,因为停顿的时间多了。
反过来,我们让GC少做几次。但因为每一次GC都会面向很多的对象和垃圾,导致一次GC所花的时间比较长,那么系统的停顿也会比较长。

CMS收集器

Concurrent Mark Sqeep 并发标记清除(并发:垃圾回收器跟应用程序线程一起执行,交替执行。并行:开多个线程执行)
使用的是标记清除算法
并发阶段会降低吞吐量
老年代收集器(新生代使用ParNew)
-XX:UseConcMarkSweepGC

CMS运行过程比较复杂,着重实现了标记的过程,可分为

  • 初始标记
    • 根对象可以直接关联到对象,做一个标记
    • 做这个标记速度很快,但会产生一个全局停顿
  • 并发标记(和用户线程一起)
    • 与用户线程一起运行,用户线程一边运行,并发标记一遍做标记,给全部对象打上标记,是垃圾还是不是垃圾对象
  • 重新标记
    • 由于并发标记时,用户线程依然运行,因此在正式清理时,再做修正。重新标记时是独占CPU的,会产生一个全局停顿
  • 并发清除(和用户线程一起)
    • 基于标记结果,直接清理对象

可见,这里面的四步,其中并发标记和并发清除与用户线程一起执行,初始标记和重新标记是会产生一定的停顿的,但是标记过程的主要工作负载在并发标记上,因此可以将全局停顿尽可能的缩小,但不能完全消除。

CMS收集器

程序运行中,在需要进行垃圾回收时,应用程序线程停止,初始标记开始(初始标记是独占的)。初始标记执行时间很短,结束后,应用程序继续执行,与此同时,并发标记也开始执行。并发标记扫描所有的对象进行标记。因为标记过程中,应用程序还在运行,会有新的垃圾产生,因此需要进行修正(重新标记)。修正时应用程序暂停(重新标记独占),可以认为重新标记是并发标记的补充,而且重新标记也不会占用大量的时间。重新标记结束后,垃圾对象已经被完全找出,就开始并发清理。清理时应用程序一起运行。并发清理完成后,并发重置会把内部的数据结构,数据对象做一个清理,为下一次收集做准备。
注意:因为在清理时,应用程序还在运行,所以我们采用的是标记清除算法,只要把对象直接从内存中删除。而如果是标记压缩算法等需要将对象移个位置或者压缩对象,此时应用程序就很难继续运行(清除时应用程序继续运行,对象位置的改变会让应用程序无法准确找到对象的位置)。

1.662: [GC [1 CMS-initial-mark: 28122K(49152K)] 29959K(63936K), 0.0046877 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
1.666: [CMS-concurrent-mark-start]
1.699: [CMS-concurrent-mark: 0.033/0.033 secs] [Times: user=0.25 sys=0.00, real=0.03 secs] 
1.699: [CMS-concurrent-preclean-start]
1.700: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
1.700: [GC[YG occupancy: 1837 K (14784 K)]1.700: [Rescan (parallel) , 0.0009330 secs]1.701: [weak refs processing, 0.0000180 secs] [1 CMS-remark: 28122K(49152K)] 29959K(63936K), 0.0010248 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
1.702: [CMS-concurrent-sweep-start]
1.739: [CMS-concurrent-sweep: 0.035/0.037 secs] [Times: user=0.11 sys=0.02, real=0.05 secs] 
1.739: [CMS-concurrent-reset-start]
1.741: [CMS-concurrent-reset: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

CMS 回收器的日志中会有CMS字样的输出

特点:

尽可能降低停顿
会影响系统整体吞吐量和性能
比如: 在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
清理不彻底
因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理
因为和用户线程一起运行,不能在空间快满时再清理
-XX:CMSInitiatingOccupancyFraction 设置出发GC的阈值
如果不幸内存预留空间不够,就会引起concurrent mode failure

concurrent mode failure 的日志

33.348: [Full GC 33.348: [CMS33.357: [CMS-concurrent-sweep: 0.035/0.036 secs] [Times: user=0.11 sys=0.03, real=0.03 secs] 
 (**concurrent mode failure**): 47066K->39901K(49152K), 0.3896802 secs] 60771K->39901K(63936K), [CMS Perm : 22529K->22529K(32768K)], 0.3897989 secs] [Times: user=0.39 sys=0.00, real=0.39 secs]

在清理的时候,应用程序不断的申请内存,导致内存不够用,此时会出现concurrent mode failure的错误。这个时候,JVM会把应用程序全部暂停,改用串行收集器进行垃圾收集。同时,系统有可能会产生一个长时间的停顿。而一般出现concurrent mode failure的错误,就意味着堆空间已经消耗殆尽,更意味着这一次的串行收集会耗费大量的时间。

标记清除和标记压缩的区别:
标记清除:标记垃圾对象后,把他从内存中删除
标记压缩:标记垃圾对象后,将可用对象移动到内存的一端,将边界之外的对象清理
运行完成之后的内存空间如下:
标记清除和标记压缩结束后内存空间的状态
可见,标记清除的结果可能会导致大量的内存碎片,标记压缩之后内存是连续的,没有碎片。
假设我们现在要申请五个连续的内存单元,在标记清除之后,是无法分配的,尽管在图中有大于五个单位的空间,但没有五个连续的内存空间。而在标记压缩过后的内存中是存在的。
因此,可以认为,标记清除相对于标记压缩的不同在于标记清除会产生碎片,而碎片会影响内存分配。
所以在使用标记清除之后,还有一件必不可少的事情,就是对标记清除之后的内容进行一次压缩处理。这就是为什么串行回收器和并行回收器采用标记压缩的算法。
而CMS更关注于减小停顿,为了在做GC的时候和用户线程一起工作,所以采用压缩算法,那么需要移动内存空间,那么用户线程可能就找不到要用的对象在哪里,就很难跟应用程序并发执行。为了达到这个要求,就要保证可用内存的位置没有改变。
所以为了解决CMS之后的碎片的问题,可以采用如下的参数:
-XX:+ UseCMSCompactAtFullCollection
Full GC后,进行一次整理
整理过程是独占的,会引起停顿时间变长(整理是需要对对象进行移动整理)
-XX:+CMSFullGCsBeforeCompaction
设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads
设定CMS的线程数量

CMS虽然尽量的保证在大部分情况下,能有一个比较短的停顿,但一旦超过了负载,在几次GC之后,碎片增多之后,由于清理碎片的问题,一样会引起长时间的停顿。

GC参数整理

-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例
-XX:NewRatio:新生代和老年代的比
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParallelGC :新生代使用并行回收收集器
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数
-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器(CMS失败的情况下使用)
-XX:ParallelCMSThreads:设定CMS的线程数量
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收

Tomcat 实例

环境
Tomcat 7
JSP 网站
测试网站吞吐量和延时
工具
JMeter
目的
让Tomcat有一个不错的吞吐量

系统结构
系统结构

JDK6: 使用32M堆处理请求
参数:
set CATALINA_OPTS=-server -Xloggc:gc.log -XX:+PrintGCDetails -Xmx32M -Xms32M -XX:+HeapDumpOnOutOfMemoryError -XX:+UseSerialGC -XX:PermSize=32M
这里写图片描述

GC信息

为了获得更好的效果,我们直接增大堆内存大小
JDK6:使用最大堆512M堆处理请求
参数:
set CATALINA_OPTS=-Xmx512m -XX:MaxPermSize=32M -Xloggc:gc.log -XX:+PrintGCDetails
结果:FULL GC很少,基本上是Minor GC

这里写图片描述

这里写图片描述

这里写图片描述
由于我们没有设置最小堆,所以在刚开始运行时,是16M的堆内存空间,系统运行一段时间之后,系统内存扩大到了60M
此时Full GC很少发生,提高了性能,因为Full GC很耗时间

JDK6:使用最大堆512M堆处理请求
参数:
set CATALINA_OPTS=-Xmx512m -Xms64m -XX:MaxPermSize=32M -Xloggc:gc.log -XX:+PrintGCDetails

结果 GC数量减少 大部分是Minor GC
这里写图片描述
这里写图片描述
上一个例子,堆空间从16M变到了60M,那么一开始我们就把堆设置为64M,可以看到GC数量大大减少
如果我们设置一个小的堆,JVM会尽量把系统维持在一个小的堆上面运作,那么就会有更多的GC,把堆设大之后,就可以减少GC

JDK6:使用最大堆512M堆处理请求
参数:
set CATALINA_OPTS=-Xmx512m -Xms64m -XX:MaxPermSize=32M -Xloggc:gc.log -XX:+PrintGCDetails -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:ParallelGCThreads=4

结果:GC压力原本不大,修改GC方式影响很小
这里写图片描述

JDK 6
set CATALINA_OPTS=-Xmx40m -Xms40m -XX:MaxPermSize=32M -Xloggc:gc.log -XX:+PrintGCDetails

减小堆大小,增加GC压力,使用Serial回收器
这里写图片描述

JDK 6
set CATALINA_OPTS=-Xmx40m -Xms40m -XX:MaxPermSize=32M -Xloggc:gc.log -XX:+PrintGCDetails -XX:+UseParallelOldGC -XX:ParallelGCThreads=4

减小堆大小,增加GC压力,使用并行回收器
这里写图片描述

JDK 6
set CATALINA_OPTS=-Xmx40m -Xms40m -XX:MaxPermSize=32M -Xloggc:gc.log -XX:+PrintGCDetails -XX:+UseParNewGC

减小堆大小,增加GC压力,使用ParNew回收器
这里写图片描述

启动Tomcat 7
使用JDK6
不加任何参数启动测试
这里写图片描述
升级JDK之后,性能带来了额外的提升

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值