IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> JVM性能优化,拿来吧你! -> 正文阅读

[Java知识库]JVM性能优化,拿来吧你!

在这里插入图片描述
对于 Java 开发的同学来说,JVM 性能优化可以说是比较难掌握的知识点。这不仅因为 JVM 性能优化需要掌握晦涩难懂的 JVM 知识,还因为 JVM 性能优化很难有使用场景。这导致了许多人对 JVM 性能优化不熟悉,感觉就像是空中楼阁的天物一样不可触及。这几天工作中做了一次 JVM 性能优化,我想这对于 JVM 调优的初学者会有较大帮助。

背景

我们都知道 JVM 分为了新生代和老年代,并且我们在启动应用的时候都会配置对应的参数,为应用程序运行的 JVM 调整内存大小。但我们都知道,很多时候我们都只是大致估计一个数,随便填填,然后就上线了。

作者所在的公司同样存在这种情况,JVM 内存大小基本上都设得挺大的,毕竟内存大总比内存溢出好,因此就造成了不少的内存浪费。所以作者收到的任务就是对所有的应用进行一次排查,调整合适的内存参数,优化 JVM 的性能。

调优实战

要对应用进行 JVM 性能调优,那么首先得知道其运行的情况。这就像去医院看医生,去开药之前需要医生先望闻问切一样。在 Java 中,有很多方式可以观察到 JVM 的内部情况,例如 JDK 提供的各种命令工作。作者所在公司使用的是 Prometheus 进行监控,因此我们可以直接在 Prometheus 上看到应用的 JVM 运行情况。
应用层面优化
除了 GC 频率、GC 停顿时间,我们还能从应用的类型来分析 JVM 的内存消耗情况。

例如对于接口类型的系统来说,很多请求都是 1 秒中之内就结束。对于这种类型的请求,他们进入应用时会分配内存,结束时内存就会立刻被回收,留存下来的对象很少。这种应用的 JVM 内存情况大概是这样的:新生代消耗比较大,并且随着周期性回收内存,但老年代的内存消耗则更小。对于那些持续性处理的应用,例如持续时间长的应用处理。因为其存活时间较久,所以可能会有更多的对象晋升到老年代,因此老年代的内存消耗就比较大。

通过观察 JVM 年轻代与老年代的内存消耗情况,再结合应用本身的特性,我们可以发现应用中不合理的地方,再对应用进行针对性的优化。例如:应用某个地方每次都会存储大量的临时数据到内容中,这样就造成了 JVM 可能爆发 GC,从而导致应用卡顿。

小总结

总结一下本篇文章的调优方法: 通过观察 GC 频率和停顿时间,来进行 JVM 内存空间调整,使其达到最合理的状态。调整过程记得小步快跑,避免内存剧烈波动影响线上服务。 这其实是最为简单的一种 JVM 性能调优方式了,可以算是粗调吧。但 JVM 性能调优还有更多、更详细的参数,后续有机会我们再聊聊。

此外,通过观察 JVM 年轻代与老年代的情况,也可以帮助我们对应用进行针对性的优化,从而提升应用本身的性能。

一、内存溢出

内存溢出的原因:程序在申请内存时,没有足够的空间。

1. 栈溢出

方法死循环递归调用(StackOverflowError)、不断建立线程(OutOfMemoryError)。

2. 堆溢出

不断创建对象,分配对象大于最大堆的大小(OutOfMemoryError)。

3. 直接内存

JVM 分配的本地直接内存大小大于 JVM 的限制,可以通过-XX:MaxDirectMemorySize 来设置(不设置的话默认与堆内存最大值一样,也会出现OOM 异常)。

4. 方法区溢出

一个类要被垃圾收集器回收掉,判定条件是比较苛刻的,在经常动态生产大量 Class 的应用中,CGLIb 字节码增强,动态语言,大量 JSP(JSP 第一次运行需要编译成 Java 类),基于 OSGi 的应用(同一个类,被不同的加载器加载也会设为不同的类),都可能会导致OOM。

二、内存泄露

程序在申请内存后,无法释放已申请的内存空间,导致这一部分的原因主要是代码写的不合理,比如以下几种情况。

1. 长生命周期的对象持有短生命周期对象的引用

例如将 ArrayList 设置为静态变量,然后不断地向ArrayList中添加对象,则 ArrayList 容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。

2. 连接未关闭

如数据库连接、网络连接和 IO 连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。

3. 变量作用域不合理

例如:

一个变量的定义的作用范围大于其使用范围。
如果没有及时地把对象设置为 null。

4. 内部类持有外部类

Java 的 非静态内部类 的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄露(可以理解为:垃圾回收器会回收掉外部类的实例,但由于内部类持有外部类的引用,导致垃圾回收器不能正常工作)。

解决办法:将非静态内部类改为 静态内部类,即加上 static 修饰,例如:

public class Jvm5 {
    private static String string = "SuunyBear";

    public static void show() {
        System.out.println("show");
    }

    public static void main(String[] args) {
        Jvm5 m = new Jvm5();
        // 非静态内部类的构造方式
        // Child c=m.new Child();
        Child c = new Child();
        c.test();
    }

