性能调优 14. JVM调优工具详解及调优实战

1. 介绍


‌‌‌  市面上调优工具无非就是JDK基础调优命令的整合,这边调优使用JDK1.8进行

2. JDK自带各种命令调优化


2.1. 前置准备


‌‌‌  如果启动一个应用程序在运行,可用jps查看其进程id,接着就可以用JDK自带各种命令优化应用。


‌‌‌  用jps查看JAVA进程的进程ID

在这里插入图片描述


2.2. Jmap命令


‌‌‌  此命令可以用来查看内存信息,实例个数以及占用内存大小。


2.2.1. 查看运行进程生成过的实例和存活实例


‌‌‌  主要命令



‌‌‌  jmap -histo 32984  #查看当前进程历史生成的实例

	jmap -histo 32984 >./log.txt #输出到日志中
	
‌‌‌  jmap -histo:live 32984  #查看当前进程,存活的实例,执行过程中可能会触发一次full gc

‌‌‌  输出结果格式如下:

‌‌‌  1. num:序号。

‌‌‌  2. instances:实例数量。

‌‌‌  3. bytes:这些实例总共占用空间大小字节。

‌‌‌  4. classname:类名称。

在这里插入图片描述


2.2.2. 查看堆信息


‌‌‌  命令



‌‌‌  jmap -heap 32984

‌‌‌  Heap Configuration表示堆的信息,可以查看最大堆内存,新生代的最大空间,新生代空间,老年代空间等。

‌‌‌  Heap Usage堆内存的使用情况,比如Eden Space可以看Eden区的大小,使用的情况,空闲的情况。

在这里插入图片描述


2.2.3. 堆内存dump



‌‌‌  jmap -dump:format=b,file=eureka.hprof 32984

‌‌‌  生成当前堆信息的快照在执行命令的当前目录下,导出的dump文件可以用可视化工具查看,比如jvisualvm。

在这里插入图片描述


‌‌‌ 2.2.3.1. jvisualvm命令工具导入该dump文件分析

‌‌‌  启动jvisualvm—文件—装载选择生成的dump文件。

‌‌‌  显示如下

在这里插入图片描述


2.2.3.2. 设置内存溢出自动导出dump文件

‌‌‌  可以设置JVM启动参数,服务内存溢出时候自动导出dump文件。

‌‌‌  1. -XX:+HeapDumpOnOutOfMemoryError。。

‌‌‌  2. -XX:HeapDumpPath=./(导出的路径,可以绝对或者相对路径)。

‌‌‌  示例代码:


‌‌‌  package com.liu.jvm_tiao_you;

‌‌‌  import lombok.Data;

‌‌‌  import java.util.ArrayList;
‌‌‌  import java.util.List;
‌‌‌  import java.util.UUID;

‌‌‌  public class Test1 {

    public static List<Object> list = new ArrayList<>();

    // JVM设置
    // -Xms1M -Xmx1M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\jvm.dump
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        int i = 0;
        int j = 0;
        while (true) {
            list.add(new User(i++, UUID.randomUUID().toString()));
            new User(j--, UUID.randomUUID().toString());
        }
    }

    @Data
    public static class  User{

        public User(int i, String name){
            this.id = i;
            this.name=name;
        }
        private int id;

        private String name;

    }
‌‌‌  }


‌‌‌  运行报oom,这时候自动导出dump,jvisualvm工具查看dump,可以查看哪些对象实例多的说明该对象创建处有问题

‌‌‌  注意

‌‌‌  1. char数组比较多,是因为字符串内部使用的是char数组。

在这里插入图片描述

2.3. Jstack


2.3.1. 用Jstack加进程id查找死锁


‌‌‌  1. 启动下面代码,造成死锁。


‌‌‌  package com.liu.jvm_tiao_you;

‌‌‌  /**
 * @author jsLiu
 * @version 1.0.0
 * @description Jstack查看死锁的问题
 * @date 2023/09/27
 */
‌‌‌  public class Test2 {

    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock1) {
                try {
                    System.out.println("thread1 begin");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                }
                synchronized (lock2) {
                    System.out.println("thread1 end");
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (lock2) {
                try {
                    System.out.println("thread2 begin");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                }
                synchronized (lock1) {
                    System.out.println("thread2 end");
                }
            }
        }).start();

        System.out.println("main thread end");
    }
