—-慢慢来比较快,虚心学技术—-

系统性能调优

很明显,jvm性能调优仅仅只是作为一个系统被优化的一道工序,真正的系统性能调优首先包含如下几个方面

图片.png

我们不能想着依靠jvm性能调优使整个系统的性能达到质的飞跃,jvm性能调优的基础是架构调优和代码调优,且架构调优对系统性能优化的作用最大。本篇文章仅从jvm性能调优角度去分析学习

JVM性能的定义

对于jvm性能调优,其对性能的定义主要是从三个指标定义的:

吞吐量(Throughput)**: 指不考虑GC引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标**

简单来说,系统运行时,不可避免的会有专门的GC线程回收垃圾,而这个GC线程会和主程序线程竞争CPU,造成程序耗时。也就是说,吞吐量=应用程序占时占程序总用时的比例

如:吞吐量=95/100,意味着100秒的程序执行总时间里面,有95秒是用来执行应用程序的,而GC线程占用了5秒

延迟**:其度量标准是缩短由于垃圾收集引起的停顿时间或完全消除由垃圾收集引起的停顿,以避免应用程序运行期间的抖动**

内存占用**:垃圾收集器流畅运行所需要的内存数量**

基本上只要任何一个属性的性能提高,都会导致另外两个属性的性能损失,不可兼得。而对于具体优化属性的选择需要基于应用程序去分析。

影响JVM性能的因素

上述三个指标明显都和垃圾收集器有关系,那么,我们从垃圾收集机制进行分析。

我们知道,垃圾收集器的主要战场是堆内存,堆内存具体分布看上一篇文章,而堆内存中主要进行两种垃圾回收:Minor GC以及Full GC,其中Minor GC的执行速度极快,且频率极高,其执行频率和年轻代的分区内存大小有关。而Full GC执行速度较慢,且执行频率较低,其执行频率和老年代的分区内存大小有关

实际上,影响JVM性能的因素主要是Minor GC和Full GC的执行频率和执行时间。(主要是Full GC的影响)

JVM性能调优的主要目标

1、降低GC频率

2、减少Full GC的执行次数

其中,Full GC的执行次数对JVM性能影响尤为巨大,所以换句话说,我们对JVM性能进行调优的基本目标是:**尽量避免触发Full GC!!!如果触发Full GC,应尽量减少Full GC的执行时间**

JVM性能调优思路

是否需要调优?

调优前首先确认是否需要进行性能调优,总不能一股脑上手就是干。通常情况下,满足如下指标,不需要进行GC调优:
**
Minor GC执行时间不超过50ms

Minor GC执行频率不低于10s/次

Full GC执行时间不超过1S

Full GC执行频率不低于10min/次

怎么调优?

那么,如果要降低GC频率和Full GC的执行次数,我们首先得要知道什么时候会执行GC:

Minor GC:年轻代内存满了(此处指Eden区满,Survivor区满不会触发)的时候触发
Full GC: 年轻代内存已满或显式调用System.gc()方法时
**
那么问题来了,本着Full GC尽量少的原则

如果我们将老年代的内存调大,Full GC的执行频率会随之降低,但显然又会导致Full GC的执行时间变长;

如果我们将老年代的内存调小,Full GC的执行频率又会随之升高且可能导致OutOfMemoryError

因此,我们需要**将老年代内存调整为合适的值**,而这个值需要通过将不同的参数值配置的程序部署到两个及以上的服务器,然后比较性能才能得到,最后将可以确定提高性能或减少Full GC频率的参数用到生产服务器中。

GC调优步骤

1、打印GC日志

  1. -XX:+PrintGC 输出GC日志
  2. -XX:+PrintGCDetails 输出GC的详细日志
  3. -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
  4. -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800
  5. -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
  6. -Xloggc:../logs/gc.log 日志文件的输出路径

运行时配置即可,如果是tomcat,则配置在JAVA_OPTS变量中。

配置如:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:../logs/gc.log