    /**
     * 内部类Child --静态的,防止内存泄漏
     */
    static class Child {
        public int i;

        public void test() {
            System.out.println("string:" + string);
            show();
        }
    }
}

5. Hash值改变

在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露。

使用例子来说明。

public class Jvm6 {
    private int x;
    private int y;

    public Jvm6(int x, int y) {
        super();
        this.x = x;
        this.y = y;
    }
    /**
     * 重写HashCode的方法
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        result = prime * result + y;
        return result;
    }
    /**
     * 改变y的值:同时改变hashcode
     */
    public void setY(int y) {
        this.y = y;
    }

    public static void main(String[] args) {
        HashSet<Jvm6> hashSet = new HashSet<Jvm6>();
        Jvm6 data1 = new Jvm6(1, 3);
        Jvm6 data2 = new Jvm6(3, 5);
        hashSet.add(data1);
        hashSet.add(data2);
        data2.setY(7); // data2的Hash值改变
        hashSet.remove(data2); // 删掉data2节点
        System.out.println(hashSet.size()); // 2
    }
}

四、了解MAT

mat是一个内存泄露的分析工具。

1. 浅堆和深堆

  • 浅堆(Shallow Heap):是指一个对象所消耗的内存。
  • 深堆(Retained Heap):这个对象被 GC
    回收后,可以真实释放的内存大小,也就是只能通过对象被直接或间接访问到的所有对象的集合。通俗地说,就是一个对象包含(引用)的所有对象的大小,如图:

2. MAT的使用

1、下载MAT工具:下载地址

2、内存溢出例子演示
参数说明:

  • -Xms5m 堆初始大小5M
  • -Xmx5m 堆最大大小5M
  • -XX:+PrintGCDetails 打印gc日志详情
  • -XX:+HeapDumpOnOutOfMemoryError 输出内存溢出文件
  • -XX:HeapDumpPath=D:/oomDump/dump.hprof 内存溢出文件保存位置,此文件用于MAT分析
/**
 * VM Args:-Xms5m -Xmx5m  -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/oomDump/dump.hprof
 */
public class Jvm7 {

    public static void main(String[] args) {
        // 在方法执行的过程中,它是GCRoots
        List<Object> list = new LinkedList<>();
        int i = 0;
        while (true) {
            i++;
            if (i % 10000 == 0) {
                System.out.println("i=" + i);
            }
            list.add(new Object());
        }
    }
}

设置参数运行后,内存溢出,程序结束,然后我们就可以用下载好的MAT来分析了,当然MAT也只是分析猜想,并不代表一定是这个原因导致内存溢出。

打开我们保存的文件目录进行分析。

分析结果。

此时可以查看详情查看具体原因,当然这个原因也只是一种猜想。

五、JDK提供的一些工具

分类属性值描述
命令行工具jps虚拟机进程状况工具
命令行工具jstat虚拟机统计信息监视工具
命令行工具jinfoJava配置信息工具
命令行工具jmapJava内存映像工具
命令行工具jhat虚拟机堆转储快照分析工具
命令行工具jstackJava堆栈跟踪工具
可视化工具JConsole监视与管理控制台
可视化工具VisualVM多合一故障处理工具

所有的工具都在jdk的安装bin目录下,比如我的在C:\My Program Files\Java\jdk1.8.0_201\bin
其中一般情况命令行在线上服务器上使用,可视化工具在本地使用,当然如果你的线上服务器允许远程的话也可以使用可视化工具。

六、GC调优

1. GC调优重要参数

生产环境推荐开启

  • -XX:+HeapDumpOnOutOfMemoryError
  • 输出内存溢出文件
  • -XX:HeapDumpPath=D:/oomDump/dump.hprof
  • 内存溢出文件保存位置,此文件用于MAT分析
  • 当然,一般Linux服务器可以设置为 ./java_pid.hprof 默认为Java进程启动位置

调优之前开始,调优之后关闭

  • -XX:+PrintGC 调试跟踪之 打印简单的 GC 信息参数:
  • -XX:+PrintGCDetails和-XX:+PrintGCTimeStamps 打印详细的 GC 信息
  • -Xlogger:logpath:log/gc.log 设置 gc 的日志路,将 gc.log 的路径设置到当前目录的 log 目录下. 应用场景: 将 gc 的日志独立写入日志文件,将 GC 日志与系统业务日志进行了分离,方便开发人员进行追踪分析

考虑使用

  • -XX:+PrintHeapAtGC 打印推信息,获取 Heap 在每次垃圾回收前后的使用状况
  • -XX:+TraceClassLoading 在系统控制台信息中看到 class 加载的过程和具体的 class 信息,可用以分析类的加载顺序以及是否可进行精简操作
  • -XX:+DisableExplicitGC
    禁止在运行期显式地调用 System.gc()

2. GC调优的原则(很重要)

  • 大多数的 java 应用不需要 GC 调优
  • 大部分需要 GC 调优的的,不是参数问题,是代码问题
  • 在实际使用中,分析 GC 情况优化代码 比 优化 GC 参数 要多得多
  • GC 调优是最后的手段