‌‌‌  }


‌‌‌  2. jps查看java进程id。

在这里插入图片描述
‌‌‌  
‌‌‌  3. jstack 20344 查找该进行id下所有线程堆栈信息。

在这里插入图片描述
  Thread-1—线程名

‌‌‌  prio—线程的优先级

‌‌‌  os_prio—操作系统内核的线程的优先级

‌‌‌  tid—java的线程id

‌‌‌  nid—线程对应的内核线程的id

‌‌‌  java.lang.Thread.State: BLOCKED—线程的状态

‌‌‌  如果有死锁会提示死锁信息。

‌‌‌  Found one Java-level deadlock,表示发现死锁,后面信息表示线程Thread-1获取锁需要等待线程Thread-0的锁释放。

在这里插入图片描述

‌‌‌  4. 利用 jvisualvm也能查看死锁。这些调优工具,只是对这些查看命令的封装。

‌‌‌  流程:启动 jvisualvm—线程—线程Dump。
在这里插入图片描述


2.3.2. Jstack找出占用cpu最高的线程堆栈信息


‌‌‌  操作系统是linux下。

‌‌‌  1. 示例代码

‌‌‌  package com.liu.jvm_tiao_you.jstack;

‌‌‌  import lombok.Data;

‌‌‌  /**
 * @author jsLiu
 * @version 1.0.0
 * @description jstack 查看cpu飙升例子
 * @date 2023/10/01
 */
‌‌‌  public class Test2 {

    public static final int initData = 666;
    public static User user = new User();

    public int compute() {  //一个方法对应一块栈帧内存区域
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Test2 test2 = new Test2();
        while (true){
            test2.compute();
        }
    }

    @Data
    public static class  User{

        public User(){
        }


    }
‌‌‌  }


‌‌‌  2. 使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如19663。

在这里插入图片描述

‌‌‌  3. 查看高负载进程下的高负载线程 按H(记住是大H,shift+h),获取每个线程的内存情况 (注意获取的是该进程下的每个线程)。

在这里插入图片描述

‌‌‌  
  4. 找到内存和cpu占用最高的线程pid,比如19664,对应是操作系统内核的线程id。转为十六进制得到 0x4cd0,此为线程id的十六进制表示。

‌‌‌  5. 执行 jstack 该线程下的进程id|grep -A 10 该线程id 。比如jstack 19663|grep -A 10 4cd0(注意小写),得到线程堆栈信息中4cd0这个线程,飙高关键行的后面10行信息,从堆栈中可以发现导致cpu飙高的调用方法。

在这里插入图片描述

‌‌‌  6. 查看对应的堆栈信息找出可能存在问题的代码。


2.4. Jinfo


‌‌‌  该命令查看正在运行的Java应用程序的扩展参数 。查看jvm的参数,查看java系统参数。

‌‌‌  命令


‌‌‌  jinfo [-命令选项] [进程id]

‌‌‌  1. 查看jvm启动的参数


‌‌‌  jinfo -flags pid(线程id)

在这里插入图片描述

‌‌‌  2. 查看java系统参数

‌‌‌  jinfo -sysprops(线程id)

在这里插入图片描述


2.5. Jstat


‌‌‌  比较重要的调优命令。jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。

‌‌‌  命令


‌‌‌  jstat命令命令格式:
‌‌‌  jstat [Options] pid [interval] [count]
 
‌‌‌  命令参数说明:
‌‌‌  Options,一般使用 -gcutil 或  -gc 查看gc 情况
‌‌‌  pid,当前运行的 java进程号 
‌‌‌  interval,间隔时间,单位为秒或者毫秒 
‌‌‌  count,打印次数,如果缺省则打印无数次
 
‌‌‌  Options 参数如下:
‌‌‌  -gc:统计 jdk gc时 heap信息,以使用空间字节数表示
‌‌‌  -gcutil:统计 gc时, heap情况,以使用空间的百分比表示
‌‌‌  -class:统计 class loader行为信息
‌‌‌  -compile:统计编译行为信息
‌‌‌  -gccapacity:统计不同 generations(新生代,老年代,持久代)的 heap容量情况
‌‌‌  -gccause:统计引起 gc的事件
‌‌‌  -gcnew:统计 gc时,新生代的情况
‌‌‌  -gcnewcapacity:统计 gc时,新生代 heap容量
‌‌‌  -gcold:统计 gc时,老年代的情况
‌‌‌  -gcoldcapacity:统计 gc时,老年代 heap容量
‌‌‌  -gcpermcapacity:统计 gc时, permanent区 heap容量



