jvm调优工具及案例分析 (下)

简介: jvm调优工具及案例分析 (下)

五、Jinfo



Jinfo命令主要用来查看jvm参数


1. 查看当前运行的jvm参数


jinfo -flags 线程id


执行结果:

1187916-20211111173033963-1905536341.png

从结果可以看出,我们使用的是CMS+Parallel垃圾收集器


2. 查看java系统参数


jinfo -sysprops 进程id

执行结果:

Java System Properties:
#Thu Nov 11 17:28:19 CST 2021
java.runtime.name=OpenJDK Runtime Environment
java.protocol.handler.pkgs=org.springframework.boot.loader
sun.boot.library.path=/data/java/jdk8/jre/lib/amd64
java.vm.version=25.40-b25
java.vm.vendor=Oracle Corporation
java.vendor.url=http\://java.oracle.com/
path.separator=\:
java.vm.name=OpenJDK 64-Bit Server VM
file.encoding.pkg=sun.io
user.country=CN
sun.java.launcher=SUN_STANDARD
sun.os.patch.level=unknown
java.vm.specification.name=Java Virtual Machine Specification
user.dir=/data/temp
java.runtime.version=1.8.0_41-b04
java.awt.graphicsenv=sun.awt.X11GraphicsEnvironment
java.endorsed.dirs=/data/java/jdk8/jre/lib/endorsed
os.arch=amd64
java.io.tmpdir=/tmp
line.separator=\n
java.vm.specification.vendor=Oracle Corporation
os.name=Linux
sun.jnu.encoding=UTF-8
java.library.path=/usr/java/packages/lib/amd64\:/usr/lib64\:/lib64\:/lib\:/usr/lib
java.specification.name=Java Platform API Specification
java.class.version=52.0
sun.management.compiler=HotSpot 64-Bit Tiered Compilers
os.version=5.10.23-5.al8.x86_64
user.home=/root
user.timezone=Asia/Shanghai
java.awt.printerjob=sun.print.PSPrinterJob
file.encoding=UTF-8
java.specification.version=1.8
user.name=root
java.class.path=chapter1-jvm-0.0.1-SNAPSHOT.jar
java.vm.specification.version=1.8
sun.java.command=chapter1-jvm-0.0.1-SNAPSHOT.jar
java.home=/data/java/jdk8/jre
sun.arch.data.model=64
user.language=zh
java.specification.vendor=Oracle Corporation
awt.toolkit=sun.awt.X11.XToolkit
java.vm.info=mixed mode
java.version=1.8.0_41
java.ext.dirs=/data/java/jdk8/jre/lib/ext\:/usr/java/packages/lib/ext
sun.boot.class.path=/data/java/jdk8/jre/lib/resources.jar\:/data/java/jdk8/jre/lib/rt.jar\:/data/java/jdk8/jre/lib/sunrsasign.jar\:/data/java/jdk8/jre/lib/jsse.jar\:/data/java/jdk8/jre/lib/jce.jar\:/data/java/jdk8/jre/lib/charsets.jar\:/data/java/jdk8/jre/lib/jfr.jar\:/data/java/jdk8/jre/classes
java.vendor=Oracle Corporation
file.separator=/
java.vendor.url.bug=http\://bugreport.sun.com/bugreport/
sun.io.unicode.encoding=UnicodeLittle
sun.cpu.endian=little
sun.cpu.isalist=

六、Jstat使用



Jstat命令是jvm调优非常重要,且非常有效的命令。我们来看看她的用法:


1. 垃圾回收统计 jstat -gc


jstat -gc 进程id

这个命令非常常用,在线上有问题的时候,可以通过这个命令来分析问题。


下面我们来测试一下,启动一个项目,然后在终端驶入jstat -gc 进程id,得到如下结果:

1187916-20211112164736562-1107523060.png


