1、JVM调优目标
使用较小的内存来获得较高的吞吐量或者较低的延迟。
-
内存占用:程序正常运行需要的内存大小。
-
延迟:由于垃圾回收而引起的程序停顿时间。
-
吞吐量:用户程序运行时间占用户程序 和 垃圾收集占用总时间的比值。
吞吐量 = CPU在用户应用程序运行的时间 / (CPU在用户应用程序运行的时间 + CPU垃圾回收的时间)
程序在上线前的测试或运行中有时会出现一些大大小小的JVM问题,比如 cpu load过高、请求延迟、tps(每秒钟事务数量)降低等,甚至出现内存泄漏(每次垃圾回收使用的时间越来越长,垃圾回收频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少)、内存溢出导致系统崩溃,因此需要对JVM进行调优,使得程序在正常运行的前提下,获得更高的用户体验和运行效率。
注:内存泄漏是指本应该被GC回收的无用对象没有被回收,导致的内存空间的浪费,当内存泄露严重时会导致OOM(out of memory);
当然,和CAP原则一样,同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的,程序的目标不同,调优时所考虑的方向也不同,在调优之前,必须要结合实际场景,有明确的的优化目标,找到性能瓶颈,对瓶颈有针对性的优化,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标。
栈:stack
堆:heap
2、JVM调优工具
调优可以依赖参考的数据有系统运行日志、堆栈错误信息、gc日志、线程快照、堆转储快照等。
- 系统运行日志:系统运行日志就是在程序代码中打印出的日志,描述了代码级别的系统运行轨迹(执行的方法、入参、返回值等),一般系统出现问题,系统运行日志是首先要查看的日志。
- 堆栈错误信息:当系统出现异常后,可以根据堆栈信息初步定位问题所在,比如:
根据“java.lang.OutOfMemoryError: Java heap space”可以判断是堆内存溢出;
根据“java.lang.StackOverflowError”可以判断是栈溢出;
根据“java.lang.OutOfMemoryError: PermGen space”可以判断是方法区溢出等。
- gc日志输出设置:
我在活着。。。。
2018-06-15T10:44:26.630-0800: [GC (System.gc()) [PSYoungGen: 2673K->496K(38400K)] 2673K->504K(125952K), 0.0010649 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2018-06-15T10:44:26.631-0800: [Full GC (System.gc()) [PSYoungGen: 496K->0K(38400K)] [ParOldGen: 8K->402K(87552K)] 504K->402K(125952K), [Metaspace: 3300K->3300K(1056768K)], 0.0066154 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
我要死了。。。。
Heap
PSYoungGen total 38400K, used 1664K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
eden space 33280K, 5% used [0x0000000795580000,0x00000007957201b0,0x0000000797600000)
from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
to space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
ParOldGen total 87552K, used 402K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
object space 87552K, 0% used [0x0000000740000000,0x0000000740064ab8,0x0000000745580000)
Metaspace used 3312K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 369K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
- 线程快照----jstack
顾名思义,根据线程快照可以看到线程在某一时刻的状态,当系统中可能存在请求超时、死循环、死锁等情况是,可以根据线程快照来进一步确定问题。通过执行虚拟机自带的“jstack pid”命令,可以dump出当前进程中线程的快照信息;
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息,如果是在64位机器上,需要指定选项"-J-d64",Windows的jstack使用方式只支持以下的这种方式:
jstack [-l] pid主要分为两个功能:
a. 针对活着的进程做本地的或远程的线程dump;
b. 针对core文件做线程dump。
- 堆转储快照
程序启动时可以使用 “-XX:+HeapDumpOnOutOfMemory” 和 “-XX:HeapDumpPath=/data/jvm/dumpfile.hprof”,当程序发生内存溢出时,把当时的内存快照以文件形式进行转储(也可以直接用jmap命令转储程序运行时任意时刻的内存快照),事后对当时的内存使用情况进行分析。
3、JVM调优经验
注:jvm配置信息在哪配置?
Linux:在tomcat下的bin/catalina.sh文件中配置。
JVM配置方面,一般情况可以先用默认配置(基本的一些初始参数可以保证一般的应用跑的比较稳定了),在测试中根据系统运行状况(会话并发情况、会话时间等),结合gc日志、内存监控、使用的垃圾收集器等进行合理的调整,当老年代内存过小时可能引起频繁Full GC,当内存过大时Full GC时间会特别长。
下图是内存监控
那么JVM的配置比如新生代、老年代应该配置多大最合适呢?
答案是不一定,调优就是找答案的过程,物理内存一定的情况下,新生代设置越大,老年代就越小,Full GC频率就越高,但Full GC时间越短;相反新生代设置越小,老年代就越大,Full GC频率就越低,但每次Full GC消耗的时间越大。建议如下:
-
-Xms(初始堆大小)和-Xmx(最大堆大小)的值设置成相等,堆大小默认为-Xms指定的大小;默认空闲堆内存小于40%时,JVM会扩大堆到-Xmx指定的大小;空闲堆内存大于70%时,JVM会减小堆到-Xms指定的大小。如果在Full GC后满足不了内存需求会动态调整,这个阶段比较耗费资源。
-
新生代尽量设置大一些,让对象在新生代多存活一段时间,每次Minor GC 都要尽可能多的收集垃圾对象,防止或延迟对象进入老年代的机会,以减少应用程序发生Full GC的频率。
-
老年代如果使用CMS收集器,新生代可以不用太大,因为CMS的并行收集速度也很快,收集过程比较耗时的并发标记和并发清除阶段都可以与用户线程并发执行。
-
方法区大小的设置,1.6之前的需要考虑系统运行时动态增加的常量、静态变量等,1.7只要差不多能装下启动时和后期动态加载的类信息就行。
JVM具有四种类型的GC实现:
- 串行垃圾收集器
- 并行垃圾收集器
- CMS垃圾收集器
- G1垃圾收集器
CMS (Concurrent Mark Sweep)收集器,以获取最短回收停顿时间为目标,CMS是基于“标记-清除”算法实现的; 其中“Concurrent”并发是指垃圾收集的线程和用户执行的线程是可以同时执行的。
G1(Garbage First)收集器, 并发 + 并行回收 + 标记管理;
面向服务端应用的垃圾收集器。G1名称由来:G1收集器,Java堆的内存布局是将整个Java堆分为多个大小相等的独立区域(Region),也保留了新生代和老年代的概念。但是新生代和老年代不再是物理隔离的,它们都是一部分Region的集合。G1跟踪各个Region里面的垃圾堆积的价值大小(也就是回收获得的空间大小以及回收需要的时间的经验值),在后台维护一个优先列表,每次根据允许的的收集时间,优先回收价值最大的Region。
代码实现方面,性能出现问题比如程序等待、内存泄漏除了JVM配置可能存在问题,代码实现上也有很大关系:
-
避免创建过大的对象及数组:过大的对象或数组在新生代没有足够空间容纳时会直接进入老年代,如果是短命的大对象,会提前出发Full GC。
-
当集合中有对象的引用,这些对象使用完之后要尽快把集合中的引用清空,这些无用对象尽快回收避免进入老年代。
-
可以在合适的场景(如实现缓存)采用软引用、弱引用,比如用软引用来为ObjectA分配实例:SoftReference objectA=new SoftReference(); 在发生内存溢出前,会将objectA列入回收范围进行二次回收,如果这次回收还没有足够内存,才会抛出内存溢出的异常。
-
避免同时加载大量数据,如一次从数据库中取出大量数据,或者一次从Excel中读取大量记录,可以分批读取,用完尽快清空引用。
-
避免产生死循环,产生死循环后,循环体内可能重复产生大量实例,导致内存空间被迅速占满。
-
尽量避免长时间等待外部资源(数据库、网络、设备资源等)的情况,缩小对象的生命周期,避免进入老年代,如果不能及时返回结果可以适当采用异步处理的方式等。
参数 | 说明 | 实例 |
---|---|---|
-Xms | 初始堆大小,默认物理内存的 1/64 | -Xms512M |
-Xmx | 最大堆大小,默认物理内存的 1/4 | -Xms2G |
-Xmn | 新生代内存大小,官方推荐为整个堆的3/8 | -Xmn512M |
-Xss | 线程堆栈大小,jdk1.5及之后默认1M,之前默认256k | -Xss512k |
-XX:NewRatio=n | 设置新生代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 | -XX:NewRatio=3 |
-XX:SurvivorRatio=n | 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:8,表示Eden:Survivor=8:1:1,一个Survivor区占整个年轻代的1/8 | -XX:SurvivorRatio=8 |
-XX:PermSize=n | 永久代初始值,默认为物理内存的1/64 | -XX:PermSize=128M |
-XX:MaxPermSize=n | 永久代最大值,默认为物理内存的1/4 | -XX:MaxPermSize=256M |
-verbose:class | 在控制台打印类加载信息 | |
-verbose:gc | 在控制台打印垃圾回收日志 | |
-XX:+PrintGC | 打印GC日志,内容简单 | |
-XX:+PrintGCDetails | 打印GC日志,内容详细 | |
-XX:+PrintGCDateStamps | 在GC日志中添加时间戳 | |
-Xloggc:filename | 指定gc日志路径 | -Xloggc:/data/jvm/gc.log |
-XX:+UseSerialGC | 年轻代设置串行收集器Serial | |
-XX:+UseParallelGC | 年轻代设置并行收集器Parallel Scavenge | |
-XX:ParallelGCThreads=n | 设置Parallel Scavenge收集时使用的CPU数。并行收集线程数。 | -XX:ParallelGCThreads=4 |
-XX:MaxGCPauseMillis=n | 设置Parallel Scavenge回收的最大时间(毫秒) | -XX:MaxGCPauseMillis=100 |
-XX:GCTimeRatio=n | 设置Parallel Scavenge垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) | -XX:GCTimeRatio=19 |
-XX:+UseParallelOldGC | 设置老年代为并行收集器ParallelOld收集器 | |
-XX:+UseConcMarkSweepGC | 设置老年代并发收集器CMS | |
-XX:+CMSIncrementalMode | 设置CMS收集器为增量模式,适用于单CPU情况。 |
对年轻代进行优化
优化吞吐量的目的其实是尽量少的Full GC ,或者尽量避免Full GC,有以下方法可以尽量减少Full GC:
- 增大年轻代: 这样同样能减少Minor GC 的频率,减慢对象进入老年代。
- 增大Eden:你可以让eden空间更大,可以减少MinorGC的次数。我知道当对象的任期或者岁数达到一定值的时候就会移动到old代,而这个任期就是对象经历MinorGC的次数,MinorGC的次数越少,对象任期增长越慢,就有可能被MinorGC回收掉,而不是进入old代。
- 增大任期阈值:同样是延长对象在年轻代的停留时间,达到减少对象进入老年代的效果。
- minorGC 和 Full GC区别
新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。
对老年代进行优化
- CMS
- 吞吐量垃圾回收器
- CMS吞吐量优化
- 额外命令
- 增加年轻代大小,减少Minor GC频率
- 增加老年代大小,减少Full GC的频率,减少碎片
- 优化堆大小,减少年轻代移动到老年代的对象
- 优化CMS周期启动
- 吞吐量垃圾回收器优化
- 关闭自适应大小(-XX:-UseAdaptiveSizePolicy),开启日志(-XX:+PrintGCDetails),自定义堆、年轻代比例
年轻代和年老代默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小。
通过观察应用一段时间,看其他在峰值时年老代会占多少内存(注意给年老代至少预留1/3的增长),在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。
- 确定堆大小和老年代大小
- 查看GC日志,看Survivor是否溢出,加快Full GC,如果有溢出的情况,就要调整Survivor大小
垃圾回收算法:
垃圾回收器算法比较
- 串行回收算法:会停止当前应用进程,回收垃圾,停顿时间久,吞吐量大,响应时间长
- 并行回收算法: 是多个线程同时执行串行回收算法(多核),也会使应用停顿,吞吐量大,响应时间长,用户体验差
- 并发回收算法:应用和垃圾回收多个线程并发执行,吞吐量相对小,响应时间短,用户体验好
- G1 : 并发 + 并行回收 + 标记管理