调优的目的

  • GC 的时间够小
  • GC 的次数够少发生
  • Full GC 的周期足够的长,时间合理,最好是不发生

: 如果满足下面的指标,则一般不需要进行 GC调优

  • Minor GC 执行时间不到 50ms
  • Minor GC 执行不频繁,约 10 秒一次
  • Full GC 执行时间不到 1s
  • Full GC 执行频率不算频繁,不低于 10 分钟 1 次

3. GC调优步骤

  1. 监控 GC 的状态使用各种 JVM 工具,查看当前日志,分析当前 JVM 参数设置,并且分析当前堆内存快照和 gc
    日志,根据实际的各区域内存划分和 GC 执行时间,觉得是否进行优化。

  2. 分析结果,判断是否需要优化如果各项参数设置合理。

    系统没有超时日志出现,GC 频率不高,GC 耗时不高,那么没有必要进行 GC 优化。 如果 GC 时间超过 1 秒,或者频繁
    GC,则必须优化。

  3. 调整 GC 类型和内存分配如果内存分配过大或过小,或者采用的 GC 收集器比较慢,则应该优先调整这些参数,并且先找 1 台或几台机器进行
    测试,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。

  4. 不断的分析和调整通过不断的试验和试错,分析并找到最合适的参数5,全面应用参数如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。

分析GC日志
主要关注 MinorGC 和 FullGC 的回收效率(回收前大小和回收比较)、回收的时间。
1、-XX:+UseSerialGC

  • 以参数-Xms5m -Xmx5m -XX:+PrintGCDetails -XX:+UseSerialGC 为例详细说明。
  • [DefNew: 1855K->1855K(1856K), 0.0000148 secs][Tenured:
    2815K->4095K(4096K), 0.0134819 secs] 4671K。
  • DefNew 指明了收集器类型,而且说明了收集发生在新生代。
  • 1855K->1855K(1856K)表示,回收前 新生代占用 1855K,回收后占用 1855K,新生代大小 1856K
  • 0.0000148 secs 表明新生代回收耗时。
  • Tenured 表明收集发生在老年代。
  • 2815K->4095K(4096K), 0.0134819 secs:含义同新生代最后的 4671K 指明堆的大小。

2、-XX:+UseParNewGC

  • 收集器参数变为-XX:+UseParNewGC。
  • 日志变为:[ParNew: 1856K->1856K(1856K), 0.0000107 secs][Tenured:
    2890K->4095K(4096K), 0.0121148 secs]。
  • 收集器参数变为-XX:+ UseParallelGC 或 UseParallelOldGC。
  • 日志变为:[PSYoungGen: 1024K->1022K(1536K)] [ParOldGen:
    3783K->3782K(4096K)] 4807K->4804K(5632K)。

3、-XX:+UseConcMarkSweepGC 和 -XX:+UseG1GC
使用这两个收集器的日志会和UseParNewGC一样有明显的相关字样。

4. 项目启动调优

开启日志分析-XX:+PrintGCDetails,启动项目时,通过分析日志,不断地调整参数,减少GC次数。

例如:

  1. 碰到 Metadata空间 不足发生GC,那么调整 Metadata空间 -XX:MetaspaceSize=64m 减少 FullGC。
  2. 碰到MinorGC,那么调整堆空间 -Xms1000m 大小减少FullGC 。
  3. 如果还是有MinorGC,那么继续增大堆空间大小,或者增大新生代比例 -Xmn900m GC,此时新生代空间为900m,老年代大小100m 。

5. 项目运行GC调优

使用 jmeter 工具 来进行压测,然后分析原因,进行调优,当然 正式上线的项目请谨慎操作 。

jmeter工具安装使用
1、下载好对应版本的jmeter,注意jdk版本。

2、jmeter需要Java运行时环境,所以如果报错请先检查你的Java环境变量设置,解压到你想要的路径,例如我解压在C:\My Program Files\apache-jmeter-5.2.1,在bin目录下有一个 jmeter.bat文件,双击启动。

至于具体怎么使用就百度吧,基本拿到软件就知道使用了,毕竟这个说来就浪费篇幅了。

聚合报告参数

这里放出我本地 jmeter 测试一个项目之后的 聚合报告参数解释

6. 推荐策略(仅作参考)

1、新生代大小选择

  • 尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,新生代收集发生的频率也是最小的.同时,减少到达老年代的对象。
  • 避免设置过小,当新生代设置过小时会导致:MinorGC 次数更加频繁、可能导致 MinorGC
    对象直接进入老年代,如果此时老年代满了,会触发 FullGC。

2、老年代大小选择

一般吞吐量优先的应用都有一个很大的新生代和一个较小的老年代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象

最后,感谢大家的观看,谢谢
如果文章有问题可以关注私信我交流更改,或在评论区互相交流。
加小助理vx:xiehuangbao1123 领取java学习资料和最新面试资料

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-07-15 16:02:30  更:2021-07-15 16:02:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/17 20:40:59-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码