JVM第7篇-性能监控 & JVM性能调优案例

性能监控

一、JVM监控及诊断工具-命令行篇

1.1 基础故障处理工具

1.1.1 jps: 虚拟机进程状况工具

jps(JVM Process Status Tool),可以列出正在进行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)。虽然功能比较单一,但它绝对是使用频率最高的JDK命令行工具,因为其他的JDK工具大多需要输入它查询到的LVMID来确定要监控的是哪一个虚拟机进程。

命令格式

jps [ options ] [ hostid ]

使用

在这里插入图片描述

1.1.2 jstat: 虚拟机统计信息监视工具

jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机中的类加载、内存、垃圾收集、即时编译等运行时数据,在没有GUI图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。

命令格式

jstat [ option vmid [intervals[s|ms] [count]] ]
参数interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次。假设需要没250ms查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是:
jstat -gc 2764 250 20

使用

在这里插入图片描述

1.1.3 jinfo: Java配置信息工具

jinfo(Configuration Info for Java)的作用是实时查看和调整虚拟机各项参数。使用jps命令的-v参数可以查看虚拟机启动时显式的指定的参数列表,但如果想知道被显示指定的参数的系统的默认值,除了找资料外,就只能使用jinfo的-flag选项进行查询了(如果只限于JDK6 或以上版本的话,使用java -XX:+PrintFlagsFinal查看参数默认值也是一个很好的选择)。jinfo还可以使用-sysprops选项把虚拟机进程的System.getProperties()的内容打印出来。

命令格式

jinfo [ option ] pid

使用

在这里插入图片描述

1.1.4 jmap: Java内存映像分析工具

