【JVM】总结篇之GC性能优化案例
文章目录
- 性能优化案例1:调整堆大小提高服务的吞吐量
- 初始配置
- 优化配置
- 性能优化案例2:JVM优化之JIT优化
- 即时编译对代码的优化
- 逃逸分析
- 编译器优化
- 栈上分配
- 同步省略
- 标量替换
- 性能优化案例3:合理配置堆内存
- 推荐配置
- 如何计算老年代存活对象
- 结论
- 你会估算GC频率吗?
- 特殊问题:新生代与老年代的比例
- 性能优化案例4:CPU占用很高排查方案
- 性能优化案例5:G1并发执行的线程数对性能的影响
- 初始状态
- 优化
- 性能优化案例6:调整垃圾回收器提高服务的吞吐量
- 性能优化案例7:日均百万级订单交易系统如何设置JVM参数
- 小结
常用的性能优化方式有哪些?(百度金融)
虚拟机如何调优?(顺丰)
内存调优怎么调?有几种方式?(顺丰)
栈溢出导致的原因?如何解决?(搜狐)
JVM调优策略 (杭州鲁尔物联科技有限公司、燕梭金融、汇博云通)
如何优化减少Full GC?(阿里-闲鱼)
当出现了内存溢出,你怎么排错。 (京东)
有实际的JVM性能调优案例吗?重点需要关注哪些核心参数? (滴滴)
OOM说一下?怎么排查?哪些会导致OOM? OOM出现在什么时候 (腾讯)
JVM性能调优都做了什么?(支付宝)
有做过JVM内存优化吗? (小米)
JVM的编译优化 (蚂蚁金服)
JVM性能调优都做了什么 (蚂蚁金服)
JVM怎样调优,堆内存栈空间设置多少合适… (蚂蚁金服)
JVM相关的分析工具使用过的有哪些?具体的性能调优步骤如何 (蚂蚁金服)
如何进行JVM调优?有哪些方法? (阿里)
JVM如何调优、参数怎么调? (字节跳动)
每秒几十万并发的秒杀系统为什么会频繁发生GC? (京东)
日均百万级交易系统如何优化JVM? (京东)
线上生产系统OOM如何监控及定位与解决? (京东)
高并发系统如何基于G1垃圾回收器优化性能? (京东)
性能优化案例1:调整堆大小提高服务的吞吐量
Tomcat案例 生产环境下,Tomcat并不建议直接在catalina.sh里配置变量,而是写在与catalina同级目录(bin目录)下的setenv.sh里。所以如果我们想要修改jvm的内存配置,那么我们就需要修改setenv.sh文件(默认没有,需新建一个setenv.sh)。
初始配置
setenv.sh文件中写入(大小根据自己情况修改):setenv.sh内容如下:
export CATALINA_OPTS="$CATALINA_OPTS -Xms30m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:SurvivorRatio=8"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx30m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseParallelGC"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/opt/tomcat8.5/logs/gc.log"
我们查看日志信息:
其中存在大量的Full GC日志,查看一下我们Jmeter汇总报告
吞吐量是866.9/sec
优化配置
export CATALINA_OPTS="$CATALINA_OPTS -Xms120m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:SurvivorRatio=8"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx120m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseParallelGC"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/opt/tomcat8.5/logs/gc.log"
重新启动tomcat,查看gc.log
vi gc.log
查找Full关键字,发现只有一处FullGC,如下图所示,我们可以看到,增大了初始化内存和最大内存之后,我们的Full次数有一个明显的减少。
查看Jmeter汇总报告,如下图所示:吞吐量变成了1142.1/sec,基本上是有一个明显的提升,这就说明,我们增加内存之后,服务器的性能有一个明显的提升,这就是我们本次案例的的演示。
性能优化案例2:JVM优化之JIT优化
即时编译对代码的优化
逃逸分析
如何将堆上的对象分配到栈,需要使用逃逸分析手段。
逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域:
当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
public void my_method() {
V v = new V();
//use v
//......
v = null;
}
没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。
逃逸分析包括:
- 全局变量赋值逃逸
- 方法返回值逃逸
- 实例引用发生逃逸
- 线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量
/**
* 逃逸分析
*/
public class EscapeAnalysis {
public EscapeAnalysis obj;
/*
方法返回EscapeAnalysis对象,发生逃逸
*/
public EscapeAnalysis getInstance(){
return obj == null?
new EscapeAnalysis() : obj;
}
/*
为成员属性赋值,发生逃逸
*/
public void setObj(){
this.obj = new EscapeAnalysis();
}
/*
对象的作用域仅在当前方法中有效,没有发生逃逸
*/
public void useEscapeAnalysis(){
EscapeAnalysis e = new EscapeAnalysis();
}
/*
引用成员变量的值,发生逃逸
*/
public void useEscapeAnalysis1(){
EscapeAnalysis e = getInstance();
}
}
参数设置:
在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析。
如果使用的是较早的版本,开发人员则可以通过:
通过选项“-XX:+DoEscapeAnalysis”显式开启逃逸分析
通过选项“-XX:+PrintEscapeAnalysis”查看逃逸分析的筛选结果。
结论:
开发中能使用局部变量的,就不要使用在方法外定义。
编译器优化
栈上分配
将堆分配转化为栈分配。如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。可以减少垃圾回收时间和次数。
/**
* 栈上分配测试
* -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*/
public class StackAllocation {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
// 查看执行时间
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
// 为了方便查看堆内存中对象个数,线程sleep
try {
Thread.sleep(100000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();
}
static class User {
}
}
未开启逃逸分析时,可以看到堆空间创建的对象数量:
同步省略
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
标量替换
public static void main(String[] args) {
alloc();
}
private static void alloc() {
Point point = new Point(1,2);
System.out.println("point.x="+point.x+"; point.y="+point.y);
}
class Point{
private int x;
private int y;
}
以上代码,经过标量替换后,就会变成:
private static void alloc() {
int x = 1;
int y = 2;
System.out.println("point.x="+x+"; point.y="+y);
}
可以看到,Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个标量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。
标量替换为栈上分配提供了很好的基础。
参数-XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上。
public class ScalarReplace {
public static class User {
public int id;
public String name;
}
public static void alloc() {
User u = new User();
u.id = 5;
u.name = "www.atguigu.com";
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
上述代码在主函数中进行了 1亿次alloc。调用进行对象创建,由于User对象实例需要占据约16字节的空间,因此累计分配空间达到将近1.5GB。如果堆空间小于这个值,就必然会发生 GC。使用如下参数运行上述代码:
-server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
这里使用参数如下:
参数-server:启动Server模式,因为在Server模式下,才可以启用逃逸分析。
参数 -XX:+DoEscapeAnalysis:启用逃逸分析
参数-Xmx100m:指定了堆空间最大为10MB
参数-XX:+PrintGC:将打印GC 日志。
参数-XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上, 比如对象拥有id和name两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配。
性能优化案例3:合理配置堆内存
推荐配置
如何计算老年代存活对象
方式1:查看日志
JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)。
方式2:强制触发FullGC
-XX:+PrintGCDetails -XX:MetaspaceSize=64m -Xss512K -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapdump3.hprof -XX:SurvivorRatio=8 -XX:+PrintGCDateStamps -Xms1024M -Xmx1024M -Xloggc:log/gc-oom3.log
项目启动,通过jmeter访问10000次(主要是看项目是否可以正常运行)之后,查看gc状态
jstat -gc pid
YGC平均耗时: 0.12s * 1000/7 = 17.14ms
FGC未产生
看起来似乎不错,YGC触发的频率不高,FGC也没有产生,但这样的内存设置是否还可以继续优化呢?是不是有一些空间是浪费的呢。
为了快速看数据,我们使用了方式2,通过命令 jmap -histo:live pid 产生几次FullGC,FullGC之后,使用的jmap -heap 来看的当前的堆内存情况。
观察老年代存活对象大小:
jmap -heap pid
或者直接查看GC日志
查看一次FullGC之后剩余的空间大小
可以看到存活对象占用内存空间大概13.36M,老年代的内存占用为683M左右。 按照整个堆大小是老年代(FullGC)之后的3-4倍计算的话,设置堆内存情况如下:
Xmx=14 * 3 = 42M 至 14 * 4 = 56M 之间
我们修改堆内存状态如下:
-XX:+PrintGCDetails -XX:MetaspaceSize=64m -Xss512K -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapdump.hprof -XX:SurvivorRatio=8 -XX:+PrintGCDateStamps -Xms60M -Xmx60M -Xloggc:log/gc-oom.log
修改完之后,我们查看一下GC状态
请求之后
YGC平均耗时: 0.195s * 1000/68 = 2.87ms
FGC未产生
整体的GC耗时减少。但GC频率比之前的1024M时要多了一些。依然未产生FullGC,所以我们内存设置为60M 也是比较合理的,相对之前节省了很大一块内存空间,所以本次内存调整是比较合理的
依然手动触发Full ,查看堆内存结构
结论
你会估算GC频率吗?
正常情况我们应该根据我们的系统来进行一个内存的估算,这个我们可以在测试环境进行测试,最开始可以将内存设置的大一些,比如4G这样,当然这也可以根据业务系统估算来的。
比如从数据库获取一条数据占用128个字节,需要获取1000条数据,那么一次读取到内存的大小就是(128 B/1024 Kb/1024M)* 1000 = 0.122M ,那么我们程序可能需要并发读取,比如每秒读取100次,那么内存占用就是0.122100 = 12.2M ,如果堆内存设置1个G,那么年轻代大小大约就是333M,那么333M80% / 12.2M =21.84s ,也就是说我们的程序几乎每分钟进行两到三次youngGC。这样可以让我们对系统有一个大致的估算。
0.122M * 100 = 12.2M /秒 —Eden区
1024M * 1/3 * 80% = 273M
273 / 12.2M = 22.38s —> YGC 每分钟2-3次YGC
特殊问题:新生代与老年代的比例
# 开启:
-XX:+UseAdaptiveSizePolicy
# 关闭
-XX:-UseAdaptiveSizePolicy
关于堆内存的自适应调节有如下三个参数:调整堆是按照每次20%增长,按照每次5%收缩
young区增长量(默认20%):-XX:YoungGenerationSizeIncrement=
old区增长量(默认20%):-XX:TenuredGenerationSizeIncrement=
收缩量(默认5%):-XX:AdaptiveSizeDecrementScaleFactor=
性能优化案例4:CPU占用很高排查方案
CPU一直处于一个比较高的占用率,我们解决问题的思路应该是:
1、首先查看java进程ID
2、根据进程 ID 检查当前使用异常线程的pid
3、把线程pid变为16进制如 31695 -> 7bcf 然后得到0x7bcf
4、jstack 进程的pid | grep -A20 0x7bcf 得到相关进程的代码
(鉴于我们当前代码量比较小,线程也比较少,所以我们就把所有的信息全部导出来)
所有的准备工作已经完成,我们接下来分析日志中的信息,来定位问题出在哪里。
打开jstack.log文件 查找一下刚刚我们转换完的16进制ID是否存在
jstack命令生成的thread dump信息包含了JVM中所有存活的线程,里面确实是存在我们定位到的线程 ID ,在thread dump中每个线程都有一个nid,在nid=0x5b9的线程调用栈中,我们发现两个线程在互相等待对方释放资源
到此就可以检查对应的代码是否有问题,也就定位到我们的死锁问题。
性能优化案例5:G1并发执行的线程数对性能的影响
8核linux
初始状态
优化
增加线程配置:
export CATALINA_OPTS=“$CATALINA_OPTS -XX:ConcGCThreads=8”
配置完线程数之后,我们的请求的平均响应时间和GC时间都有一个明显的减少了,仅从效果上来看,我们这次的优化是有一定效果的。大家在工作中对于线上项目进行优化的时候,可以考虑到这方面的优化。
性能优化案例6:调整垃圾回收器提高服务的吞吐量
系统配置是单核,我们看到日志,显示DefNew,说明我们用的是串行收集器,SerialGC
那么就考虑切换一下并行收集器是否可以提高性能,增加配置如下:
export CATALINA_OPTS="$CATALINA_OPTS -Xms60m"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx60m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseParallelGC"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/opt/tomcat8.5/logs/gc6.log"
查看GC状态:
发生3次FullGC,可以接受
查看吞吐量,997.6/sec,吞吐量并没有明显变化,我们究其原因,本身UseParallelGC是并行收集器,但是我们的服务器是单核。
接下来我们把服务器改为8核。
8核状态下的性能表现如下,吞吐量大幅提升,甚至翻了一倍,这说明我们在多核机器上面采用并行收集器对于系统的吞吐量有一个显著的效果。
接下来我们改为G1收集器看看效果
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
export CATALINA_OPTS="$CATALINA_OPTS -Xms60m"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx60m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/opt/tomcat8.5/logs/gc6.log"
查看GC状态:
没有产生FullGC,效果较之前有提升。
查看压测效果,吞吐量也是比串行收集器效果更佳,而且没有了FullGC。此次优化较为成功。
性能优化案例7:日均百万级订单交易系统如何设置JVM参数
一天百万级订单这个绝对是现在顶尖电商公司交易量级,百万订单一般在4个小时左右产生,我们计算一下每秒产生多少订单,3000000/3600/4 = 208.3单/s,我们大概按照每秒300单来计算。
这种系统我们一般至少要三四台机器去支撑,假设我们部署了三台机器,也就是每台每秒钟大概处理完成100单左右,也就是每秒大概有100个订单对象在堆空间的新生代内生成,一个订单对象的大小跟里面的字段多少及类型有关,比如int类型的订单id和用户id等字段,double类型的订单金额等,int类型占用4字节,double类型占用8字节,初略估计下一个订单对象大概1KB左右,也就是说每秒会有100KB的订单对象分配在新生代内。
真实的订单交易系统肯定还有大量的其他业务对象,比如购物车、优惠券、积分、用户信息、物流信息等等,实际每秒分配在新生代内的对象大小应该要再扩大几十倍,我们假设20倍,也就是每秒订单系统会往新生代内分配近2M的对象数据,这些数据一般在订单提交完的操作做完之后基本都会成为垃圾对象。
假设我们选择4核8G的服务器,就可以给JVM进程分配四五个G的内存空间,那么堆内存可以分到三四个G左右,于是可以给新生代至少分配1G,这样算下差不多需要10分钟左右才能把新生代放满触发minor gc,这样的GC频率我们是可以接受的。我们还可以继续调整young区大小。不一定是1:2,这样就可以降低GC频率。这样进入老年代的对象也会降低,减少Full GC频率。
如果系统业务量继续增长那么可以水平扩容增加更多的机器,比如五台甚至十台机器,这样每台机器的JVM处理请求可以保证在合适范围,不至于压力过大导致大量的gc。
假设业务量暴增几十倍,在不增加机器的前提下,整个系统每秒要生成几千个订单,之前每秒往新生代里分配的1M对象数据可能增长到几十M,而且因为系统压力骤增,一个订单的生成不一定能在1秒内完成,可能要几秒甚至几十秒,那么就有很多对象会在新生代里存活几十秒之后才会变为垃圾对象,如果新生代只分配了几百M,意味着一二十秒就会触发一次minor gc,那么很有可能部分对象就会被挪到老年代,这些对象到了老年代后因为对应的业务操作执行完毕,马上又变为了垃圾对象,随着系统不断运行,被挪到老年代的对象会越来越多,最终可能又会导致full gc,full gc对系统的性能影响还是比较大的。
问如何进行服务器配置只是第一个层面的问题!
第二个层面问题:如果要求响应时间控制在100ms如何实现? 压测实验调出
小结
问题一:有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器是32位的,1.5G的堆,用户反馈网站比较缓慢。因此公司决定升级,新的服务器为64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了!
-
为什么原网站慢?
频繁的GC,STW时间比较长,响应时间慢! -
为什么会更卡顿?
内存空间越大,FGC时间更长,延迟时间更长 -
咋办?
垃圾回收器:parallel GC ; ParNew + CMS ; G1
配置GC参数:-XX:MaxGCPauseMillis 、 -XX:ConcGCThreads
根据log日志、dump文件分析,优化内存空间的比例
jstat jinfo jstack jmap
问题二:系统CPU经常100%,如何调优?(面试高频)
CPU100%的话,一定是有线程占用系统资源。具体步骤前面已经讲过。
注意: 工作中有时候是工作线程100%占用了CPU,还有可能是垃圾回收线程占用了100%
问题三:系统内存飙高,如何查找问题?(面试高频)
一方面:jmap -heap 、jstat 、… ; gc日志情况
另一方面:dump文件分析
问题四:如何监控JVM
命令行工具
图形化界面工具