前言

视频教程:https://www.bilibili.com/video/BV1Dz4y1A7FB?p=2&vd_source=85ac5ee1b07df12a44b648a8751d30f6

相关知识点

优化案例1:调整堆大小提高服务的吞吐量

调整方法

修改tomcatJVM配置。

生产环境下,Tomcat并不建议直接在catalina.sh里配置变量,而是写在与catalina同级目录(bin目录)下的setenv.sh里。

1
2
3
4
5
6
7
8
9
修改参数
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"

性能优化案例2:JVM优化之JIT优化

堆,是分配对象的唯一选择吗

如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。

前提:逃逸分析

逃逸分析的基本行为就是分析对象动态作用域:

  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

逃逸分析包括:

  • 全局变量赋值逃逸
  • 方法返回值逃逸
  • 实例引用发生逃逸
  • 线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量

参数设置

在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析。
如果使用的是较早的版本,开发人员则可以通过:
通过选项“-XX:+DoEscapeAnalysis”显式开启逃逸分析
通过选项“-XX:+PrintEscapeAnalysis”查看逃逸分析的筛选结果。

逃逸分析存在的问题

是无法保证非逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。

方法:栈上分配

JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。

方法:同步省略(消除)

同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

  • 线程同步的代价是相当高的,同步的后果是降低并发性和性能。
  • 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。

方法:标量替换

标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。

相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

参数设置

参数-XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上。

性能优化案例3:合理配置堆内存

堆大小设置原则

Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。
方法区(永久代 PermSize和MaxPermSize 或 元空间 MetaspaceSize 和 MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍。
年轻代Xmn的设置为老年代存活对象的1-1.5倍。
老年代的内存大小设置为老年代存活对象的2-3倍。

如何计算老年代存活对象

  1. JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)。

  2. 强制触发一次FullGC,来观察FullGC之后的老年代存活对象大小。

    • 如何强制触发Full GC?
      • jmap -dump:live,format=b,file=heap.bin <pid> 将当前的存活对象dump到文件,此时会触发FullGC
      • jmap -histo:live 打印每个class的实例数目,内存占用,类全名信息.live子参数加上后,只统计活的对象数量. 此时会触发FullGC
      • 在性能测试环境,可以通过Java监控工具来触发FullGC,比如使用VisualVM和JConsole,VisualVM集成了JConsole,VisualVM或者JConsole上面有一个触发GC的按钮

新生代与老年代的比例

JDK 1.8 默认使用 UseParallelGC 垃圾回收器,该垃圾回收器默认启动了 AdaptiveSizePolicy,会根据GC的情况自动计算计算 Eden、From 和 To 区的大小;所以这是由于JDK1.8的自适应大小策略导致的

开启:-XX:+UseAdaptiveSizePolicy

关闭:-XX:-UseAdaptiveSizePolicy

如果不想动态调整内存大小,以下是解决方案:

1、保持使用 UseParallelGC,显式设置 -XX:SurvivorRatio=8。

2、使用 CMS 垃圾回收器。CMS 默认关闭 AdaptiveSizePolicy。配置参数 -XX:+UseConcMarkSweepGC

性能优化案例4:CPU占用很高排查方案

排查方案

方案一:

  1. 首先查看java进程ID
  2. 根据进程 ID 检查当前使用异常线程的pid
  3. 把线程pid变为16进制如 31695 -> 7bcf 然后得到0x7bcf
  4. jstack 进程的pid | grep -A20 0x7bcf 得到相关进程的代码 (鉴于我们当前代码量比较小,线程也比较少,所以我们就把所有的信息全部导出来)
  5. jps -l。 # 查看所有java进程 ID
  6. top -Hp 1456。 # 根据进程 ID 检查当前使用异常线程的pid

方案二

1-3同上

  1. ps aux | grep java 查看到当前java进程使用cpu、内存、磁盘的情况获取使用量异常的进程
  2. top -Hp 进程pid 检查当前使用异常线程的pid
  3. 把线程pid变为16进制如 31695 - 》 7bcf 然后得到0x7bcf
  4. jstack 进程的pid | grep -A20 0x7bcf 得到相关进程的代码

性能优化案例5:G1并发执行的线程数对性能的影响

对于G1GC来说,增加线程数之后,我们的请求的平均响应时间和GC时间都有一个明显的减少了,仅从效果上来看,我们这次的优化是有一定效果的。大家在工作中对于线上项目进行优化的时候,可以考虑到这方面的优化。

性能优化案例6:调整垃圾回收器提高服务的吞吐量