jmap(Memory Map for java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。如果不使用jmap命令,要想获取Java堆栈转储快照也还有一些比较"暴力"的手段:譬如在第2章中用过的-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件,通过-XX:+HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成堆转储快照文件,又或者在Linux系统下通过Kill -3命令发送进程退出信号"恐吓"一下虚拟机,也能顺利拿到堆转储快照。
jmap的作用并不仅仅是为了获取堆转储快照,它还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。
和jinfo命令一样,jmap有部分功能在Windows平台下是受限的,除了生成堆转储快照的-dump选项和用于查看每个类的实例、空间占用统计的-histo选项在所有操作系统中都可以使用外,其余选项都只能在Linux/Solaris中使用。

命令格式

jmap [ option ] vmid

1.1.5 jstack: Java堆栈跟踪工具

jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待什么资源。

命令格式

jstack [ option ] vmid

使用

在这里插入图片描述

1.1.6 jhat: 虚拟机堆转储快照分析工具

JDK提供jhat(JVM Heap Analysis Tool) 命令与jmap搭配使用,来分析jmap生成的堆转储快照。jhat内置了一个微型的HTTP/Web服务器,生成堆转储快照的分析结果后,可以在浏览器中查看。不过实事求是地说,在实际工作中,除非手上真的没有别的工具可用,否则多数人是不会直接使用jhat命令来分析堆转储快照文件的,主要原因有两个方面。一是一般不会在部署应用程序的服务器上直接分析堆转储快照,即使可以这样做,也会尽量将堆转储快照文件复制到其他机器上进行分析,因为分析工作是一个耗时而且极为耗费硬件资源的过程,既然都要在其他机器上进行,就没有必要再受命令行工具的限制了。另一个原因是jhat的分析功能相对来说比较简陋,后文将介绍到的VisualVM,以及专业用于分析堆转储快照文件的Eclipse Memory Analyzer、IBM HeapAnalyzer等工具,都能实现比jhat更强大专业的分析功能。

二、JVM监控及诊断工具-GUI篇

  • jconsole
  • visualvm
  • eclipse MAT
  • jprofiler(收费)
  • Arthas
  • java mission control

三、调优概述

3.1 生产环境中的问题

  • 发生了内存溢出该如何处理?
  • 给服务器分配多少内存合适?
  • 如何对垃圾收集器的性能进行调优
  • CPU负载飙高该如何处理?
  • 应该给应用分配多少线程合适?
  • 不加log,如何确定请求是否执行了某一行代码?
  • 不加log,如何实时查看某个方法的入参与返回值?

3.2 调优基本问题

为什么要调优?

  • 防止出现OOM,进行JVM规划和预调优
  • 解决程序运行中各种OOM
  • 减少Full GC出现的频率,解决运行慢、卡顿问题

调优的大方向

  • 合理的编写代码
  • 充分并合理的使用硬件资源
  • 合理地进行jvm调优

不同阶段的考虑

  • 上线前
  • 项目运行阶段
  • 线上出现OOM

总结

  • 调优,从业务场景开始,没有业务场景的调优都是耍流氓
  • 无监控,不调优!

3.3 调优监控的依据

  • 运行日志
  • 异常堆栈
  • GC日志
  • 线程快照
  • 堆转储快照

3.4 性能优化的步骤

第1步: 熟悉业务场景

第2步: (发现问题): 性能监控

一种以非强行或者入侵方式收集或查看应用运营性能数据的活动。
监控通常是指一种在生产、质量评估或者开发环境下实施的带有预防或主动性的活动。
当应用相关干系人提出性能问题却没有提供足够多的线索时,首先我们需要进行性能监控,随后是性能分析。
监控前,设置好回收器组合,选定CPU(主频越高越好),设置年代比例,设置日志参数(生产环境中通常不会只设置一个日志文件)。比如

-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCFiles=5 
-XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
  • GC频繁
  • cpu load过高
  • OOM
  • 内存泄漏
  • 死锁
  • 程序响应时间较长

第3步(排查问题):性能分析

  • 打印GC日志,通过GCviewer或者http://gceasy.io来分析日志信息
  • 灵活运用命令行工具,jstack,jmap,jinfo等
  • dump出堆文件,使用内存分析工具分析文件
  • 使用阿里Arthas,或jconsole,JVisualVM,jprofiler,MAT来实时查看JVM状态
  • jstack查看堆栈信息

第4步(解决问题): 性能调优

  • 适当增加内存,根据业务背景选择垃圾收集器
  • 优化代码,控制内存使用
  • 增加机器,分散节点压力
  • 合理设置线程池线程数量
  • 使用中间件提高程序效率,比如缓存,消息队列等
  • 其它…

四、OOM案例

4.1 OOM案例1: 堆溢出

controller层代码

@RestController
public class MemoryTestController {
    
    
    @Autowired
    private PeopleSevice peopleSevice;

    /**
     * 案例1:模拟线上环境OOM
     */
    @RequestMapping("/add")
    public void addObject(){
    
    
        System.err.println("add"+peopleSevice);
        ArrayList<People> people = new ArrayList<>();
        while (true){
    
    
            people.add(new People());
        }
    }


    

    /**
     * 性能优化案例3:合理配置堆内存
     */
    @RequestMapping("/getData")
    public List<People> getProduct(){
    
    
        List<People> peopleList = peopleSevice.getPeopleList();
        return peopleList;
    }
}

案例模拟

发送请求: http://localhost:8080

jvm参数设置

参数设置: -Xms50M -Xmx50M

-XX:+PrintGCDetails -XX:MetaspaceSize=64m
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapdump.hprof
-XX:+PrintGCDateStamps -Xms200M -Xmx200M -Xloggc:log/gc-oomHeap.log

运行结果

在这里插入图片描述

原因及解决方案

原因:

  1. 代码中可能存在大对象分配
  2. 可能存在内存泄漏,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。

解决方法:

  • 检查是否存在大对象的分配,最有可能的是大数组分配
  • 通过jmap命令,把堆内存dump下来,使用MAT等工具分析一下,检查是否存在内存泄漏的问题
  • 如果没有找到明显的内存斜口,使用功能-Xmx加大堆内存
  • 检查是否有大量的自定义的Finalizable对象,也有可能是框架内部提供的,考虑其存在的必要性。

dump文件分析

jvisualvm分析

  • 装入dump文件
    在这里插入图片描述
    在这里插入图片描述
  • 分析dump文件
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

MAT分析

  • 装入dump文件
    在这里插入图片描述
    在这里插入图片描述
  • 分析问题
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

gc日志分析

在这里插入图片描述

4.2 OOM案例2: 元空间溢出

方法区的内存回收目标主要是针对常量池的回收和对类型的卸载。

案例模拟

	/**
     * 案例2:模拟元空间OOM溢出
     */
    @RequestMapping("/metaSpaceOom")
    public void metaSpaceOom(){
    
    
        ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
        while (true){
    
    
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(People.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
    
    
                System.out.println("我是加强类,输出print之前的加强方法");
                return methodProxy.invokeSuper(o,objects);
            });
            People people = (People)enhancer.create();
            people.print();
            System.out.println(people.getClass());
            System.out.println("totalClass:" + classLoadingMXBean.getTotalLoadedClassCount());
            System.out.println("activeClass:" + classLoadingMXBean.getLoadedClassCount());
            System.out.println("unloadedClass:" + classLoadingMXBean.getUnloadedClassCount());
        }
    }