‌  1. 垃圾回收统计


‌‌‌  命令

‌‌‌  jstat -gc pid(线程id)

‌‌‌  命令后续可以跟参数 比如 jstat -gc pid 1000 10 代表每间隔一秒执行1次,共10次。

‌‌‌  最常用,可以评估程序内存使用及GC压力整体情况。

在这里插入图片描述

‌‌‌  1. S0C:第一个S区的大小,单位KB。

‌‌‌  2. S1C:第二个S区的大小。

‌‌‌  3. S0U:第一个S区的使用大小。

‌‌‌  4. S1U:第二个S区的使用大小。

‌‌‌  5. EC:Eden区的大小。

‌‌‌  6. EU:Eden区的使用大小。

‌‌‌  7. OC:老年代大小。

‌‌‌  8.OU:老年代使用大小。

‌‌‌  9. MC:方法区大小(元空间)。

‌‌‌  10. MU:方法区使用大小。

‌‌‌  11. CCSC:压缩类空间大小。

‌‌‌  12. CCSU:压缩类空间使用大小。

‌‌‌  13. YGC:年轻代垃圾回收次数。

‌‌‌  14. YGCT:年轻代垃圾回收总共消耗时间,单位s。

‌‌‌  15. FGC:老年代垃圾回收次数 。

‌‌‌  16. FGCT:老年代垃圾回收总共消耗时间,单位s。

‌‌‌  17. GCT:垃圾回收消耗总时间,单位s。


‌  2. 堆内存统计


‌‌‌  命令

‌‌‌  jstat -gccapacity pid (线程id)

‌‌‌  
‌‌‌  最常用

在这里插入图片描述

‌‌‌  1. NGCMN:新生代最小容量。

‌‌‌  2. NGCMX:新生代最大容量。

‌‌‌  3. NGC:当前新生代的容量。

‌‌‌  4. S0C:第一个S区大小。

‌‌‌  5. S1C:第二个S区的大小。

‌‌‌  6. EC:Eden区的大小。

‌‌‌  7. OGCMN:老年代最小容量。

‌‌‌  8. OGCMX:老年代最大容量。

‌‌‌  9. OGC:当前老年代大小。

‌‌‌  10. OC: 当前老年代大小。

‌‌‌  11. MCMN: 最小元空间容量。

‌‌‌  12. MCMX:最大元空间容量。

‌‌‌  13. MC:当前元空间大小。

‌‌‌  14. CCSMN:最小压缩类空间大小。

‌‌‌  15. CCSMX:最大压缩类空间大小。

‌‌‌  16. CCSC:当前压缩类空间大小。

‌‌‌  17. YGC:年轻代GC次数。

‌‌‌  18. FGC:老年代GC次数。


‌  3. 新生代垃圾回收统计


‌  命令

‌‌‌  jstat - gcnew pid (线程id)

在这里插入图片描述

‌‌‌  1. S0C:第一个S区的大小。

‌‌‌  2. S1C:第二个S区的大小。

‌‌‌  3. S0U:第一个S区的使用大小。

‌‌‌  4. S1U:第二个S区的使用大小。

‌‌‌  5. TT: 对象在新生代存活的次数。

‌‌‌  6. MTT: 对象在新生代存活的最大次数。

‌‌‌  7. DSS: 期望的S区大小。

‌‌‌  8. EC:Eden区的大小。

‌‌‌  9. EU:Eden区的使用大小。

‌‌‌  10. YGC:年轻代垃圾回收次数。

‌‌‌  11. YGCT:年轻代垃圾回收消耗时间。


  1. 新生代内存统计


‌‌‌  jstat - gc newcapacity pid (线程id)

‌‌‌  1. NGCMN:新生代最小容量。

‌‌‌  2. NGCMX:新生代最大容量。

‌‌‌  3. NGC:当前新生代容量。