上面的参数分别是什么意思呢?先识别参数的含义,然后根据参数进行分析


  • S0C: 第一个Survivor区的容量
  • S1C: 第二个Survivor区的容量
  • S0U: 第一个Survivor区已经使用的容量
  • S1U:第二个Survivor区已经使用的容量
  • EC: 新生代Eden区的容量
  • EU: 新生代Eden区已经使用的容量
  • OC: 老年代容量
  • OU:老年代已经使用的容量
  • MC: 方法区大小(元空间)
  • MU: 方法区已经使用的大小
  • CCSC:压缩指针占用空间
  • CCSU:压缩指针已经使用的空间
  • YGC: YoungGC已经发生的次数
  • YGCT: 这一次YoungGC耗时
  • FGC: Full GC发生的次数
  • FGCT: Full GC耗时
  • GCT: 总的GC耗时,等于YGCT+FGCT

连续观察GC变化的命令


jstat -gc 进程ID 间隔时间  打印次数

举个例子:我要打印10次gc信息,每次间隔1秒


jstat -gc 进程ID 1000 10

1187916-20211112171023928-44170232.png

这样就连续打印了10次gc的变化,每次隔一秒。


这个命令是对整体垃圾回收情况的统计,下面将会差分处理。


2.堆内存统计


这个命令是打印堆内存的使用情况。


jstat -gccapacity 进程ID

1187916-20211112174139395-715365764.png

  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:当前新生代容量
  • S0C:第一个Survivor区大小
  • S1C:第二个Survivor区大小
  • EC:Eden区的大小
  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:当前老年代大小
  • OC: 当前老年代大小
  • MCMN: 最小元数据容量
  • MCMX:最大元数据容量
  • MC:当前元数据空间大小
  • CCSMN:最小压缩类空间大小
  • CCSMX:最大压缩类空间大小
  • CCSC:当前压缩类空间大小
  • YGC:年轻代gc次数
  • FGC:老年代GC次数


3.新生代垃圾回收统计


命令:


jstat -gcnew 进程ID [ 间隔时间  打印次数]


这个指的是当前某一次GC的内存情况

1187916-20211112174721317-912134699.png

  • S0C:第一个Survivor的大小
  • S1C:第二个Survivor的大小
  • S0U:第一个Survivor已使用大小
  • S1U:第二个Survivor已使用大小
  • TT: 对象在新生代存活的次数
  • MTT: 对象在新生代存活的最大次数
  • DSS: 期望的Survivor大小
  • EC:Eden区的大小
  • EU:Eden区的使用大小
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间


4. 新生代内存统计


jstat -gcnewcapacity 进程ID


1187916-20211112180001851-1158415520.png


参数含义:


  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:当前新生代容量
  • S0CMX:Survivor 1区最大大小
  • S0C:当前Survivor 1区大小
  • S1CMX:Survivor 2区最大大小
  • S1C:当前Survivor 2区大小
  • ECMX:最大Eden区大小
  • EC:当前Eden区大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代回收次数


5. 老年代垃圾回收统计


命令:


jstat -gcold 进程ID

1187916-20211112180515374-586913543.png

参数含义:


  • MC:方法区大小
  • MU:方法区已使用大小
  • CCSC:压缩指针类空间大小
  • CCSU:压缩类空间已使用大小
  • OC:老年代大小
  • OU:老年代已使用大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间,新生代+老年代


6. 老年代内存统计


命令:


jstat -gcoldcapacity 进程ID


1187916-20211112180805153-654037985.png

参数含义:


  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:当前老年代大小
  • OC:老年代大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间


7. 元数据空间统计


命令


jstat -gcmetacapacity 进程ID

1187916-20211112180954514-1304020207.png

  • MCMN:最小元数据容量
  • MCMX:最大元数据容量
  • MC:当前元数据空间大小
  • CCSMN:最小指针压缩类空间大小
  • CCSMX:最大指针压缩类空间大小
  • CCSC:当前指针压缩类空间大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间


8.整体运行情况


命令

jstat -gcutil 进程ID


1187916-20211112181313350-215766105.png


  • S0:Survivor 1区当前使用比例
  • S1:Survivor 2区当前使用比例
  • E:Eden区使用比例
  • O:老年代使用比例
  • M:元数据区使用比例
  • CCS:指针压缩使用比例
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间


七、案例分析



1. JVM运行情况预估


现在有一个线上的异常情况。具体的详情如下;

  • 机器配置:2核4G
  • JVM内存大小:2G
  • 系统运行时间:7天
  • 期间发生的Full GC次数和耗时:500多次,200多秒
  • 期间发生的Young GC的次数和耗时:1万多次,500多秒。