现有如下代码进行测试GC日志

  1. public class GC_LOG_TEST {
  2. public static GC_LOG_TEST test;
  3. //重写父类中protect修饰finalize()方法以供调用
  4. @Override
  5. public void finalize() throws Throwable {
  6. super.finalize();
  7. System.out.println("执行finalize方法");
  8. test=this; //重新建立连接
  9. }
  10. public static void main(String[] args) throws InterruptedException {
  11. test = new GC_LOG_TEST();
  12. test = null; //断开对象引用
  13. System.gc();//执行垃圾回收,首先判断是否重写了finalize方法而且gc没有执行过finalize方法,如果是,则先调用finalize方法
  14. Thread.sleep(50);
  15. if(null == test){
  16. System.out.println("我挂了");
  17. }else{
  18. System.out.println("我还活着");
  19. test=null;
  20. System.gc();
  21. Thread.sleep(50);
  22. if(null == test){
  23. System.out.println("我挂了");
  24. }else{
  25. System.out.println("我还活着");
  26. }
  27. }
  28. }
  29. }

以上述配置环境下运行代码,得到日志文件内容如下:

  1. Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for windows-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:36:39 by "java_re" with MS VC++ 10.0 (VS2010)
  2. Memory: 4k page, physical 8283460k(2659856k free), swap 20866372k(13813464k free)
  3. CommandLine flags: -XX:InitialHeapSize=132535360 -XX:MaxHeapSize=2120565760 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
  4. 2019-11-13T21:49:31.845+0800: 0.174: [GC (System.gc()) [PSYoungGen: 2676K->776K(38400K)] 2676K->784K(125952K), 0.0076276 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
  5. 2019-11-13T21:49:31.852+0800: 0.178: [Full GC (System.gc()) [PSYoungGen: 776K->0K(38400K)] [ParOldGen: 8K->589K(87552K)] 784K->589K(125952K), [Metaspace: 3062K->3062K(1056768K)], 0.0079222 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
  6. 2019-11-13T21:49:31.912+0800: 0.238: [GC (System.gc()) [PSYoungGen: 2662K->224K(38400K)] 3252K->813K(125952K), 0.0012830 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  7. 2019-11-13T21:49:31.913+0800: 0.239: [Full GC (System.gc()) [PSYoungGen: 224K->0K(38400K)] [ParOldGen: 589K->622K(87552K)] 813K->622K(125952K), [Metaspace: 3217K->3217K(1056768K)], 0.0088097 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
  8. Heap
  9. PSYoungGen total 38400K, used 1664K [0x00000000d5d80000, 0x00000000d8800000, 0x0000000100000000)
  10. eden space 33280K, 5% used [0x00000000d5d80000,0x00000000d5f20198,0x00000000d7e00000)
  11. from space 5120K, 0% used [0x00000000d8300000,0x00000000d8300000,0x00000000d8800000)
  12. to space 5120K, 0% used [0x00000000d7e00000,0x00000000d7e00000,0x00000000d8300000)
  13. ParOldGen total 87552K, used 622K [0x0000000081800000, 0x0000000086d80000, 0x00000000d5d80000)
  14. object space 87552K, 0% used [0x0000000081800000,0x000000008189bbb0,0x0000000086d80000)
  15. Metaspace used 3224K, capacity 4496K, committed 4864K, reserved 1056768K
  16. class space used 350K, capacity 388K, committed 512K, reserved 1048576K

2、分析GC日志得到关键性指标

我们一点点的分析上述GC日志内容

首先是前三项—————**项目的配置参数。
此处展示了项目运行环境的具体参数以及JVM的内存参数**

Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for windows-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:36:39 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8283460k(2659856k free), swap 20866372k(13813464k free)
CommandLine flags: -XX:InitialHeapSize=132535360 -XX:MaxHeapSize=2120565760 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

然后是**GC动作日志**

格式:
执行时间戳:[GC类型(GC执行原因)[GC发生的区域:GC之前所占用该区域的大小 -> GC之后所占用该区域的大小(该区域整个的大小)] GC之前占用堆内存大小->GC之后占用堆内存大小(堆内存整个的大小),当前时间戳下GC占用所耗费的时间 secs] [Times:user=用户耗时 sys=系统耗时, real=实际耗时 secs]

下面内容的GC(System.gc())指的是Minor GC

2019-11-13T21:49:31.845+0800: 0.174: [GC (System.gc()) [PSYoungGen: 2676K->776K(38400K)] 2676K->784K(125952K), 0.0076276 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
2019-11-13T21:49:31.852+0800: 0.178: [Full GC (System.gc()) [PSYoungGen: 776K->0K(38400K)] [ParOldGen: 8K->589K(87552K)] 784K->589K(125952K), [Metaspace: 3062K->3062K(1056768K)], 0.0079222 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
2019-11-13T21:49:31.912+0800: 0.238: [GC (System.gc()) [PSYoungGen: 2662K->224K(38400K)] 3252K->813K(125952K), 0.0012830 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-11-13T21:49:31.913+0800: 0.239: [Full GC (System.gc()) [PSYoungGen: 224K->0K(38400K)] [ParOldGen: 589K->622K(87552K)] 813K->622K(125952K), [Metaspace: 3217K->3217K(1056768K)], 0.0088097 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

根据GC动作日志,我们可以知道JVM执行GC的频率以及发生GC的原因和GC内存细节信息,从而得到应用运行过程中的内存变化,进而定位问题

最后一部分是**堆内存信息**,包括每个区域的总大小,占用以及空闲大小等信息

Heap
 PSYoungGen      total 38400K, used 1664K [0x00000000d5d80000, 0x00000000d8800000, 0x0000000100000000)
  eden space 33280K, 5% used [0x00000000d5d80000,0x00000000d5f20198,0x00000000d7e00000)
  from space 5120K, 0% used [0x00000000d8300000,0x00000000d8300000,0x00000000d8800000)
  to   space 5120K, 0% used [0x00000000d7e00000,0x00000000d7e00000,0x00000000d8300000)
 ParOldGen       total 87552K, used 622K [0x0000000081800000, 0x0000000086d80000, 0x00000000d5d80000)
  object space 87552K, 0% used [0x0000000081800000,0x000000008189bbb0,0x0000000086d80000)
 Metaspace       used 3224K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

3、分析GC原因,调优JVM参数

通过分析GC日志,我们可以得出具体的GC频率和详细信息, 主要是看Full GC的执行频率以及执行原因和内存信息,通过分析Full GC的执行频率和执行原因判断需要调优的JVM参数,然后通过不断地尝试,以寻找到最合适的JVM环境配置。

如当日志分析发现出现频繁的Full GC且GC原因是Metadata GC Threshold时,我们知道是系统启动时加载类到元空间(即方法区)中内存不够所导致的,我们需要调整元空间的大小以适应程序性能需求

JVM性能调优参数

-Xms-XX:InitialHeapSize)堆内存初始大小,默认为物理内存的1/64当堆内存剩余不足-Xms指定值的40%时,JVM会将堆内存增大至-Xmx指定大小

-Xmx**(-XX:MaxHeapSize)堆内存最大大小,默认为无力内存的1/4,当堆内存剩余大于-Xmx指定值的70%时,JVM会将堆内存缩小至-Xms指定大小

-Xmn**(-XX:NewSize)年轻代内存初始大小(Eden区+Survivor区)

-XX:MaxNewSize**:年轻代内存最大大小

-XX:NewRatio**:年轻代和老年代的相对大小,如-XX:NewRatio=3,意为老年代/年轻代=3/1,老年代占堆大小的3/4,年轻代占堆大小的1/4

-XX:SuvivorRatio**:年轻代中Eden区和Suvivor的To区的相对大小,如-XX:SuvivorRatio=10,意为年轻代中Eden区/To区=10/1,即Eden区占年轻代的10/12,而From区和To区各占年轻代的1/12(From区和To区永远等大)

相关参考

1、为了避免堆空间的伸缩带来的额外时间消耗,我们通常将-Xms和-Xmx设置为一致

2、为了避免年轻代空间伸缩带来的额外时间消耗,我们将-Xmn和-XX:MaxNewSize设置为一样

3、年轻代与老年代的默认比例按1:2分配堆内存,具体可以通过两者比例-XX:NewRatio来调整

4、更大的年轻代必将导致更小的老年代,就会增加更频繁的Full GC

更大的老年代必将导致更小的年轻代,增加Minor GC的频率,但是执行时间很短,减少Full GC的产生,但是有可能导致Full GC的执行时间加长。

至于如何选择合理的年轻代和老年代空间大小,有如下规律:

当应用中存在大量的临时对象,采用更大的年轻代;

当应用中存在大量的持久对象,采用更大的老年代;

5、在配置较好的机器上,可以适当为老年代选择并行收集算法

注:IDEA中打印GC日志

图片.png

图片.png

总结

1、jvm性能调优的基础是架构调优和代码调优,且架构调优对系统性能优化的作用最大

2、jvm性能调优主要调整三个指标:吞吐量、延迟和内存占用

3、
影响JVM性能的因素主要是Minor GC和Full GC的执行频率和执行时间**

如有贻误,还请评论指正