‌‌‌  4. S0CMX:第一个S区最大大小。

‌‌‌  5. S0C:第一个S区大小。

‌‌‌  6. S1CMX:第二个S2区最大大小。

‌‌‌  7. S1C:第二个S2区最大大小。

‌‌‌  8. ECMX:Eden区最大大小。

‌‌‌  9. EC:当前Eden区大小。

‌‌‌  10. YGC:年轻代垃圾回收次数。

‌‌‌  11. FGC:老年代回收次数。


‌‌‌  5. 老年代垃圾回收统计



‌‌‌  jstat -gcold pid (线程id)

‌‌‌  1. MC:方法区大小。

‌‌‌  2. MU:方法区使用大小。

‌‌‌  3. CCSC:压缩类空间大小(比如压缩指针)。

‌‌‌  4. CCSU:压缩类空间使用大小。

‌‌‌  5. OC:老年代大小。

‌‌‌  6. OU:老年代使用大小。

‌‌‌  7. YGC:年轻代垃圾回收次数。

‌‌‌  8. FGC:老年代垃圾回收次数。

‌‌‌  9. FGCT:老年代垃圾回收消耗时间。

‌‌‌  10. GCT:垃圾回收消耗总时间。


‌‌‌  6. 老年代内存统计



‌‌‌  jstat -gcoldcapacity pid(线程id)

在这里插入图片描述

‌‌‌  1. OGCMN:老年代最小容量。

‌‌‌  2. OGCMX:老年代最大容量。

‌‌‌  3. OGC:当前老年代大小。

‌‌‌  4. OC:老年代大小。

‌‌‌  5. YGC:年轻代垃圾回收次数。

‌‌‌  6. FGC:老年代垃圾回收次数。

‌‌‌  7. FGCT:老年代垃圾回收消耗时间。

‌‌‌  8. GCT:垃圾回收消耗总时间。


‌‌‌  7. 元数据空间统计



‌‌‌  jstat -gcmetacapacity  pid(线程id)

‌‌‌  1. MCMN:最小元数据容量。

‌‌‌  2. MCMX:最大元数据容量。

‌‌‌  3. MC:当前元数据空间大小

‌‌‌  4. CCSMN:最小压缩类空间大小。

‌‌‌  5. CCSMX:最大压缩类空间大小。

‌‌‌  6. CCSC:当前压缩类空间大小。

‌‌‌  7. YGC:年轻代垃圾回收次数。

‌‌‌  8. FGC:老年代垃圾回收次数。

‌‌‌  9. FGCT:老年代垃圾回收消耗时间。

‌‌‌  10. GCT:垃圾回收消耗总时间。

‌‌‌  8. 统计堆使用比例和垃圾回收次数


‌‌‌  jstat -gcutil  pid(线程id)

在这里插入图片描述

‌‌‌  1. S0:第一个S区当前使用比例。

‌‌‌  2. S1:第二个S区当前使用比例。

‌‌‌  3. E:Eden区使用比例。

‌‌‌  4. O:老年代使用比例。

‌‌‌  5. M:元数据区使用比例。

‌‌‌  6. CCS:压缩使用比例。

‌‌‌  7. YGC:年轻代垃圾回收次数。

‌‌‌  8. FGC:老年代垃圾回收次数。

‌‌‌  9. FGCT:老年代垃圾回收消耗时间。

‌‌‌  10. GCT:垃圾回收消耗总时间。


2. jvisualvm进行远程连接调试


‌‌‌  如果远程服务器开启了jvm进程,可以通过jvisualvm进行远程链接,生产不可能这么用

‌‌‌  运行程序时候需要注册端口和连接地址,才能用jvisualvm进行远程连接(其实使用是jmx的rmi方式连接)。

‌‌‌  如下面借助jar包启动一个程序注册端口和连接地址, jvisualvm就能进行远程连接调试。
‌‌‌

‌‌‌  -Dcom.sun.management.jmxremote.port 为远程机器的JMX端口
‌‌‌  -Djava.rmi.server.hostname 为远程机器IP


‌‌‌  java -Dcom.sun.management.jmxremote.port=8888 -Djava.rmi.server.hostname=192.168.65.60 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -jar microservice-eureka-server.jar

