JVM性能调优
一、概念
1.生产环境中的问题
- 生产环境发生了内存溢出该如何处理?
- 生产环境应该给服务器分配多少内存合适?
- 如何对垃圾回收器的性能进行调优?
- 生产环境CPU负载飙高该如何处理?
- 生产环境应该给应用分配多少线程合适?
- 不加log,如何确定请求是否执行了某一行代码?
- 不加log,如何实时查看某个方法的入参与返回值?
2.调优的基本问题
- 为什么要调优
- 防止出现OOM,进行JVM规划和预调优
- 解决程序运行中各种OOM
- 减少Full GC出现的频率,解决运行慢、卡顿问题
- 调优大方向
- 合理地编写代码
- 充分并合理的使用硬件资源
- 合理地进行JVM调优
- 不同阶段的考虑
- 上线前
- 项目运行阶段
- 线上出现O0M
- 两句话
- 调优,从业务场景开始,没有业务场景的调优都是耍流氓!
- 无监控,不调优!
3.调优监控的依据
- 运行日志
- 异常堆栈
- GC日志
- 线程快照
- 堆转储快照
4.性能优化步骤
- 第1步:熟悉业务场景
- 第2步(发现问题):性能监控
0. 一种以非强行或者入侵方式收集或查看应用运营性能数据的活动。监控通常是指一种在生产、质量评估或者开发环境下实施的带有预防或主动性的活动。当应用相关干系人提出性能问题却没有提供足够多的线索时,首先我们需要进行性能监控,随后是性能分析。监控前,设置好回收器组合,选定CPU(主频越高越好),设置年代比例,设置日志参数(生产环境中通常不会只设置一个日志文件)。比如:- -Xloggc:/opt/xxx/logs/xxx-×xx-gc-%t.log -XX:+UseGCLogFileRotation
- -XX:NumberofGCLogFiles=5 -XX:GCLogFileSize=20M -XX: +PrintGCDetails
- -XX:+PrintGCDateStamps -xX:+PrintGCCause
- GC 频繁
- cpu load过高
- OOM
- 内存泄漏
- 死锁
- 程序响应时间较长
- 第3步(排查问题):性能分析
- 打印GC日志,通过GCviewer或者http://gceasy.io来分析日志信息
- 灵活运用命令行工具,jstack,jmap, jinfo等
- dump出堆文件,使用内存分析工具分析
- 使用阿里Arthas,或jconsole,JVisuaVM来实时查看JVM
- jstack查看堆栈信息
- 第4步(解决问题):性能调优
- 适当增加内存,根据业务背景选择垃圾回收器
- 优化代码,控制内存使用
- 增加机器,分散节点压力
- 合理设置线程池线程数量
- 使用中间件提高程序效率,比如绶存,消息队列等
- 其他…
5.性能评价/测试指标
二、OOM案例
一、堆溢出
- 报错信息:java.lang.OutOfMemoryError: Java heap space
- 案例模拟:
- 使用while不停new对象
- JVM参数配置:
- 参数配置:初始-Xms30M
- -Xmx30M
- -XX:+PrintGCDetails -XX:MetaspaceSize=64m:打印详细信息,元空间大小:64MB
- -XX:+HeapDumponoutofMemoryError - XX:HeapDumpPath=heap/heapdump.hprof:
- -XX:+PrintGcDatestamps - Xms200M - Xmx200M -X1oggc:1og/gc-oomHeap.1og
- 运行结果
- 原因及解决方案
- Dump文件分析 :
- Java VisualVM读取dump文件
- Memory Analyzet 读取dump文件
- gc日志分析
二、元空间溢出
- 元空间数据类型
- 方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
- Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定
- JDK8后,元空间替换了永久代,元空间使用的是本地内存
- 原因:
- 运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
- 应用长时间运行,没有重启
- 元空间内存设置过小
- 解决方法:
- 因为该 O0M 原因比较简单,解决方法有如下几种:
- 检查是否永久代空间或者元空间设置的过小
- 检查代码中是否存在大量的反射操作
- dump之后通过mat检查是否存在大量由于反射生成的代理类
- JVM参数
- -XX:+PrintGCDetails -XX: MetaspaceSize=60m -XX: MaxMetaspaceSize=60m
- -Xss512K -XX:+HeapDumpOnOutofMemoryError
- -XX:HeapDumpPath=heap/heapdumpMeta.hprof -XX: SurvivorRatio=8
- -XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:+PrintGCDateStamps
- -Xms60M -Xmx60M -Xloggc:log/gc-oomMeta.log
- 分析及解决
- 查看监控
- 查看GC状态
-
$ jstat -gc pid 1000 10 # 每1000毫米打印10次
-
- 查看GC日志
- 分析dump文件
- 解决方案
- 那么我们可以想一下解决方案,每次是不是可以只加载一个代理类即可,因为我们的需求其实是没有必要如此加载的,当然如果业务上确实需要加载很多类的话,那么我们就要考虑增大方法区大小了,所以我们这里修改代码如下:
-
enhancer.setUseCache(true); // 设置为true的话,相同的类可以共用,不用每个类加载器都去加载一遍
- enhancer.setuseCache(false),选择为true的话,使用和更新一类具有相同属性生成的类的静态缓存,而不会在同一个类文件还继续被动态加载并视为不同的类,这个其实跟类的equals()和hashCode()有关,它们是与cglib内部的class cache的key相关的。
三、GC overhead limit exceeded(回收效率过低警告)
- 案例模拟
- 示例一:
- JVM配置:
- -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=heap/dumpExceeded.hprof
- -xx:+PrintGCDatestamps - Xms10M -Xmx10M
- -Xloggc :log/ gc- oomExceeded. log
- JVM配置:
- 示例一:
- 代码解析
- 第一段代码:运行期间将内容放入常量池的典型案例
- intern()方法
- 如果字符串常量池里面已经包含了等于字符串X的字符串,那么就返回常量池中这个字符串的引用;
- 如果常量池中不存在,那么就会把当前字符串添加到常量池并返回这个字符串的引用
- 第二段代码:不停的追加字符串str
- 你可能会疑惑,看似demo也没有差太多,为什么第二个没有报GC
- overhead limit exceeded呢?以上两个demo的区别在于:
- Java heap space的demo每次都能回收大部分的对象(中间产生的UUID),只不过有一个对象是无法回收的,慢慢长大,直到内存溢出
- GC overhead limit exceeded的demo由于每个字符串都在被list引用,所以无法回收,很快就用完内存,触发不断回收的机制。
- 第一段代码:运行期间将内容放入常量池的典型案例
- 分析及解决
- 定位问题代码块:
- jvisualvm分析
- MAT分析
- 分析dump文件直方图
- 看到发生OOM是因为进行了死循环,不停的往 ArrayList 存放字符串常量,JDK1.8以后,字符串常量池移到了堆中存储,所以最终导致内存不足发生了OOM。
- 打开Histogram,可以看到,String类型的字符串占用了大概8M的空间,几乎把堆占满,但是还没有占满,所以这也符合Sun 官方对此的定义:** **,本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出。
- 代码修改
- 根据业务来修改是否需要死循环。
- 原因:
这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做Gc并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出
- 原因:
- 解决方法:
- 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
- 添加参数〝-xX:-UseGCOverheadLimit°禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java. lang. OutofMemoryError: Java heapspaceo
- dump内存,检查是否存在内存泄漏,如果没有,加大内存。
- 根据业务来修改是否需要死循环。
- 定位问题代码块:
四、线程溢出
- 报错信息 : java.lang.outofMemoryError:unable to create new native Thread
- 问题原因:出现这种异常,基本上都是创建了大量的线程导致的
- 栈设置为512k的时候,大概创建了1w5线程报异常,但是设置为1024,依然是这么多线程
- 因为线程使用的是计算机总的内存大小,系统会设置限制,不然64位操作系统可以允许创建非常多线程,会将资源占满
- 案例模拟
1. - 分析及解决
- 解决方向一:
- 通过 -Xss设置每个线程栈大小的容量
- JDk5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。
- 正常情况下,在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
- 能创建的线程数的具体计算公式如下:
- (MaxProcessMemory - JVMMemory - ReservedosMemory) / (ThreadStackSize)=Number of threads
- MaxProcessMemory 指的是进程可寻址的最大空间
- JVMMemory :JVM内存
- ReservedosMemory 保留的操作系统内存
- ThreadStackSize : 线程栈的大小
- 在Java语言里,当你创建一个线程的时候,虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程,而这个系统线程的内存用的不是JVMMemory, 而是系统中剩下的内存(MaxProcessMemory - JVMMemory -ReservedOsMemory)。
- 由公式得出结论:你给JVM内存越多,那么你能创建的线程越少,越容易发生java. lang.OutofMemoryError: unable to create new native thread
- 问题解决:
- 如果程序中有bug,导致创建大量不需要的线程或者线程没有及时回收,那么必须解决这个bug:修改参数是不能解决问题的。
- 如果程序确实需要大量的线程,现有的设置不能达到要求,那么可以通过修改MaxProcessMemory, JVMMemory, ThreadstackSize这三个因素,来增加能创建的线程
- MaxProcessMemory 使用64位操作系统
- JVMMemory 减少JVMMemory的分配
- ThreadstackSize 减小单个线程的栈大小
- 经实测,在32位windows系统下较为严格遵守;64位系统下只能保证正/负相关性,甚至说相关性也不能保证。即:在测试的过程中,64位操作系统下调整Xss的大小并没有对产生线程的总数产生影响,程序执行到极限的时候,操作系统会死机。无法看出效果
- 解决方向二:
- 线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:
- /proc/sys/kernel/pid_max :系统最大pid值,在大型系统里可适当调大
- 查看命令:cat /proc/sys/kernel/pid_max :大概是13w多
- /proc/sys/kernel/threads-max : 系统允许的最大线程数 大约1w5
- maxuserprocess (ulimit -u) : 系统限制某用户下最多可以运行多少进程或线程
- /proc/sys/vm/max_map_count
- maxmap_count文件包含限制一个进程可以拥有的VMA(虚拟内存区域)的数量。虚拟内存区域是一个连续的虚拟地址空间区域。在进程的生命周期中,每当程序尝试在内存中映射文件,链接到共享内存段,或者分配堆空间的时候,这些区域将被创建调优这个值将限制进程可拥有VMA的数量。限制一个进程拥有VMA的总数可能导致应用程序出错,因为当进程达到了VMA上线但又只能释放少量的内存给其他的内核进程使用时,操作系统会抛出内存不足的错误。如果你的操作系统在NORMAL区域仅占用少量的内存,那么调低这个值可以帮助释放内存给内核用。
- 解决方向一:
五、栈溢出
- 会出现溢出情况,但是不叫OOM
- java.lang.StackOverflowError
- 原因:嵌套层数太多,超过栈空间大小
- 出现循环递归
- 栈内存空间设置太小,使用-Xss调整参数
三、性能测试工具:Jmeter
- 基本概述:
- JMeter是一种开源的Java应用程序,用于性能测试和负载测试。它由Apache软件基金会开发和维护。JMeter可以模拟大量用户同时对目标服务器发送请求,以评估服务器的性能和稳定性。
- 使用流程
四、性能优化案例
案例一:调整堆大小提高服务吞吐量
- 修改tomcat配置
- 文件位置:tomcat/bin/catalina.sh 这个是官方的配置文件,不建议用户去修改
- 自定义文件:tomcat/bin/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"
- 初始化配置
- 首先看一下堆内存在30M时的性能
- 优化配置
- 堆设置为120M时的性能,吞吐量提升100%以上,FullGC次数只有1次
案例二:JIT优化
- 堆是分配对象的唯一选择吗?
- JIT的优化可以将对象分配放到栈上,但是Java没有真的在栈上分配,而是把栈上对象(称为聚合量)分解为标量
- 在《深入理解Java虚拟机中》关于Java堆内存有这样一段描述:
随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。 - 在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
- 此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH (GC invisible heap)技术实现off-heap,将生命周期较长的了ava对象从heap中移至heap外,并且Gc不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升Gc的回收效率的目的。
- 编译开销
- 时间开销
- 解释器的执行,抽象的看是这样的:
- 输入的代码->[解释器 解释执行]->执行结果
- JIT编译然后再执行的话,抽象的看则是:
- 输入的代码->[编译器 编译]->编译后的代码->[执行]->执行结果
- 注意:
说JIT比解释快,其实说的是“执行编译后的代码”比“解释器解释执行”要快,并不是说“编译”这个动作比“解释〞这个动作快。]IT编译再怎么快,至少也比解释执行一次略慢一些,而要得到最后的执行结果还得再经过一个“执行编译后的代码”的过程。所以,对“只执行一次”的代码而言,解释执行其实总是比3IT编译执行要快。怎么算是〝只执行一次的代码〝呢?粗略说,下面条件同时满足时就是严格的〝只执行一次。- 只被调用一次,例如类的构造器(class initializer, ())
- 没有循环,对只执行一次的代码做JIT编译再执行,可以说是得不偿失。
- 对只执行少量次数的代码,JTT编译带来的执行速度的提升也未必能抵消掉最初编译带来
- 解释器的执行,抽象的看是这样的:
- 空间开销
- 对一段的Java方法而言:编译后代码的大小相对手字节码的大小:膨胀比达到10+是很正常的。同上面说的时间开销一样,这里的空间开销也是,只有对执行频繁的代码才值得编译,如果把所有代码都编译则会显著增加代码所占空间,导致代码爆炸。这也就解释了为什么有些JVM会选择不总是做JIT编译,而是选择用解释器+]IT编译器的混合执行引擎。
- 时间开销
- 即时编译对代码的优化
- 逃逸分析
0. 如果一个变量的使用仅限于方法内部,那么就没有发生逃逸;没有逃逸的变量才可以进行标量替换- 代码举例1
- 代码举例2
- 参数设置:JDK 6时默认开启逃逸分析
- 代码优化一:栈上分配
- 使用逃逸分析,编译器可以对代码做如下优化:
- 栈上分配。将堆分配转化为栈分配。如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了可以减少垃圾回收时间和次数。
- JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
- 代码举例
- 代码优化二:同步省略(消除)
- 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 线程同步的代价是相当高的,同步的后果是降低并发性和性能。
- 在动态编译同步块的时候,了IT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么了IT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
- 注意!字节码文件里面并不会同步省略,JIT是在解释运行的时候才会生效
- 代码举例
- 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 代码优化三:标量替换
- 代码举例1
public static void main (String [] args){ alloc(); } private static void alloc(){ Point point = new Point (1,2); System.out.printin("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="+×+"; point.y="+y); }
- 参数设置: 参数-XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上。
- 代码举例2
- 代码举例1
- 逃逸分析小结
- 开逃逸分析不开标量替换是没有效果的
- 逃逸分析小结:逃逸分析并不成熟
- 关于逃逸分析的论文在1999年就己经发表了,但直到JDK 1.6才有实现,而且这项技术到如今也并不是十分成熟的。
- 其根本原因就是无法保证非逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
- 一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
- 虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。
- 注意到有一些观点,认为通过逃逸分析,]VM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。
- 目前很多书籍还罪基于JDK 7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。
- 逃逸分析
案例三:合理配置堆内存
- 推荐配置
- 在案例1中我们讲到了增加内存可以提高系统的性能而且效果显著,那么随之带来的一个问题就是,我们增加多少内存比较合适?如果内存过大,那么如果产生FullGC的时候,GC时间会相对比较长,如果内存较小,那么就会频繁的触发GC,在这种情况下,我们该如何合理的适配堆内存大小呢?
- 分析:依据的原则是根据Java Performance里面的推荐公式来进行设置。
- Java整个堆大小设置,Xmx 和Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。
- 方法区(永久代 PermSize和MaxPermSize或元空间 MetaspaceSize 和MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍
- 年轻代Xmn的设置为老年代存活对象的1-1.5倍。
- 如何计算老年代存活对象
- 方式一:查看日志
- JVM參数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)
- 方式二:强制触发FullGC
- 会影响线上服务,慎用!
- 方式1的方式比较可行,但需要更改JVM参数,并分析日志。同时,在使用CMS回收器的时候,有可能不能触发FullGC,所以日志中并没有记录FullGC的日志。在分析的时候就比较难处理。所以,有时候需要强制触发一次FullGC,来观察FullGC之后的老年代存活对象大小。
- 注:强制触发FullGC,会造成线上服务停顿(STW),要谨慎!建议的操作方式为,在强制FullGC前先把服务节点摘除,FullGC之后再将服务挂回可用节点,对外提供服务,在不同时间段触发FullGC,根据多次FullGC之后的老年代内存情况来预估FullGC之后的老年代存活对象大小
- 如何强制触发Full GC?
- jmap -dump:live,format=b,file=heap.bin <pid>将当前的存活对象dump到文件,此时会触发FullGC
- jmap -histo:live <pid>打印每个class的实例数目,内存占用,类全名信息.live子参数加上后,只统计活的对象数量.此时会触发FullGC
- 在性能测试环境,可以通过Java监控工具来触发FullGC,比如使用VisualVM和JConsole,VisualVM集成了JConsole,VisualVM或者JConsole上面有一个触发GC的按钮。
- 方式一:查看日志
- 你会估算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。这样可以让我们对系统有一个大致的估算。
- 案例演示
- 现在我们通过idea启动springboot工程,我们将内存初始化为1024M。我们这里就从1024M的内存开始分析我们的GC日志,根据我们上面的一些知识来进行一个合理的内存设置。
- JVM设置如下:
- -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
- 数据分析
- 结论
案例四:CPU占用很高排查方案
- 案例
- 死锁导致cpu飙高
- 指令:
# $ javac JstackDeadLockDemo # $ java JstackDeadLockDemo # 查看java进程 $ jps -l # 查看线程cpu占比 $ top -Hp pid # 打印该进程的线程做了什么 $ jstack pid > jstack.log
- 问题呈现
- 问题分析
- 延伸
- Xxx大厂问题排查过程:
- 省略;
- ps aux | grep java 查看到当前java进程使用cpu 、内存、磁盘的情况获取使用量异常的进程
- top -Hp 进程pid 检查当前使用异常线程的pid
- 把线程pid变为16进制如 31695->7bcf 然后得到Ox7bcf
- jstack 进程的pid | grep -A20 Ox7bcf 得到相关进程的代码
- 解决方案
- 调整锁的顺序,保持一致
- 或者采用定时锁,一段时间后,如果还不能获取到锁就释放自身持有的所有锁。
案例五:G1并发执行的线程数对性能影响
- 配置信息
- 硬件 8核
- JVM配置
-
export CATALINA_OPTS="$CATALINA OPTS -XX:+UseG1GC" export CATALINA OPTS="$CATALINA_OPTS -Xms30m" export CATALINA OPTS="$CATALINA_OPTS -Xmx30m" 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" export CATALINA_OPTS="$CATALINA_OPTS -XX:ConcGcThreads=1"
- 说明:最后一个参数可以在使用G1GC测试初始并发GCThreads之后再加上。
- 初始化内存和最大内存调整小一些,目的发生 Ful1GC,关注GC时间
- 关注点是:GC次数,GC时间,以及 Jmeter的平均响应时间
-
- 初始状态
- 优化之后
- 总结
- 调整线程数对于YGC速度提升不大
- 吞吐量会有所提升
- 响应时间会降低
- 总之,提升吞吐量,降低响应时间
案例六:调整垃圾回收器对吞吐量的影响
- 初始配置
- 系统配置是单核,我们看到日志,显示DefNew,说明我们用的是串行收集器,SerialGC
- 优化配置1 : 并行垃圾回收
- 查看日志:
- 3次FullGC
- 查看吞吐量,997.6/sec,吞吐量并没有明显变化,我们究其原因,本身UseParallelGC但是我们的服务器是单核。
- 总之:单核下没有区别
- 查看日志:
- 优化配置2 :8核并行垃圾回收
- 查看日志
- 8核状忍下的性能表现如下,吞吐量大幅提升,甚至翻了一倍,这说明我们在多核机器上面采用并行收集器对于系统的吞吐量有一个显著的效果。
- 优化配置3 :G1 GC
- 查看日志:
- 吞吐量高一些
- Full GC没有出现,只有YGC
- 总之:提升吞吐量,可以设置可控制的暂停时间下去回收
案例七:日均百万级订单交易系统如何设置JVM参数
- 一天百万级订单这个绝对是现在顶尖电商公司交易量级,百万订单一般在4个小时左右产生,我们计算一下每秒产生多少订单,3000000/3600/4= 208.3单/s,我们大概按 照每秒300单来计算。
- 通常分配给JVM的情况如下:
- 新生代:>1G
- 老年代:3~4G
- 1333M/2M/60s = 11.1分钟就会把新生代占满,触发MinorGC
- 还可以提高新生代,进一步降低GC频率,这样进入老年代的对象也会降低,减少FGC频率
- 水平扩容更多机器
- 原则:确保压力不要过大,导致大量GC
- 扩至五台至十台,控制每台机器JVM处理的请求数
- 不扩容机器
- 假设每秒几千个订单,则新生代新增对象几十M/秒
- 由于系统压力,一个订单几秒到几十秒才可生成
- 新生代的订单对象会存活几秒到几十秒
- 在订单执行完后,成为垃圾。老年代中的对象越来越多,FGC约频繁
拓展:响应时间控制
特殊问题:新生代老年代比例
JVM 参数设置为:
# 打印日志详情 打印日志打印日期 初始化内存200M 最大内存200M 日志 路径
-XX:+PrintGCDetails -XX:+PrintGCDatestamps -Xms 300M -Xmx300M-Xloggc: log/gc. log
新生代(Young)与老年代 一 0ld)的比例为1:2,所以,内存分配应该是新生代100M,老年代 200M
我们可以先用命令查看一下堆内存分配是怎么样的:
# 查看进程ID
jps -1
# 查看对应的进程工D的堆内存分配
jmap -heap 3725
结果大家可以看到:我们的SurvivorRatio= 8 但是西存分配却不是8:为什么呢?
参数AdaptiveSizePolicy
这是因为JDK 1.8 默认使用 UseParallelGC 垃圾回收器,该垃圾回收器默认启动了 AdaptivesizePolicy,会根据GC的情况自动 计算 Eden、 From 和 To区的大小;所以这是由于JDK1.8的自适应大小策略导致的,除此之外,我们下面观察GC日志发现有很多类似这样的FULLGC (Ergonomics),也是一样的原因。我们可以在jvm参数中配置开启和关闭该配置:
#开启:
-XX:+UseAdaptiveSizePolicy
#关闭
-XX: -UseAdaptiveSizePolicy
注意事项:
- 在 JDK 1.8 中,如果使用 CMS,无论 UseAdaptiveSizePolicy 如何设置,都会将 UseAdaptiveSizePolicy 设置为 false;不过不同版本的JDK存在差异;
- UseAdaptiveSizePolicy不要和SurvivorRatio参数显示设置搭配使用,一起使用会导致参数失效:
- 由于UseAdaptiveSizePolicy会动态调整 Eden、 Survivor 的大小,有些情况存在Survivor 被自动调为很小,比如十几MB甚至几MB的可能,这个时候YGC回收掉 Eden区后,还存活的对象进入Survivor装不下,就会直接晋升到老年代,导致老年代占用空间逐渐增加,从而触发FULL GC,如果一次FULL GC的耗时很长(比如到达几百毫秒),那么在要求高响应的系统就是不可取的。
- 附:对于面向外部的大泒量、低延迟系统,不建议启用此参数,建议关闭该参数。
- 如果不想动态调整内存大小,以下是解决方案:
- 保持使用 UseParallelGC,显式设置 -xx: SurvivorRatio=8
- 使用 CMS 垃圾回收器。CMS 默认关闭 AdaptiveSizePolicy。 配置参数-XX: +UseConcMarkSweepGC
使用paraLLelGC的情说下,不管是否开启 FUseAdaptivesizePolicy 参数,默认Eden与Survivor的比例都为:6:1:1
面试小结
Q1:12306如何支持春节抢票
12306号称是国内并发量最大的秒杀网站,并发量达到百万级别。
普通电商订单–> 下单–>订单系统(10)减库存 —>等待用户付款
12306一种可能的模型:下单 -->减库存和订单 (redis、 kafka)同时异步进行 --> 等付款
但减库存最后还会把压力压到一台服务器上。如何?
分布式本地库存+单独服务器做库存均衡!
大流量的处理思路:分而治之
Q2:
问题一:有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器是32位的 1.5G的堆,用
户反馈网站比较缓慢。因此公司决定升级,新的服务器为64位,16G的堆内存,结果用户反馈卡顿十
分严重,反而比以前效率更低了!
- 为什么原网站慢?
- 频繁的GC,STW时间比较长,响顾时间慢!
- 为什么会更卡顿?
- 内存空间越大,GC时间越长,延迟时间更长,更卡顿
- 咋办?
- 调整垃圾回收器 : parallel GC ;ParNew + CMS: G1
- 调整配置参数:
- -XX: MaxGCPauseMillis 限制回收时间提升吞吐量
- -XX:ConcGCThreads 增加线程数,提高响应时间
- 根据日志和dump文件分析,优化内存空间分配比例 jstack; jinfo; jstat; jmap
问题二:系统cPU经常100%,如何调优?(面试高频)
CPU100%的话,一定是有线程占用系统资源。具体步骤前面已经讲过。
注意:工作中有时候是工作线程100%占用了CPU,还有可能是垃圾回收线程占用了100%
问题三:系统内存飙高,如何查找问题?(面试高频)
定位进程
一方面:jmap -heap ; gc日志情况
另一方面:dump文件分析
问题四:如何监控JVM
- 命令行
- 阿尔萨斯
JVM监控及诊断工具
命令行工具
- jps:查看正在运行时Java进程
- jstat:查看JVM统计信息
- option参数 -gc
- interval参数 :用于指定输出统计数据的周期,单位为毫秒。即:查询间隔
- count参数:用于指定查询的总次数
- -t参数:可以在输出信息前加上一个Timestamp列,显示程序的运行时间。单位:秒
- -h参数:可以在周期性数据输出时,输出多少行数据后输出一个表头信息
- jinfo:实时查看和修改JVM配置参数
- jmap:导出内存映像文件&内存使用情况
- jhat:JDK自带堆分析工具
- jstack:打印JVM中线程快照
- jcmd:多功能命令行
GUI
- -工具概述
- -jConsole
- -Visual VM
- -eclipse MAT
补充1:再谈内存泄漏
补充2:支持使用OQL语言 - -JProfiler
- -Arthas
- -Java Mission Control
- 其它工具