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了。说明这次优化是有效的。

相关文章
|
29天前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
2月前
|
监控 算法 Java
jvm-48-java 变更导致压测应用性能下降,如何分析定位原因?
【11月更文挑战第17天】当JVM相关变更导致压测应用性能下降时,可通过检查变更内容(如JVM参数、Java版本、代码变更)、收集性能监控数据(使用JVM监控工具、应用性能监控工具、系统资源监控)、分析垃圾回收情况(GC日志分析、内存泄漏检查)、分析线程和锁(线程状态分析、锁竞争分析)及分析代码执行路径(使用代码性能分析工具、代码审查)等步骤来定位和解决问题。
|
2月前
|
Arthas Prometheus 监控
监控堆外使用JVM工具
监控堆外使用JVM工具
44 7
|
2月前
|
监控 Java 编译器
Java虚拟机调优指南####
本文深入探讨了Java虚拟机(JVM)调优的精髓,从内存管理、垃圾回收到性能监控等多个维度出发,为开发者提供了一系列实用的调优策略。通过优化配置与参数调整,旨在帮助读者提升Java应用的运行效率和稳定性,确保其在高并发、大数据量场景下依然能够保持高效运作。 ####
35 1
|
2月前
|
存储 算法 Java
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
|
2月前
|
监控 Java 测试技术
Elasticsearch集群JVM调优垃圾回收器的选择
Elasticsearch集群JVM调优垃圾回收器的选择
59 1
|
2月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
2月前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
2月前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
3月前
|
Arthas 监控 数据可视化
JVM进阶调优系列(7)JVM调优监控必备命令、工具集合|实用干货
本文介绍了JVM调优监控命令及其应用,包括JDK自带工具如jps、jinfo、jstat、jstack、jmap、jhat等,以及第三方工具如Arthas、GCeasy、MAT、GCViewer等。通过这些工具,可以有效监控和优化JVM性能,解决内存泄漏、线程死锁等问题,提高系统稳定性。文章还提供了详细的命令示例和应用场景,帮助读者更好地理解和使用这些工具。