‌‌‌  tomcat情况下的JMX配置:在catalina.sh文件里的最后一个JAVA_OPTS的赋值语句下一行增加如下配置行记住是赋值语句后面

‌‌‌  JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=8888 -Djava.rmi.server.hostname=192.168.50.60 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"

‌‌‌  连接时记得确认下端口是否通畅,可以临时关闭下防火墙或者开放端口


‌‌‌  systemctl stop firewalld   #临时关闭防火墙


3. JVM运行情况预估


‌‌‌  用 jstat gc -pid 命令可以计算出如下一些关键数据,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。


3.1. 查看年轻代对象增长的速率


‌‌‌  可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。


3.2.查看Young GC的触发频率和每次耗时


‌‌‌  知道年轻代对象增长速率就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC 公式算出,大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。


‌‌‌3.3. 每次Young GC后有多少对象存活和进入老年代


‌‌‌  这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden,survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率


3.4. Full GC的触发频率和每次耗时


‌‌‌  知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。

‌‌‌  优化思路其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。


4. 系统频繁Full GC导致系统卡顿分析例子


‌‌‌  机器配置:2核4G。
‌‌‌  JVM内存大小:2G
‌‌‌  系统运行时间:7天
‌‌‌  期间发生的Full GC次数和耗时:500多次,200多秒
‌‌‌  期间发生的Young GC次数和耗时:1万多次,500多秒
‌‌‌  大致算下来每天会发生70多次Full GC,平均每小时3次(正常几天一次),每次Full GC在400毫秒左右。
‌‌‌  每天会发生1000多次Young GC,每分钟会发生1次,每次Young GC在50毫秒左右。

‌‌‌  JVM参数设置如下

‌‌‌  1. -Xms设置堆初始大小。
‌‌‌  2. -Xmx设置堆最大大小。
‌‌‌  3. -Xmn设置新生代的代谢。
‌‌‌  4. -Xss设置每个线程的线程栈大小。
‌‌‌  5. -XX:SurvivorRatio设置一个s区占Eden区的比例。
‌‌‌  6. -XX:MetaspaceSize设置元空间大小
‌‌‌  7. -XX:MaxMetaspaceSize元空间最大大小。
‌‌‌  8. -XX:+UseParNewGC 年轻代垃圾回收使用ParNew。
‌‌‌  9. -XX:+UseConcMarkSweepGC 代表启用CMS收集老年代
‌‌‌  10. -XX:CMSInitiatingOccupancyFraction:当老年代使用达到该比例时会触发OldGC(CMS的GC,避免并发失败)。
‌‌‌  11. -XX:+UseCMSInitiatingOccupancyOnly:开启表示只使用设定的回收阈值( 配合参数-XX:CMSInitiatingOccupancyFraction设定的值使用)。如果不开启,JVM仅在第一次使用设定值,后续则会自动调整。


‌‌‌  -Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=6  -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M 

‌‌‌  -XX:+UseParNewGC  -XX:+UseConcMarkSweepGC  -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly 

‌‌‌  **这边使用的是CMS垃圾收集器,老年代满其实做的是Old GC只不过习惯叫Full GC,严格来说Full GC触发跟垃圾回收器有关,比如CMS并发模式

‌‌‌  分析

‌‌‌  结合上面运行情况,计算平均值,不用考虑峰值情况,示意图如下。

‌‌‌  每分钟一次Young GC可知Eden区一分钟满一次,每二十分钟发生一次Old GC推测老年代二十分钟满一次,根据设置条件70%时候就算满了,所以一分钟有700多M对象进入老年代。

在这里插入图片描述

‌‌‌  结合对象挪动到老年代那些规则推理下这个程序可能存在的一些问题。

‌‌‌  1. 大对象直接进入老年代,基本熟悉项目会知道,首先排除。

‌‌‌  2. 长期存活对象进入老年代,基本都是缓存那些对象,基本不肯会增加,一般的对象都是GC时候清理掉,不可能放到老年代。

‌‌‌  3. 假设系统压力大,每次Young GC 前几秒的创建的对象还被引用着(比如还没完整创建完,比如根据业务赋值等),则推测Young GC后就有30多M进入S区。在新生代GC后这块S区存活对象大于当前区域50%则就会触发动态年龄判断机制,这批对象进入老年代。