如何能够知道系统运行期间发生了多少次young gc和多少次full gc,并且他们的耗时是多少呢?使用如下命令:


jstat -gcutil 进程ID


然后就可以看到程序运行的结果了;

1187916-20211117105554009-610366005.png


这几个参数的具体含义是什么呢?

  • S0:Survivor 1区当前使用比例
  • S1:Survivor 2区当前使用比例
  • E:Eden区使用比例
  • O:老年代使用比例
  • M:元数据区使用比例
  • CCS:指针压缩使用比例
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

2. JVM优化的思路

JVM优化的目标其实主要是Full GC。只要不发生Full GC,基本就不会出现OOM。所以如何优化Full GC就是我们的目标。往前推,老年代的对象是怎么来的呢?从新生代来的,那么我们就要避免朝生夕死的新生代对象进入到老年代。

1) 分析GC数据:

期间发生的Full GC次数和耗时:500多次,200多秒。那么平均7 * 24 * 3600秒/500 = 20分钟发生一次Full GC, 每次full GC耗时:200秒/500=400毫秒;

期间发生的Young GC次数和耗时:1万多次,500多秒,那么平均7 * 24 * 3600秒/10000 = 60秒也就是1分钟发生一次young GC,每次young GC耗时:500/10000=50毫秒;

其实,从full GC和young GC的时间来看,还好,不太长。主要是发生的频次,full gc发生的频次太高了,20分钟一次,通常我们的full gc怎么也要好几个小时触发一次,甚至1天才触发一次。而young gc触发频次也过于频繁,1分钟触发一次。


2)梳理内存模型


根据上述信息,我们可以画一个内存模型出来。


先来看看原系统的JVM参数配置信息


‐Xms1536M ‐Xmx1536M ‐Xmn512M ‐Xss256K ‐XX:SurvivorRatio=6 ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M 2 ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC ‐XX:CMSInitiatingOccupancyFraction=75 ‐XX:+UseCMSInitiatingOccupancyOnly
  • 堆空间:1.5G
  • 新生代:512M
  • 线程大小:256k
  • ‐XX:SurvivorRatio新生代中Eden区域和Survivor区域:6。也就是Eden:S0: S1 = 6:1:1
  • 元数据空间:256M
  • 采用的垃圾收集器:CMS
  • -XX:CMSInitiatingOccupancyFraction=75:CMS在对内存占用率达到75%的时候开始GC
  • -XX:+UseCMSInitiatingOccupancyOnly: 只是用设定的回收阈值(上面指定的70%), 如果不指定, JVM仅在第一次使用设定值, 后续则自动调整.


根据参数我们梳理如下内存模型。堆内存空间都分配好了,那么上面说了每过60s触发一次Young GC,那么就是说,平均每秒会产生384/60=6.4M的垃圾。而老年代,每过20分钟就会触发一次GC,而老年代可用的内存空是0.75G,也就是750多M。


1187916-20211117184106471-501355368.png


现在的问题,为什么每过20分钟,就会有750M的对象挪到老年代呢?解决了这个问题,我们就可以阻止对象挪到老年代


结合对象挪动到老年的规则分析这个模型可能会有哪些问题:


  1. 大对象
  2. 顽固的对象
  3. 动态年龄判断机制
  4. 老年代空间担保机制
  • 首先分析:我们的系统里会不会有大对象。其实代码使我们自己写的,我们知道里面没有特别大的对象。在年轻代放不下了,直接进入老年代,所以这种情况排除
  • 第二个顽固的对象:我们这里通常都是朝生夕死的对象,顽固的对象就是系统的那些对象,如果是系统对象,也不应该是每过20分钟都会产生700M老顽固对象啊。
  • 第三个动态年龄判断机制:一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了。这个是有可能的,可能在垃圾收集的时候,Survivor区放不下了,那么就会直接放入到老年代。


既然这种情况有可能,那我们就来分析一下:


线程每秒中产生6M多的垃圾,如果并发量比较大的时候, 处理速度比较慢,可能1s处理不完,假设处理完数据要四五秒,就按5s来算,那一秒就可能产生30M的垃圾,这时候触发Dden区垃圾回收的时候,这30M的垃圾要进入到S1区,而S1区很可能本身就有一部分对象了,再加上这30M就大于S1区的一半了,直接进入老年代。