模拟请求: http://localhost/metaSpaceOom

运行结果

在这里插入图片描述

参数配置

-XX:+PrintGCDetails -XX:MetaspaceSize=60m -XX:MaxMetaspaceSize=60m -Xss512k -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapdumpMeta.hprof -XX:SurvivorRatio=8 -XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:+PrintGCDateStamps -Xms60m -Xmx60m -Xloggc:log/gc-oomMeta.log

分析及解决

查看gc状态

在这里插入图片描述

查看gc日志

在这里插入图片描述

分析dump文件

jvisualvm分析

在这里插入图片描述

mat分析

在这里插入图片描述
在这里插入图片描述

解决方案

我们每次是不是可以只加载一个代理类即可,因为我们的需求其实是没有必要如此加载的,当然如果业务上确实需要加载很多类的话,那么我们就要考虑增大方法区大小了,所以修改代码如下:

enhancer.setUseCache(true)

选择为true的话,使用和更新一类具有相同属性生成的类的静态缓存,而不会在同一个类文件还继续被动态加载并视为不同的类,这个其实跟类的equals()和hashCode()有关,它们是与cglib内部的class cache的key相关的。

4.3 OO案例3: GC overhead limit exceeded

案例模拟

示例代码1

public static void test1() {
    
    
        int i = 0;
        List<String> list = new ArrayList<>();
        try {
    
    
            while (true) {
    
    
                list.add(UUID.randomUUID().toString().intern());
                i++;
            }
        } catch (Throwable e) {
    
    
            System.out.println("************i: " + i);
            e.printStackTrace();
            throw e;
        }
    }
JVM配置

-XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapExceeded.hprof -XX:+PrintGCDateStamps -Xms10m -Xmx10m -Xloggc:log/gc-oomExceeded.log

运行结果

在这里插入图片描述

示例代码2

public static void test2() {
    
    
        String str = "";
        Integer i = 1;
        try {
    
    
            while (true) {
    
    
                i++;
                str += UUID.randomUUID();
            }
        } catch (Throwable e) {
    
    
            System.out.println("************i: " + i);
            e.printStackTrace();
            throw e;
        }
    }
JVM配置

-XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/dumpHeap1.hprof -XX:+PrintGCDateStamps -Xms10m -Xmx10m -Xloggc:log/gc-oomHeap1.log

运行结果

在这里插入图片描述

代码解析

第一段代码: 运行期间将内容放入常量池的典型案例
第二段代码: 不停地追加字符串str

分解及解决

第1步: 定位问题代码块
jvisualvm分析(跳过)
mat分析

在这里插入图片描述
在这里插入图片描述

第2步: 分析dump文件直方图

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

第3步: 代码修改

根据业务来修改是否需要死循环。
原因:
这个是JDK6新加的错误类型,一般都是堆太小导致的。 Sun官方对此的定义: 超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有出现真正的内存溢出。

解决方法:

  1. 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
  2. 添加参数 -XX:-UseGCOverheadLimit禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终会出现java.lang.OutOfMemoryErro: Java heap space。
  3. dump内幕才能,检查是否存在内存泄漏,如果没有,加大内存。

4.4 OOM案例4: 线程溢出

问题原因

基本上都是创建了大量的线程导致的

案例模拟

public class TestNativeOutOfMemoryError {
    
    
    public static void main(String[] args) {
    
    
        for (int i = 0; ; i++) {
    
    
            System.out.println("i = " + i);
            new Thread(new HoldThread()).start();
        }
    }
}

class HoldThread extends Thread {
    
    
    CountDownLatch cdl = new CountDownLatch(1);

    @Override
    public void run() {
    
    
        try {
    
    
            cdl.await();
        } catch (InterruptedException e) {
    
    
        }
    }
}

运行结果

在这里插入图片描述

分析及解决