‌‌‌  4. 再看老年代空间担保机制,基本不可能。

‌‌‌  经过分析感觉可能是由于对象动态年龄判断机制导致Old GC较为频繁。

对应示例

‌‌‌  这边不好传示例代码,大体看个分析流程。,配置上面jvm参数,启动程序之后根据进程pid,打印jstat的结果如下。


‌‌‌  jstat -gc 13456 2000 10000

‌‌‌  字段数据太长会出现偏移,别看错。

在这里插入图片描述
  分析

‌‌‌  启动一次做一次YounGC和Full GC很正常。

‌‌‌  对于对象动态年龄判断机制导致的Full GC较为频繁可以先试着优化下JVM参数,把年轻代适当调大点调大老年代满GC的比例,没有高并发可以不考虑CMS的并发清理失败不用调比例


‌‌‌  -Xms1536M -Xmx1536M -Xmn1024M -Xss256K -XX:SurvivorRatio=6  -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M 
‌‌‌  -XX:+UseParNewGC  -XX:+UseConcMarkSweepGC  -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly 



‌‌‌  优化完发现没什么变化,Full GC的次数比Minor GC的次数还多了。

在这里插入图片描述

推测下Full GC比Minor GC还多的原因

‌‌‌  1. 元空间不够导致的多余Full GC,可以看元空间使用。

‌‌‌  2. 代码里头显示调用System.gc()造成多余的Full GC,这种一般线上尽量通过-XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那么代码中调用System.gc()就不会在GC。

‌‌‌  3. 老年代空间分配担保机制。如果Minor GC之前因为机制先触发Full GC。后续如果触发一次Minor GC,然后可能老年代满又触发Full GC,但是这种情况不会OOM。

‌‌‌  最快速度分析完这些我们推测的原因以及优化后,发现Young GC和Full GC依然很频繁了,而且看到有大量的对象频繁的被挪动到老年代。

‌‌‌  1. 第一种方式查看对象占用总字节数和生产实例数

‌‌‌  可以借助jmap命令大概看下是什么对象

在这里插入图片描述

‌‌‌  jvisualvm启动—选择运行的进程—抽样器—内存(可以看实例数量),就是间隔调用jmp命令。

‌‌‌  根据对象的字节总数和数量,查到了有大量User对象产生,这个可能是问题所在,但不确定,还必须找到对应的代码确认。

‌‌‌  代码里全文搜索生成User对象的地方(适合只有少数几处地方的情况)。

‌‌‌  第二种查看代码cpu占用时间

‌‌‌  如果生成User对象的地方太多,无法定位具体代码,我们可以同时分析下占用cpu较高的线程,一般有大量对象不断产生,对应的方法代码肯定会被频繁调用,占用的cpu必然较高。

‌‌‌  可以用上面讲过的jstack或jvisualvm—抽样器—cpu来定位cpu占用时间长的代码。

在这里插入图片描述

‌‌‌  最终定位到的代码如下:

‌‌‌  import java.util.ArrayList;

‌‌‌  @RestController
‌‌‌  public class IndexController {

    @RequestMapping("/user/process")
    public String processUserData() throws InterruptedException {
        ArrayList<User> users = queryUsers();

        for (User user: users) {
            //TODO 业务处理
            System.out.println("user:" + user.toString());
        }
        return "end";
    }

    /**
     * 模拟批量查询用户场景
     * @return
     */
    private ArrayList<User> queryUsers() {
        ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 5000; i++) {
            users.add(new User(i,"zhuge"));
        }
        return users;
    }
‌‌‌  }

‌‌‌  同时,java的代码也是需要优化的,一次查询出500M的对象出来,明显不合适,要根据之前说的各种原则尽量优化到合适的值,尽量消除这种朝生夕死的对象导致的Full GC。

5. 内存泄露问题

‌‌‌  之前用了对象没有回收,占用内存,这是一种常见的内存泄漏。

‌‌‌  一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存,大多数同学可能为了图方便对于JVM级缓存就简单使用一个hashmap,于是不断往里面放缓存数据,但是很少考虑这个map的容量问题,结果这个缓存map越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁,这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致Full GC,还有可能导致OOM。

‌‌‌  这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存,自动淘汰旧数据。

  • 22
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值