这只是一种可能。


  • 第四个触发老年代空间担保机制:其实触发老年代空间担保机制的概率很小,通常都是老年代空间很小的会后,会触发。我们这里老年代比较大,所以基本不可能。


综上所述,现在最有可能频繁触发GC的可能的原因是动态年龄判断机制。我们之前在做优化的时候,遇到过。可以将Survivor区域放大一点,就可以了。


3. 案例模拟分析


我们用下面这个案例来模拟分析上述情况。分析找到问题。


第一步:启动主程序


  • 这时一个springboot的web程序,内容很简单。创建项目的时候注意选择web就可以了
  • 然后里面定义了一个User对象。这个User对象比较特别,里面有个参数a分配0.1M的内存空间。


package com.jvm;
public class User {
  private int id;
  private String name;
  byte[] a = new byte[1024*100];
......
}
  • 接下来定义了一个接口类
package com.jvm;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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;
    }
}

接口类很简单,每次调用接口,先创建5000个用户,然后让这5000个用户区执行各自的业务逻辑。需要注意的是5000个用户占用内存空间约500M。也就是说,每次调用这个接口,都会产生500M的对象。


  • 然后定义一个测试类,测试类


@RunWith(SpringRunner.class)
@SpringBootTest(classes={Application.class})// 指定启动类
public class ApplicationTests {
  @Bean
  public RestTemplate restTemplate() {
    return new RestTemplate();
  }
  @Autowired
  private RestTemplate restTemplate;
  @Test
  public void test() throws Exception {
    for (int i = 0; i < 10000; i++) {
      String result = restTemplate.getForObject("http://localhost:8080/user/process", String.class);
      Thread.sleep(1000);
    }
  }
}

测试类很简单,就是手动调用上面的接口。循环调用10000次。如果启动10000次的话,

  • 启动主程序


@SpringBootApplication
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
  @Bean
  public RestTemplate restTemplate() {
    return new RestTemplate();
  }
}

第二步:配置jvm参数


我们要模拟线上的情况,所以参数也设置和线上一样的情况。


-Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly

第三步:使用jstat gc命令观察垃圾收集情况


程序启动起来以后,可以使用jps命令查看进程,输入命令查看gc触发情况


jstat -gc 进程ID   [间隔时间  触发次数]
jstat -gc 8620 1000  10000
表示观察8620这个进程,每隔1s打印一次gc情况,连续打印10000次


1187916-20211117154454101-700877733.png

我们看到,程序启动以后都是出发了4次young gc, 1次full gc。程序启动触发gc都是ok的。后面基本没有什么垃圾产生了。


第四步:启动test代码,调用process接口


这一步没啥说的,直接启动程序就可以了


第五步:观察终端gc的变化


为了保险期间,我们查看一下运行的参数是不是我们配置的参数


jinfo -flags 8620


image.png

程序启动以后,我们发现频发的触发了gc,新生代gc触发很频繁,老年代也很频繁。老年代gc触发那么频繁,那就是有问题了。根据上面的分析,最有可能的情况是动态年龄分配机制。可能产生的对象在survivor区放不下,直接进入老年代 。处理这个问题的方法是,扩大年轻代空间。


第六步:优化1:增加新生代内存空间,以及老年代触发gc的比例


  • 新生代空间扩大到1G
  • 老年代空间缩小为0.5G
  • 还要重新设置一个参数就是CMSInitiatingOccupancyFraction,原来是是75,这个参数是为了防止触发老年代空间担保机制。但这样会有25%的空间基本是空闲的。当很少触发full gc的时候,这个值可以缩小一些。


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

内存空间变化后:

1187916-20211117160524755-827935390.png

再次启动项目,看运行结果:

1187916-20211117162303107-726041933.png


基本没有gc触发。说明我们的优化是有效的。然后在启动测试程序:

image.png


这一次我们发现,触发gc的次数相对来说少了,gc的速度相对于上一次小了一些,但是又有新的问题发生:老年代gc比年轻代还有频繁。这是怎么回事呢?有什么情况会让老年代触发gc的频率大于年轻代呢?


