在实操作本章内容之前,请一定详细了解1/3章GC基础的内容,同时因为每个应用的情况不太一样,所以JVM调优没有一个统一的模式,只有深入了解其原理后才能进行调优操作。笔者大概罗列了一下JVM调优的必要过程:1、了解jvm原理;2、了解jvm相关参数;3、可读懂gc日志;4、上线压测。
一、基础知识
本节内容就是JVM调优时要修改的各种参数,笔者按垃圾回收器来进行分类,同时也常用的参数罗列一下,多数情况下在选定垃圾回收器类型后,只关注这些参数即可。
1.1、GC 事件分类
- Young GC:Young GC 每次都会引起全线停顿(Stop-The-World),暂停所有的应用线程,停顿时间相对老年代 GC 造成的停顿,几乎可以忽略不计;
- Old GC:只清理老年代空间的 GC 事件,只有 CMS 的并发收集是这个模式;
- Full GC:清理整个堆的 GC 事件,包括新生代、老年代、元空间等 ;
- Mixed GC:清理整个新生代以及部分老年代的 GC,只有 G1 有这个模式;
1.2、GC 调优目标
- 响应速度(Responsiveness):响应速度指程序或系统对一个请求的响应有多迅速。比如,用户订单查询响应时间,对响应速度要求很高的系统,较大的停顿时间是不可接受的。调优的重点是在短的时间内快速响应;
- 吞吐量(Throughput):吞吐量关注在一个特定时间段内应用系统的最大工作量。例如每小时批处理系统能完成的任务数量,在吞吐量方面优化的系统,较长的 GC 停顿时间也是可以接受的,因为高吞吐量应用更关心的是如何尽可能快地完成整个任务,不考虑快速响应用户请求;
1.3、优化方向
- 将新生代留在新生代
- 避免短命的大对象进入老年代
1.4、GC评测指标
GC 调优中,GC 导致的应用暂停时间影响系统响应速度,GC 处理线程的 CPU 使用率影响系统吞吐量。HotSpot 采用主动中断的方式,让执行线程在运行期轮询是否需要暂停的标志,若需要则中断挂起。需关注几个内存大小的设置和栈的设置大小(这会直接影响线程的个数)
- 吞吐量:总运行时间-GC时间/100;
- GC负载:GC时间/总运行时间;
- 停顿时间:stop the world, 如果是并行回收器,可能会并行业务线程和GC线程其时间不一定比单线程回收器的时间;
- GC回收频率:可以增大堆大小,但可能会增加stw时间;
- 反应时间:回收时间
性能表现方面,执行速度、内存分配、启动时间、负载承受能力。考核指标:执行时间、CPU时间、内存分配、磁盘吞量、网络吞量、响应时间,所以调优是一个遵循木桶理论的操作,需要综合考虑其它很多因素。进行优化时可以先计算下预期效果,运用Amdahl定律,公式为speedup(加速比) <= 1 /(F(串行化程序比重) + (1-F)/N )(处理器数量)。如果N趋近于无穷大,这时speedup = 1/F,所以程序优化时除了增加cpu来需要考虑并行化。
二、优化前准备
2.1、打印日志
修改jvm启动参数,打印日志,以下配置只有第一行配置是必须的,其它都可选
//开启的 JVM 启动参数如下
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
//滚动 GC 日志文件
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m
//GC 日志文件重定向到内存文件系统
-Xloggc:/dev/shm/mq_gc_%p.log
2.2、禁止.gc()方法
禁用的目的是防止人为因素的干扰(按道理来讲,gc()方法也应该在日常开发中被禁用)
-XX:+DisableExplicitGC
-Xnoclassgc:禁止gc时卸载类
三、JVM配置
整个jvm的配置或是调优主要就两大块内容,内存分配和垃圾回收器配置。多数情况下都是因为内存配置不合理引起的,只要内存设置合理很少出现需要修改垃圾回收器参数的情况。
3.1、内存配置
内存设置主要分为堆、栈、堆外内存三类,最重要的是堆内容的设置。详细如下:
3.1.1、堆和栈内存设置
###堆内内存设置,一般是设置ratio
-Xmx:设置堆最大值
-Xms:设置堆最小值
-Xmn:设置新生代大小,这个值过大会影响老年代大小, -XX:NewSize和-XX:MaxNewSize 设置初始新生代大小
-XX:NewRatio:新老比例
-XX:SurvivorRation:2个Survivor区和Eden区的比值,默认为8表示两个Survivor:Eden = 2:8 ,每个Survivor占 1/10
-server -Xms1500m -Xmx1500m -XX:SurvivorRatio=3(survivor=210/2=105M,eden=90M) -XX:NewRatio=4(新生代1/5=300m,老年代4/5=1200m)
###栈内存设置
-Xss:设置栈内存,这个值太大会影响并行线程数
###堆外内存设置
-XX:PermSize:设置永生代初始化大小, -XX:MaxPermSize最大大小
###禁用偏向锁定可能会减少 JVM 暂停,需要在无线程竞争的情况下
-XX:-UseBiasedLocking
3.1.2、META元数据配置
-XX:MetaspaceSize,初始空间大小(也是初始的阈值,即初始的high-water-mark):
达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的,取决于本地系统空间容量。
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比(即元数据在当前分配大小的最大占用大小)
如果空闲比小于这个参数(即超过了最大占用大小),那么将对meta space进行扩容。
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比(即元数据在当前分配大小的最小占用大小),
如果空闲比大于这个参数(即小于最小占用大小),那么将对meta space进行缩容.
3.2、垃圾回收器配置
3.2.1、Serial 垃圾收集器(单线程、复制算法)
适合比较简单的机器,效率会比并行的要好,新生代一般用复制,老年代一般用标记算法实现
-XX:+UseSerialGC:指定新和老生代都使用串行回收器
-XX:SurvivorRatio:设置eden和survivor区大小比例
-XX:PretenureSizeThreshold:设置大对象直接进入老年代的值
-XX:MaxTenuringThreshold:设置进入老年代的年龄,CMS默认是6,G1默认是15,在运行过程中会变化
- 设置过大:原本应该晋升对象到survivor区,直到survivor区溢出,一旦溢出发生,Eden+Svuvivor中对象将不再依据年龄全部提升到老年代,这样对象老化的机制就失效了.
- 设置过小:eden区的对象会陆续送入old区,对象移动本身就是开销,cms老年代回收还会造成碎片化。
- 这个参数一般与-XX:MaxTenuringThreshold=10 -XX:TargetSurvivorRatio=90 设定老年代阀值的上限为10,survivor空间目标使用率为90%
3.2.2、ParNew垃圾收集器(Serial+多线程)
-XX:+UseParNewGC:新生代并行,老年代串行
-XX:+UseParallelGC:新生代并行,老年代串行
-XX:+UseParallelOldGC:老新生代都使用并行
-XX:ParallelGCThreads:设置线程数,一般小于CPU核心数
-XX:MaxGCPauseMillis:设置最大STW时间
-XX:GCTimeRatio:设置吞量大小
-XX:+UseAdaptiveSizePolicy:GC自适应策略,它会自动调JVM堆参数;
3.2.3、CMS收集器(多线程标记清除算法)
可控制STW时间,采用标记清除算法
-XX:UseConcMarkSweepGC:新手代并行,老年代CMS
-XX:ParallelCMSThreads:设置线程数,也可以用(-XX:ParallelGCThreads+3)/4来代替
-XX:CMSInitiatingOccupancyFraction:触发CMS老年代的阈值,默认68%
-XX:+UseCMSCompactAtFullCollection:CMS-GC后是否进行一次碎片整理
-XX:CMSFullGCsBeforeCompaction:CMS-GC后是否进行一次内存压缩
-XX:+CMSClassUnloadingEnabled:是否回收元数据
-XX:CMSInitiatingPermOccupancyFraction:CMSClassUnloadingEnabled开启后触发的内存比率
3.2.4、G1回收器
采用标记压缩算法,不会产生碎片,同时可控制STW时间,
-XX:+UseG1GC:使用G1回收器
-XX:MaxGCPauseMillis:最大STW时间,如果值设置太小JVM 会使用一个小的年轻代来实现这个目标,这会导致非常频繁的 Minor GC
-XX:G1HeapRegionSize=16m
-XX:G1ReservePercent=25
-XX:InitiatingHeapOccupancyPercent=30
四、GC日志分析
gc调优最直接的方式是日志分析。所以首先要了解日志格式:
4.1、日志格式
4.1.1、young GC日志
4.1.2、full gc日志
4.2、分析过程
4.2.1、Young GC (Allocation Failure)
GC (Allocation Failure)造成的young gc。Allocation Failure表示向young generation(eden)给新对象申请空间,但是young generation(eden)剩余的合适空间不够所需的大小导致的minor gc。
4.2.1.1、-XX:+PrintTenuringDistribution
- age 年龄: 处于当前年龄段的对象大小 总大小(各个年龄段是累计的),
这个Desired survivor size表示survivor区域允许容纳的最大空间大小为75497472 bytes,这个值*10就是整个年轻化大小,*8=eden区大小;
Desired survivor size 75497472 bytes, new threshold 15 (max 15)
- age 1: 19321624 bytes, 19321624 total
- age 2: 79376 bytes, 19401000 total
- age 3: 2904256 bytes, 22305256 total
————————————————
一次 GC 后幸存对象大约 19 MB, 两次GC 后幸存对象大约79 KB , 三次GC 后幸存对象大约 3 MB .
每行结尾,显示直到本年龄全部对象大小.所以,最后一行的 total 表示幸存区To 总共被占用22 MB .
幸存区To 总大小为 75 MB ,当前老年代阀值为15,可以断定在本次GC中,没有对象会移动到老年代。
现在假设下一次GC 输出为:
Desired survivor size 75497472 bytes, new threshold 2 (max 15)
- age 1: 68407384 bytes, 68407384 total
- age 2: 12494576 bytes, 80901960 total
- age 3: 79376 bytes, 80981336 total
- age 4: 2904256 bytes, 83885592 total
age=1的对象占了68407384, age=2占了12494576,一直到age=4,共占了83885592大小。上述表示经过2次GC,部分对象进入了老年代
- 明显的,年龄2和年龄3 的对象还保持在幸存区中,因为我们看到年龄3和4的对象大小与前一次年龄2和3的相同。同时发现幸存区中,有一部分对象已经被回收,因为本次年龄2的对象大小为 12MB 最近的GC中,有68 MB 新对象,从伊甸园区移动到幸存区。本次GC 幸存区占用总大小 84 MB -大于75 MB. 结果,JVM 把老年代阀值从15降低到2,在下次GC时,一部分对象会强制离开幸存区,这些对象可能会被回收(如果他们刚好死亡)或移动到老年代。
4.2.1.2、age list为空
59.463: [GC (Allocation Failure)
Desired survivor size 134217728 bytes, new threshold 7 (max 15)
[PSYoungGen: 786432K->14020K(917504K)] 804872K->32469K(1966080K), 0.1116049 secs] [Times: user=0.10 sys=0.01, real=0.20 secs]
这里Desired survivor size这行下面并没有各个age对象的分布,那就表示此次gc之后,当前survivor区域并没有age小于max threshold的存活对象。
而这里一个都没有输出,表示此次gc回收对象之后,没有存活的对象可以拷贝到新的survivor区。
2016-08-23T02:23:07.219-02001: 64.3222:[GC3(Allocation Failure4) 64.322:
[ParNew5: 613404K->68068K6(613440K)7, 0.1020465 secs8] 10885349K->10880154K9(12514816K)10, 0.1021309 secs11]
[Times: user=0.78 sys=0.01, real=0.11 secs]12
开始的时候:整个堆的大小是 10885349K,年轻代大小是613404K,这说明老年代大小是 10885349-613404=10271945K,
收集完成之后:整个堆的大小是 10880154K,年轻代大小是68068K,这说明老年代大小是 10880154-68068=10812086K,
老年代的大小增加了:10812086-10271945=608209K,也就是说 年轻代到年老代promot了608209K的数据;
4.2.1.3、gc之后survivor有对象的例子
jstat -gcutil -h10 7 10000 10000
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 99.99 90.38 29.82 97.84 96.99 413 158.501 4 14.597 173.098
11.60 0.00 76.00 29.83 97.84 96.99 414 158.511 4 14.597 173.109
11.60 0.00 77.16 29.83 97.84 96.99 414 158.511 4 14.597 173.109
0.00 13.67 60.04 29.83 97.84 96.99 415 158.578 4 14.597 173.176
0.00 13.67 61.05 29.83 97.84 96.99 415 158.578 4 14.597 173.176
- 在ygc之前young generation = eden + S1;ygc之后,young generation = eden + S0
- 观察可以看到ygc之后old generation空间没变,表示此次ygc,没有对象晋升到old generation。
- gc之后,存活对象搬移到了另外一个survivor区域
- 这里由于是每个10秒采样一次,存在延迟,即gc之后,立马有新对象在eden区域分配了,因此这里看到的eden区域有对象占用。
4.2.2、Full/Major GC
1、2016-08-23T11:23:07.321-0200: 64.425: [GC (CMS Initial Mark)1 [1 CMS-initial-mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
/*2016-08-23T11:23:07.321-0200: 64.42 – GC事件开始,包括时钟时间和相对JVM启动时候的相对时间,下边所有的阶段改时间的含义相同;
CMS Initial Mark – 收集阶段,开始收集所有的GC Roots和直接引用到的对象;
10812086K – 当前老年代使用情况;
(11901376K) – 老年代可用容量;
10887844K – 当前整个堆的使用情况;
(12514816K) – 整个堆的容量;
0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] – 时间计量;
*/上面是第一次STOP the world
2016-08-23T11:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]
2016-08-23T11:23:07.357-0200: 64.460: [: 0.035/0.035 secs] [Times: user=0.07 sys=0.00, real=0.03 secs]
//035/0.035 secs – 展示该阶段持续的时间和时钟时间;
2016-08-23T11:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]
2016-08-23T11:23:07.373-0200: 64.476: [CMS-concurrent-preclean3: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2016-08-23T11:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]
2016-08-23T11:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean4: 0.167/1.074 secs] [Times: user=0.20 sys=0.00, real=1.07 secs]
//0.016/0.016 secs – 展示该阶段持续的时间和时钟时间;
2016-08-23T11:23:08.447-0200: 65.550: [GC (CMS Final Remark5)
//这个阶段是CMS中第二个并且是最后一个STW的阶段
[YG occupancy: 387920 K (613440 K)]65.550: [Rescan (parallel) , 0.0085125 secs]65.559:
[weak refs processing, 0.0000243 secs]65.559: [class unloading, 0.0013120 secs]65.560:
[scrub symbol table, 0.0008345 secs]65.561: [scrub string table, 0.0001759 secs][1 CMS-remark: 10812086K(11901376K)] 11200006K(12514816K), 0.0110730 secs]
[Times: user=0.06 sys=0.00, real=0.01 secs]
2016-08-23T11:23:08.447-0200: 65.550 – 同上;
CMS Final Remark – 收集阶段,这个阶段会标记老年代全部的存活对象,包括那些在并发标记阶段更改的或者新创建的引用对象;
YG occupancy: 387920 K (613440 K) – 年轻代当前占用情况和容量;
[Rescan (parallel) , 0.0085125 secs] – 这个阶段在应用停止的阶段完成存活对象的标记工作;
weak refs processing, 0.0000243 secs]65.559 – 第一个子阶段,随着这个阶段的进行处理弱引用;
class unloading, 0.0013120 secs]65.560 – 第二个子阶段(that is unloading the unused classes, with the duration and timestamp of the phase);
scrub string table, 0.0001759 secs – 最后一个子阶段(that is cleaning up symbol and string tables which hold class-level metadata and internalized string respectively)
10812086K(11901376K) – 在这个阶段之后老年代占有的内存大小和老年代的容量;
11200006K(12514816K) – 在这个阶段之后整个堆的内存大小和整个堆的容量;
0.0110730 secs – 这个阶段的持续时间;
[Times: user=0.06 sys=0.00, real=0.01 secs] – 同上;
2016-08-23T11:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start]
2016-08-23T11:23:08.485-0200: 65.588: [CMS-concurrent-sweep6: 0.027/0.027 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
2016-08-23T11:23:08.485-0200: 65.589: [CMS-concurrent-reset-start]
2016-08-23T11:23:08.497-0200: 65.601: [CMS-concurrent-reset7: 0.012/0.012 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
五、程序优化的常用手段
本小节列出常用的程序方面的优化方法:
- 缓冲:
- 缓存:
- 池:
- 并行替代串行:
- 负载均衡:
- 时间换空间&空间换时间:这种优化手段是要充分考虑机器配置。
5.1、java程序优化
其实就是强、弱引用等问题
5.1.1、String
1、对于大字符串少用subString(),而用下列代码代替:
public String getSubString(int begin, int end){
//这处返回了新的字符串,返回了一个新字符串,切断了原来的强引用,使内存更容易被回收;
return new String(str.substring(begin, end));
}
2、使用StringTokenizer代替原来的split,但影响是会占用大量的内存空间,好处是节省了时间
public static void Stringsplid(String str){
StringTokenizer st = new StringTokenizer(str, ",");
while (st.hasMoreTokens()){
System.out.println(st.nextToken());
}
}
3、用多个charAt()代替startWidth(),效率大概高一倍左右吧
if (orgStr.charAt(0) == 'a' && orgStr.charAt(1) == 'b')
4、对超大string操作或者是循环操作时,也会涉及到+和stringbuilder的选择,少量的数据时JVM会自动优化成StringBuilder。
//无线程要求的前提下用StringBuilder,在有线程安全要求的前提下使用StringBuffer.
5、指定StringBuilder和StringBuffer需要指定容量,防止频繁扩容,默认大小是16字节
5.1.2、编程技巧
- 用nio代替io
- 4种引用类型使用
- 局部变量代替全局变量(提速)
- 位运算代替四则运算
- 轻量级的对象用clone()代替new,因为clone会跳过构造函数
5.1.3、线程
- 线程池大小经验公式: N = Ncpu * Ucpu * (1+W/C)
- Ncpu:cpu的数量
- Ucpu:目标cpu的使用率
- W/C:等待时间与计算时间的比率;
ThreadPool:最简单的线程池,没大用
Executor框架:固化了4种不同类型的线程池
ThreadPoolExecutor:自定义线程池
-XX:-UseBiasedLocking:偏向锁(如果程序没有竞争,则取消之前已经取得锁的线程同步操作),大多数时应该关掉此参数
协程:这是一个概念,其实就是线程的分割,提高并行度(类似进程分割成线程),也可一个Kilim的框架