性能优化案例7:日均百万级订单交易系统如何设置JVM参数

一天百万级订单这个绝对是现在顶尖电商公司交易量级,百万订单一般在4个小时左右产生,我们计算一下每秒产生多少订单,3000000/3600/4 = 208.3单/s,我们大概按照每秒300单来计算。

image-20240504163657635

种系统我们一般至少要三四台机器去支撑,假设我们部署了三台机器,也就是每台每秒钟大概处理完成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频率。

image-20240504163711819

如果系统业务量继续增长那么可以水平扩容增加更多的机器,比如五台甚至十台机器,这样每台机器的JVM处理请求可以保证在合适范围,不至于压力过大导致大量的gc。

假设业务量暴增几十倍,在不增加机器的前提下,整个系统每秒要生成几千个订单,之前每秒往新生代里分配的1M对象数据可能增长到几十M,而且因为系统压力骤增,一个订单的生成不一定能在1秒内完成,可能要几秒甚至几十秒,那么就有很多对象会在新生代里存活几十秒之后才会变为垃圾对象,如果新生代只分配了几百M,意味着一二十秒就会触发一次minor gc,那么很有可能部分对象就会被挪到老年代,这些对象到了老年代后因为对应的业务操作执行完毕,马上又变为了垃圾对象,随着系统不断运行,被挪到老年代的对象会越来越多,最终可能又会导致full gc,full gc对系统的性能影响还是比较大的。

面试题

常用的性能优化方式有哪些?(百度金融)
虚拟机如何调优?(顺丰)
内存调优怎么调?有几种方式?(顺丰)
栈溢出导致的原因?如何解决?(搜狐)
JVM调优策略 (杭州鲁尔物联科技有限公司、燕梭金融、汇博云通)
如何优化减少Full GC?(阿里-闲鱼)
当出现了内存溢出,你怎么排错。 (京东)
有实际的JVM性能调优案例吗?重点需要关注哪些核心参数? (滴滴)
OOM说一下?怎么排查?哪些会导致OOM? OOM出现在什么时候 (腾讯)
JVM性能调优都做了什么?(支付宝)
有做过JVM内存优化吗? (小米)
JVM的编译优化 (蚂蚁金服)
JVM性能调优都做了什么 (蚂蚁金服)
JVM怎样调优,堆内存栈空间设置多少合适… (蚂蚁金服)
JVM相关的分析工具使用过的有哪些?具体的性能调优步骤如何 (蚂蚁金服)
如何进行JVM调优?有哪些方法? (阿里)
JVM如何调优、参数怎么调? (字节跳动)
每秒几十万并发的秒杀系统为什么会频繁发生GC? (京东)
日均百万级交易系统如何优化JVM? (京东)
线上生产系统OOM如何监控及定位与解决? (京东)
高并发系统如何基于G1垃圾回收器优化性能? (京东)

1
2
3
4
```

**12306遭遇春节大规模抢票如何支撑?**

12306号称是国内并发量最大的秒杀网站,并发量达到百万级别。

普通电商订单–> 下单 --> 订单系统(IO)减库存 --> 等待用户付款

12306一种可能的模型:下单 --> 减库存和订单(redis、kafka)同时异步进行 --> 等付款
但减库存最后还会把压力压到一台服务器上。如何?

分布式本地库存+单独服务器做库存均衡!

1
2
3

**有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器是32位的,1.5G的堆,用户反馈网站比较缓慢。因此公司决定升级,新的服务器为64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了!**

  1. 为什么原网站慢?
    频繁的GC,STW时间比较长,响应时间慢!

  2. 为什么会更卡顿?
    内存空间越大,FGC时间更长,延迟时间更长

  3. 咋办?

垃圾回收器:parallel GC ; ParNew + CMS ; G1
配置GC参数:-XX:MaxGCPauseMillis 、 -XX:ConcGCThreads
根据log日志、dump文件分析,优化内存空间的比例
jstat jinfo jstack jmap

1
2
3

**系统CPU经常100%,如何调优?(面试高频)**

(同 性能优化案例4:CPU占用很高排查方案)
CPU100%的话,一定是有线程占用系统资源。具体步骤前面已经讲过。
注意: 工作中有时候是工作线程100%占用了CPU,还有可能是垃圾回收线程占用了100%

1
2
3

**系统内存飙高,如何查找问题?(面试高频)**

一方面:jmap -heap 、jstat 、… ; gc日志情况
另一方面:dump文件分析


**如何监控JVM**

1
2
> 命令行工具
> 图形化界面工具