这可能会有几种情况


  • 元数据空间不够,导致full gc,不断扩大元数据空间


元数据空间比较好看,我们直接看输出的参数

1187916-20211117163717582-1950766636.png

红框圈出的就是元数据空间和元数据已用空间。我们来看实际使用情况

image.png

通过观察,我们发现元数据大小基本上是不变的。所以,元数据空间不太会增加导致触发full gc。


  • 显式的调用System.gc(),造成多余的full gc触发。

这个情况一般在线上都会进制成都代码触发full gc。量通过­XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果。

  • 触发了老年代空间担保机制

结合之前学习的理论,我们知道,老年代空间担保机制。有可能在触发一次minor GC的时候触发两次Full GC。


来复习一下:

1187916-20211014173551789-494201143.png


年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间。如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象),就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了,如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小


如果上一步结果是小于或者之前说的参数没有设置,那么就会直接触发一次Full GC,然后再触发Minor GC, 如果回收完还是没有足够空间存放新的对象就会发生"OOM"

如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full GC,Full GC完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM”。

在梳理一下这块逻辑,为什么叫担保机制。在触发Minor GC的时候,进行了一个条件判断,预估老年代空间是否能够放的下新生代的对象,如果能够放得下,那么就直接触发Minor GC, 如果放不下,那么先触发Full GC。在触发Full GC的时候设置了担保参数会增加异步判断,而不是直接触发Full GC。判断老年代剩余可用空间 是否小于 历史每次Minor GC后进入老年代对象的平均值。这样的判断可以减少Full GC的次数。因为新生代在触发Full GC以后是会回收一部分内存的,剩余部分再放入老年代,可能就能放下了。


通过回顾,我们看到老年代空间担保机制中,当触发一次Minor GC的时候,有可能会触发两次Full GC。这样就导致Full GC的次数大于Minor GC。


由此可见,我们这次优化是失败的, 还引入了新的问题。这里还有可能是大对象导致的,不一定是非常大的一个对象,也可能是多个对象在一个时刻产生的大对象。


第七步:优化2:查找大对象


我们在查找是否有大对象,或者某一个时间是否有大对象占用较大的内存空间,可以使用命令或者终端查看


jmap -histo 进程ID

1187916-20211117165202701-726181348.png


前面都是系统对象,往下找我们看到一个自定义对象User,这个实例有10000个,占用内存空间240M


或者使用jvisualvm

1187916-20211117170519964-991168952.png


点击内存,就可以实时查看到系统进程的内存占用情况。点击内存其实就是对 【jmap -histo 进程ID】命令的包装


内存占用最多的是byte[]数组,占用了内存的95%。是什么情况让byte数组占用这么多的内存呢?这个通常都是用户自定义对象造成的。往下看,我们看到了User对象,user对象占用了12w字节数据,有5000个实例。


假如这个代码不是我们写的,是别人写的,我们不熟悉。这时候可以通过以下方法定位问题

  • new User()对象不多。可以通过反查定位找出问题。