解决方向1

  • 通过-Xss设置每个线程栈大小的容量
  • 能创建的线程数的具体计算公式如下
    (MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
    • MaxProcessMemory 指的是进程可寻址的最大空间
    • JVMMemory JVM内存
    • ReservedOsMemory 保留的操作系统内存
    • ThreadStackSize 线程栈的大小

64位的操作系统中MaxProcessMemory寻址空间2^64=17179869184GB,已经非常大了,创建正常绝对用不完的,但是操作系统有保护机制。

解决方向2

线程总数也受到系统空闲时间内存和操作系统的限制,检查是否该系统下有此限制:

  • /proc/sys/kernel/pid_max 系统最大pid值,在大型系统中可适当调大
  • /proc/sys/kernel/threads-max 系统允许的最大线程数
  • maxuserprocess(ulimit -u) 系统限制某用户下最多可以运行多少进程或线程
  • /proc/sys/vm/max_map_count
    在这里插入图片描述

五、JVM性能优化案例

5.1 性能优化案例1: 调整堆大小提高服务的吞吐量

5.1.1 修改tomcatJVM配置

linux在tomcat的安装目录下的bin目录下,新建setenv.sh,内容如下:

export CATALINA_OPTS="$CATALINA_OPTS -Xms30m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:SurvivorRatio=8"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx30m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseParallelGC"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/www/server/tomcat/logs/gc.log"

测试

  • 启动tomcat(在tomcat安装目录下的bin目录下)
./startup.sh
  • 查看tomcat进程
    在这里插入图片描述

  • 查看tomcat进程的gc信息

jstat -gc 5397 1000 5

在这里插入图片描述

  • jmeter压测50000个请求
    在这里插入图片描述
  • 再次查看gc信息
    在这里插入图片描述
  • 查看gc的log文件,可以看到最下面非常多的Full gc

5.1.2 优化配置

export CATALINA_OPTS="$CATALINA_OPTS -Xms120m"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx120m"
  • 重新启动tomcat
  • 查看tomcat进程
    在这里插入图片描述
  • 查看初始gc信息
jstat -gc 5765 1000 5

在这里插入图片描述

  • jmeter压测50000个请求
    在这里插入图片描述
  • 查看压测后gc信息
    在这里插入图片描述
  • 从结果可以看出,full gc没有变化,只是younggc增加了一点,gc的时间很短,说明用户请求的延迟非常短。

5.2 性能优化案例2: JVM优化之JIT优化

5.2.1 逃逸分析

逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。
逃逸分析的基本原理是: 分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
如果能证明一个对象不会逃逸到方法或线程之外(换句话说是别的方法或线程无法通过任何途径访问到这个对象),或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则则可能为这个对象实例采取不同程度的优化,如:

  • 栈上分配(Stack Allocation): 在Java虚拟机中,Java堆上分配创建对象的内存空间几乎是Java程序员都知道的常识,Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。虚拟机的垃圾收集子系统会回收堆中不再使用的对象,但回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,但不能支持线程逃逸。
  • 标量替换(Scalar Replacement): 若一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,那么这些数据就可以被称为标量。相对的,如果一个数据可以继续分解,那它就被称为聚合量(Aggregate),Java中的对象就是典型的聚合量。如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始的类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。标量替换可以视为栈上分配的一种特例,实现更简单(不用考虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。
  • 同步消除(Synchronization Elimination): 线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其它线程访问,那么这个变量的读写就不会有竞争,对这个变量实施的同步措施也就可以安全的清除掉。

5.2.2 代码举例

public class EscapeAnalysis {
    
    

    public EscapeAnalysis obj;

    /*
    方法返回EscapeAnalysis对象,发生逃逸
     */
    public EscapeAnalysis getInstance(){
    
    
        return obj == null? new EscapeAnalysis() : obj;
    }
    /*
    为成员属性赋值,发生逃逸
     */
    public void setObj(){
    
    
        this.obj = new EscapeAnalysis();
    }
    //思考:如果当前的obj引用声明为static的,会发生逃逸吗?会!

    /*
    对象的作用域仅在当前方法中有效,没有发生逃逸
     */
    public void useEscapeAnalysis(){
    
    
        EscapeAnalysis e = new EscapeAnalysis();
    }
    /*
    引用成员变量的值,发生逃逸
     */
    public void useEscapeAnalysis1(){
    
    
        EscapeAnalysis e = getInstance();
        //getInstance().xxx()同样会发生逃逸
    }
    /*
    * 也发生了逃逸
    * */
    public void operate(EscapeAnalysis e){
    
    
        // e
    }
}

5.3.4 代码示例及优化

优化一: 栈上分配

//只要开启了逃逸分析,就会判断方法中的变量是否发生了逃逸。如果没有发生了逃逸,则会使用栈上分配
public class StackAllocation {
    
    
    public static void main(String[] args) {
    
    
        long start = System.currentTimeMillis();

        for (int i = 0; i < 10000000; i++) {
    
    
            alloc();
        }
        // 查看执行时间
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为: " + (end - start) + " ms");
        // 为了方便查看堆内存中对象个数,线程sleep
        try {
    
    
            Thread.sleep(1000000);
        } catch (InterruptedException e1) {
    
    
            e1.printStackTrace();
        }
    }

    private static void alloc() {
    
    
        User user = new User();//是否发生逃逸? 没有!
    }

    static class User {
    
    

    }
}

  • 默认逃逸分析是开启的,我们先关掉
-Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
  • 查看创建10000000个对象所需时间
    在这里插入图片描述

  • 通过jvisualvm查看对象创建情况
    在这里插入图片描述

  • 开启逃逸分析

-Xmx1G -Xms1G -XX:+PrintGCDetails

在这里插入图片描述
在这里插入图片描述

  • 通过对比我们可以看出开启了逃逸分析和没开启创建实例的情况。

优化二: 同步消除(锁消除)

public class SynchronizedTest {
    
    
    public void f() {
    
    
        /*
        * 代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,
        * 并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。
        *
        * 问题:字节码文件中会去掉hollis吗?
        * */
        Object hollis = new Object();
        synchronized(hollis) {
    
    
            System.out.println(hollis);
        }

        /*
        * 优化后;
        * Object hollis = new Object();
        * System.out.println(hollis);
        * */
    }
}

优化三: 标量替换

/**
 * 标量替换测试
 *  -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:-EliminateAllocations
 *  结论:Java中的逃逸分析,其实优化的点就在于对栈上分配的对象进行标量替换。
 */
public class ScalarReplace {
    
    
    public static class User {
    
    
        public int id;
        public String name;
    }

    public static void alloc() {
    
    
        User u = new User();//未发生逃逸
        u.id = 5;
        u.name = "www.hmxwp123.xyz";
    }

    public static void main(String[] args) {
    
    
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
    
    
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为: " + (end - start) + " ms");

    }
}
  • 关闭标量替换
-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:-EliminateAllocations

在这里插入图片描述

  • 开启标量替换(开启了逃逸分析就默认开启了标量替换)
-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+EliminateAllocations

在这里插入图片描述

  • 通过对比可以发现,java中栈上分配的体现主要就在于标量替换,通过Eden区和Old区使用的情况也可以看出标量替换的优化。

5.3 性能优化案例3: 合理配置堆内存

推荐配置

在案例1中我们增加堆内存可以提高系统的性能而且效果显著,那么随之带来的一个问题就是,我们增加多少内存比较合适?如果内存过大,那么如果产生FullGC的时候,GC时间会相对比较长,如果内存较小,那么就会频繁的触发GC,在这种情况下,我们该如何合理的适配堆内存大小呢?

分析:
依据的原则是根据Java Performance里面的推荐公式来进行设置。
在这里插入图片描述

  • Xms和Xmx设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。
  • 方法区(永久代PermSize和MaxPerSize或元空间MetaspaceSize和MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍。
  • 年轻代Xmn的设置为老年代存活对象的1-1.5倍。

但是,上面的说法也不是绝对的,也就是说这给的是一个参考值,根据多种调优之后得出的一个结论,大家可以根据这个值来设置一下我们的初始化内存,在保证程序正常运行的情况下,我们还要去查看GC的回收率,GC停顿耗时,内存里的实际数据来判断,Full GC是基本上不能有的,如果有就要做内存Dump分析,然后再去做一个合理的内存分配。

我们还要注意到一点就是,上面说的老年代存活对象怎么去判定。

如何计算老年代存活对象

方式1: 查看日志

推荐/比较稳妥

JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)。

方式2:强制触发FullGC

  • 会影响线上服务,慎用
  • 在使用CMS回收器的时候,有可能不能触发FullGC,所以日志中并没有记录FullGC的日志。在分析的时候就比较难处理。所以,有时候需要强制触发一次FullGC,来观察FullGC之后的老年代存活对象大小。
  • 在强制FullGC前先把服务节点摘除,FullGC之后再将服务挂回可用节点,对外提供服务,在不同时间段触发FullGC,根据多次FullGC之后的老年代内存情况来预估FullGC之后的老年代存活对象大小
  • 如何强制触发Full GC?
    • jmap -dump:live,format=b,file=heap.bin <pid>将当前的存活对象dump到文件,此时会触发FullGC
    • jmap -histo:live <pid>打印每个class的实例数目,内存占用,类全名信息.live子参数加上后,只统计活的对象数量,此时会触发FullGC
    • 在性能测试环境,可以通过Java监控工具来触发FullGC,比如使用VisualVM和JConsole,VisualVM集成了JConsole,VisualVM或者JConsole上面有一个触发GC的按钮。

在这里插入图片描述

案例演示

jvm配置参数

现在我们通过idea启动springboot工程,我们将内存初始化为1024M。我们这里就从1024M的内存开始分析我们的GC日志,根据我们上面的一些知识来进行一个合理的内存设置。

JVM设置如下:
-XX:+PrintGCDetails -XX:MetaspaceSize=64m -Xss512k -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapdump3.hprof -XX:SurvivorRatio=8 -XX:+PrintGCDateStamps -Xms1024M -Xmx1024M -Xloggc:log/gc-oom3.log

gc信息

  • 压测前gc信息
    在这里插入图片描述

  • 压测
    在这里插入图片描述

  • 压测后gc信息
    在这里插入图片描述

  • 强制执行Full GC
    jmap -histo:live vmid

  • 触发了三次Full gc之后查看信息
    在这里插入图片描述

  • 查看堆占用情况
    jmap -heap vmid
    在这里插入图片描述

  • 可以看到经过三次full gc后的老年代存活对象占20MB,所以我们调整堆的大小为80MB
    -Xms80M -Xmx80M

  • 调整堆大小后重启程序查看gc信息
    在这里插入图片描述

  • 压测

  • 查看gc信息
    在这里插入图片描述

  • 查看堆空间占用情况
    在这里插入图片描述

  • 经过对比把堆大小从1024MB调整为80MB,可以看出younggc次数增多了一点,但每次回收的时间更短,因此要配置合适的堆空间。

结论

可以用较小的内存满足当前的服务需要,但当内存相对宽裕时,可以相对给服务多加点内存,可以减少GC的频率。

估算GC频率

比如从数据库获取一条数据占用128个字节,需要获取1000条数据,那么一次读取到内存的大小就是(128B/1024Kb/1024M) * 1000 = 0.122M,那么我们程序可能需要并发读取,比如每秒读取100次,那么内存占用就是0.122*100 = 12.2M,如果堆内存设置为1个G,那么年轻代大小大约就是333M,那么333M*80%/12.2M = 21.84s,也就是说我们的程序几乎每分钟进行2-3次Younggc。这样可以让我们对系统有一个大致的估算。

0.122M * 100 = 12.2M/s --Eden区
1024 * 1/3 * 80% = 273M
273/12.2M==22.38s --> YGC 每分钟2-3次YGC

5.4 性能优化案例4: CPU占用很高排查方案

案例

public class JstackDeadLockDemo {
    
    
    /**
     * 必须有两个可以被加锁的对象才能产生死锁,只有一个不会产生死锁问题
     */
    private final Object obj1 = new Object();
    private final Object obj2 = new Object();

    public static void main(String[] args) {
    
    
        new JstackDeadLockDemo().testDeadlock();
    }

    private void testDeadlock() {
    
    
        Thread t1 = new Thread(() -> calLock_Obj1_First());
        Thread t2 = new Thread(() -> calLock_Obj2_First());
        t1.start();
        t2.start();
    }

    /**
     * 先synchronized  obj1,再synchronized  obj2
     */
    private void calLock_Obj1_First() {
    
    
        synchronized (obj1) {
    
    
            sleep();
            System.out.println("已经拿到obj1的对象锁,接下来等待obj2的对象锁");
            synchronized (obj2) {
    
    
                sleep();
            }
        }
    }

    /**
     * 先synchronized  obj2,再synchronized  obj1
     */
    private void calLock_Obj2_First() {
    
    
        synchronized (obj2) {
    
    
            sleep();
            System.out.println("已经拿到obj2的对象锁,接下来等待obj1的对象锁");
            synchronized (obj1) {
    
    
                sleep();
            }
        }
    }

    /**
     * 为了便于让两个线程分别锁住其中一个对象,
     * 一个线程锁住obj1,然后一直等待obj2,
     * 另一个线程锁住obj2,然后一直等待obj1,
     * 然后就是一直等待,死锁产生
     */
    private void sleep() {
    
    
        try {
    
    
            Thread.sleep(100);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

问题分析

  • 在服务器中运行上面的java代码
  • 查看进程id
jps -l

在这里插入图片描述

  • 查看进程信息
top -Hp 6088

在这里插入图片描述

  • 查看线程快照信息
jstack 6088 > jstack.log
vi jstack.log

在这里插入图片描述

延伸

问题排查过程

  1. ps aux | grep java 查看到当前java进程使用cpu、内存、磁盘的情况获取使用量异常的进程
    在这里插入图片描述

  2. top -Hp 进程pid检查当前使用异常线程的pid

  3. 把线程pid变为16进制如31695 -> 7bcf 然后得到0x7bcf

  4. jstack进程的pid | grep -A20 0x7bcf 得到相关进程的代码(列出对应线程后面20行的代码)
    在这里插入图片描述

解决方案

  1. 调整锁的顺序,保持一致
  2. 或者采用定时锁,一段时间后,如果还不能获取到锁就释放自身持有的所有锁。

5.5 性能优化案例5: G1并发执行的线程数对性能的影响

  • 通过设置不同的垃圾收集线程数查看对性能的影响

jvm初始参数

export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
export CATALINA_OPTS="$CATALINA_OPTS -Xms30m"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx30m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/www/server/tomcat/logs/gc.log"
  • jmeter压测吞吐量
    在这里插入图片描述

优化参数

  • 配置垃圾回收线程数
export CATALINA_OPTS="$CATALINA_OPTS -XX:ConcGCThreads=2"
  • jmeter压测吞吐量
    在这里插入图片描述

总结

  • 通过对比我们可以发现,通过配置垃圾回收线程的数量可以适当提高程序性能,提高吞吐量,减少gc时间.

5.6 性能优化案例6: 调整垃圾回收器提高服务的吞吐量

初始配置

系统配置是单核,我们看到日志,显示DefNew,说明我们用的是串行收集器SerialGC

优化配置(4核)

  • 查看gc状态
  • jmeter压测查看吞吐量
    997.6/s

优化配置(8核)

  • 查看gc状态
  • jmeter压测查看吞吐量
    吞吐量相比上面翻倍

优化配置(8核)

export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseParallelGC"
export CATALINA_OPTS="$CATALINA_OPTS -Xms60m"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx60m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/www/server/tomcat/logs/gc.log"
  • 查看gc信息
  • jmeter压测查看吞吐量和响应情况
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

修改垃圾收集器

export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
  • 查看gc信息
  • 压测吞吐量
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

5.7 性能优化案例7: 日均百万级订单交易系统如何设置JVM参数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.8 面试小结

问题一: 有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器是32位的,1.56G的堆,用户反馈网站比较缓慢。因此将服务器升级为了64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了!

  1. 为什么原网站慢?
    GC频繁,STW时间比较长,响应时间慢
  2. 为什么会更卡顿
    内存空间大,FGC时间更长,延迟时间更长
  3. 解决方案
  • 垃圾收集器: parallel gc;ParNew + CMS; G1
  • 配置GC参数: -XX:MaxGCPauseMillis、-XX:ConcGCThreads
  • 根据log日志、dump文件分析,优化内存空间的比例
    jstat jinfo jstack jmap

问题二: 系统CPU经常100%,如何调优?(面试高频)

  • ps aux | grep java 查看对应java进程信息
  • top -Hp 进程pid 检查异常线程的pid
  • 把线程pid变为16进制如31695 > 77bcf 然后得到0x7bcf
  • jstack进程的pid | grep -A20 0x7bcf 得到相关线程的后20行快照信息

问题三: 系统内存飙高,如何查找问题?(面试高频)
一方面: jmap -heap、jstat、…;gc日志情况
另一方面: dump文件分析

问题四: 如何监控JVM

  • 命令行工具
  • 图形化界面工具

猜你喜欢

转载自blog.csdn.net/qq_43478625/article/details/121497151
今日推荐