/**
     * 模拟批量查询用户场景
     * @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;
    }
  • 我们发现,在这里竟然创建了5000个对象。不过5000个对象应该也不多。通常一个对象也就几k,我们进到User里看看


public class User {
  private int id;
  private String name;
  byte[] a = new byte[1024*100];
}
  • 意外发现,User里定义了一个byte数组,一个byte数组占用100k的空间。那问题就是这里了。


  • 如果系统中new User()对象很多,应该怎么办呢?
    系统中有这么多对象,说明什么问题呢?new User()在反复执行,这样的话,cpu占用率应该不低。如果这边不好找,我们可以看看cpu占用情况。

1187916-20211117171921730-1208738903.png


这里的cpu其实就是对命令是对jstack命令的封装【jstack 4013440|grep -A 10 67187778】


通过分析我们看出第一个take()方法占用cpu最高,达到98%,但是这个是什么东西,我们不太熟悉,看看第二个,第二个是queryUsers(),这个是我们自己的方法,可以看看这个方法的具体内容:


/**
     * 模拟批量查询用户场景
     * @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;
    }
public class User {
  private int id;
  private String name;
  byte[] a = new byte[1024*100];
}

刚好就定位到这段代码,我们发现他一下查询了5000个对象,并且每个对象里定义了一个大对象。这样我们就定位到了问题。


所以在查询数据的时候,要注意是否有大对象,如果有大对象的话,需要预估一下内存消耗。剩下就是代码优化的问题了。


我们这里降低查询用户数从一次5000到一次500,然后重启代码试一下:

private ArrayList<User> queryUsers() {
        ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 500; i++) {
            users.add(new User(i,"zhuge"));
        }
        return users;
    }


来看看运行效果


image.png


触发young gc的频率降低了,而且基本不会触发full gc了。说明这次优化是有效的。

相关文章
|
10天前
|
存储 监控 算法
jvm-性能调优(二)
jvm-性能调优(二)
|
3月前
|
Arthas 监控 Java
(十一)JVM成神路之性能调优篇:GC调优、Arthas工具详解及各场景下线上最佳配置推荐
“在当前的互联网开发模式下,系统访问量日涨、并发暴增、线上瓶颈等各种性能问题纷涌而至,性能优化成为了现时代开发过程中炙手可热的名词,无论是在开发、面试过程中,性能优化都是一个常谈常新的话题”。
214 3
|
11天前
|
Kubernetes Java 编译器
解锁极致性能:Quarkus如何让JVM应用调优变得前所未有的简单与高效!
Quarkus是一款专为GraalVM和OpenJDK设计的Kubernetes Native Java框架,采用AOT编译技术将Java应用转化为本地代码,大幅提升启动速度与运行效率。它简化了性能调优流程,如自动优化垃圾回收、类加载、内存管理及线程管理等,使开发者无需深入理解JVM细节即可轻松提升应用性能。与传统JVM应用相比,Quarkus显著降低了性能调优的复杂度。
44 2
|
2月前
|
缓存 监控 算法
吃透 JVM 诊断方法与工具使用
【8月更文挑战第4天】深入了解并掌握JVM诊断需把握几大要点:1) 熟悉JVM内存模型,如堆、栈及方法区;2) 掌握垃圾回收机制与算法;3) 运用工具如`jps`(查看Java进程)、`jstat`(监控运行状态)、`jmap`(生成堆快照)、`jhat`(分析堆快照)、`jstack`(检查线程栈); 4) 利用专业工具如Eclipse Memory Analyzer分析堆转储文件查找内存泄漏; 5) 动态监控与调整JVM参数; 6) 结合日志分析性能瓶颈。通过实战案例加深理解,有效应对JVM性能问题。
|
3月前
|
运维 监控 Java
(十)JVM成神路之线上故障排查、性能监控工具分析及各线上问题排错实战
经过前述九章的JVM知识学习后,咱们对于JVM的整体知识体系已经有了全面的认知。但前面的章节中,更多的是停留在理论上进行阐述,而本章节中则更多的会分析JVM的实战操作。
|
2月前
|
监控 JavaScript Java
JVM源码级别分析G1发生FullGC元凶的是什么
线上系统遭遇频繁Old GC问题,监控显示出现多次“to-space exhausted”日志,这表明垃圾回收过程中因年轻代 Survivor 区或老年代空间不足导致对象晋升失败。通过 JVM 源码分析,此问题源于对象转移至老年代失败时,JVM 无法找到足够的空间存放存活对象。进一步排查发现大对象分配占用了预留空间,加剧了空间不足的情况。使用 JFR 分析工具定位到定期报表序列化导致大量大对象生成,通过改用堆外内存进行序列化输出,最终解决了频繁 Old GC 问题。
|
2月前
|
Arthas Prometheus 监控
使用JDK自带工具调优JVM的常用命令
使用JDK自带工具调优JVM的常用命令
|
2月前
|
监控 Java Android开发
吃透 JVM 诊断方法与工具使用
【8月更文挑战第3天】要精通JVM诊断,需掌握关键监控指标如内存(堆/非堆)、CPU使用及线程状态;熟悉工具如`jstat`(监控状态)、`jmap`(堆转储)、`jstack`(线程堆栈);并能利用Eclipse Memory Analyzer (MAT)分析堆转储找内存泄漏;同时理解GC日志以优化垃圾回收行为;通过实践案例加深理解。
|
2月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
5天前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
15 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制