只会CRUD?来学学JVM指标监控和性能调优入门实战吧

3552 篇文章 114 订阅

前言

对于JVM监控与调优,部分Java开发还停留在听说过的层面但是并没有实践过的情况,笔者也曾停留在这个阶段,自认为JVM监控与调优不过是书本上的东西,自认为JVM十分稳定,会帮我们处理任何的Java程序问题。诚然,Java发展到如今,JVM已经是一个很成熟且稳定的产品了,但是,再好的武器也要看使用者,新手拿着屠龙刀也杀不死一只小公鸡,对于一些初入编程的小白来说,是有可能写出一些代码让稳定的JVM不再稳定的,所以问了解决这些问题,学好并且用好JVM监控与调优工具是很有必要的一件事情,通过合理的使用工具和分析,我相信我们可以解决百分之九十的JVM问题,至于那百分之十,这钱我们不赚也罢。

一、监控篇

公司的基建一般只能监控整体运行情况,内存占用,GC延迟,是否出现异常等,并没有提供具体的监控(线程、内存、堆栈、GC)。

1.1 基础监控工具

1.1.1 jps:jvm进程监控工具

列出正在运行的虚拟机进程,并显示虚拟机主类。

1.1.2 jstat:查看JVM统计信息

jstat:查看JVM统计信息,注意,它只能监控JVM整体的运行状态,比如堆内存分布,GC收集状态等。

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

表4-2 jstat工具主要选项

不过其实各大厂其实都有监控相关的基建平台,针对JVM的堆内存分布,GC执行情况等统计信息都会有相应的监控和报警。

1.1.3 jinfo:Java配置信息工具

jinfo的作用是实时查看和调整虚拟机各项参数。使用jps命令的 -v参数可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,除 了去找资料外,就只能使用jinfo的 -flag 选项进行查询了。

JDK 6之后,jinfo在Windows和Linux平台都有提供,并且加入了在运行期修改部分参数值的能力(可以使用-flag[+|-]name或者-flag name=value在运行期修改一部分运行期可写的虚拟机参数值)

1.1.4 jmap⭐️:Java内存映像工具(解决内存问题)

jmap命令用于生成堆转储快照(一般称为heapdump或dump文件) 。如果不使用jmap 命令,要想获取J ava堆转储快照也还有一些比较“ 暴力”的手段:譬如**- XX:+HeapDumpOnOutOfMemoryError**参数,可以让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件。

jmap 的作用并不仅仅是为了获取堆转储快照,它还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。虽然他最大的用途是用来获取HeapDump文件。

生成了heapdump快照文件之后,我们就可以利用一些工具比如jhat、Eclipse MAT、VisualVM等来分析heapdump文件,从而知道生成这一快照的时候的堆内存主要分布在哪些对象上面,从而来解决出现的如堆内存飙升,OOM等问题。

1.1.5 jstack⭐️:Java堆栈跟踪工具(解决线程问题)

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

比如常见的面试题:如何排查CPU100%?就可以通过top + jstack来分析占用CPU最高的线程的堆栈信息。

比如常见的面试题:如何排查一个请求长时间没有响应?就可以通过jstack来分析该请求线程的堆栈信息。

总结

对于以上提到的基础命令行工具,程序员们应该要熟练掌握,虽然下文提到的可视化故障处理工具囊括了以上工具的所有功能(没错就是这么牛掰),但是可视化工具如果要连接远程的服务器,基本上都是基于一些远程连接协议支持的,但是并不是所有远程服务器都支持的,也许刚好你的项目所部署的服务器不支持一些远程连接协议,那么此时就只能通过命令行工具来进行了,所以掌握以上这些工具对于程序员来说是很有必要的。

1.2 可视化故障处理工具

JDK中除了附带大量的命令行工具外,还提供了几个功能集成度更高的可视化工具,用户可以使用这些可视化工具以更加便捷的方式进行进程故障诊断和调试工作。这类工具主要包括JConsoleJHSDBVisualVMJMC四个。其中前三个是免费使用,后一个是需要付费的,由于可视化故障处理工具都大差不差,所以本文主要介绍一下VisualVM这个功能最强大的工具。

1.2.1 VisualVM:多功能故障处理工具

VisualVM(All-in-One Java Troubleshooting Tool)是功能最强大的运行监视和故障处理程序之一, 曾 经 在 很 长 一 段 时 间 内 是 Oracle官 方 主 力 发 展 的 虚 拟 机 故 障 处 理 工 具 。 Oracle曾 在 VisualVM的 软 件 说 明 中写上了“ All-in-One”的字样,预示着它除了常规的运行监视、故障处理外,还将提供其他方面的能 力,譬如性能分析(Profiling)。VisualVM 的性能分析功能比起JProfiler、YourKit等专业且收费的 Profiling工 具 都 不 遑 多 让 。 而 且 相 比 这 些 第 三 方 工 具 , VisualVM还 有 一 个 很 大 的 优 点 : 不 需 要 被 监 视 的 程序基于特殊Agent去运行,因此它的通用性很强,对应用程序实际性能的影响也较小,使得它可以直 接应用在生产环境中。这个优点是JProfiler、YourKit等工具无法与之媲美的。

VisualVM具备丰富的插件拓展功能,通过不同的功能插件, VisualVM可以做到 :

  • 显示虚拟机进程以及进程的配置、环境信息(jp s、jinfo)。
  • 监视应用程序的处理器、垃圾收集、堆、方法区以及线程的信息(jstat、jstack)。
  • dump 以及分析堆转储快照(jmap 、jhat )。
  • 方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法。
  • 离线程序快照:收集程序的运行时配置、线程dump 、内存dump 等信息建立一个快照,可以将快 照发送开发者处进行Bug反馈。
  • 其他插件带来的无限可能性。

(1)启动方式

1.使用JDK自带的工具包,在命令行输入jvisualvm即可启动程序

2.单独下载VisualVM软件工具

(2)插件安装

菜单栏——> 工具 ——> 插件

连接方式

(3)主要功能

visualVm的功能十分丰富,涵盖了我们之前说的所有基础监控工具的功能,并且还有一些自己独有的功能。

他的主要功能如下:

  1. 生成/读取HeapDump文件(堆内存快照) == jmap

  2. 查看JVM参数和系统属性 == jinfo

  3. 查看运行中的虚拟机进程 == jps

  4. 生成/读取线程快照 == jstack

  5. 程序资源的实时监控

  6. 其他功能

    1. JMX远程连接
    2. 远程环境监控
    3. CPU分析和内存分析

真可谓是十分强大了。这里我们重点介绍一下堆内存快照分析和线程快照分析

(4)HeapDump文件生成和分析

在监视页面点击 堆dump 之后,就会生成一份Dump文件。

  • dump文件概述:我们可以在概要信息中看到此时堆内存的统计信息

  • 堆内存分布信息⭐️:通过该信息,我们可以发现哪些类占有大量的内存,从而分析程序的堆内存问题

(5)线程Dump文件生成和分析

  • 在线程面板生成线程Dump文件

  • 查看Dump文件:我们以第一个举例

    • “RMI TCP ....”表示线程名
    • daemon表示是一个后台线程
    • prio表示线程的优先级,用处不大
    • tid:Java虚拟机中的线程ID
    • nid:线程本地标识,是线程在当前操作系统的线程ID标识(和TOP 命令展示的线程/进程ID是同一个)
    • [xxxxxx]:对象的内存地址
    • Java.lang.thread.State:RUNNABLE:显示线程当前时刻的状态,括号里面是处于该状态的原因。
    • 线程堆栈:最下面以at开头的是线程的调用堆栈信息,通过它可以看出线程执行代码的流程,以及该线程目前在哪个代码块阻塞了

二、案例篇

2.1 内存泄露

可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题,当出现以下两个场景的时候,就代表发生了内存泄漏。

  • 是否还被引用?是
  • 是否还被需要?否

申请了内存用完了不释放,比如一共有 1024M 的内存,分配了 512M 的内存一直不回收,那么可以用的内存只有 512M 了,仿佛泄露掉了一部分;通俗一点讲的话,内存泄漏就是【占着茅坑不拉 shi】

2.2 几种情况

下面介绍一下可能会出现内存泄漏的几种情况,同学们在进行相关程序开发的时候,要格外注意以下这些情况,防止出现内存泄漏导致程序内存问题。

(1)静态集合类

静态集合类,如 HashMap、LinkedList 等等。如果这些容器为静态的,那么它们的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

public class MemoryLeak {
    static List list = new ArrayList();
    public void oomTests(){
        Object obj=new Object();//局部变量
        list.add(obj);
    }
}
复制代码

(2)单例模式

单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

(3)内部类持有外部类

内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

最佳实践:内部类最好只在内部使用。

(4)各种连接,如数据库连接、网络连接和 IO 连接等

在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对 Connection、Statement 或 ResultSet 不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

public static void main(String[] args) {
    try{
        Connection conn =null;
        Class.forName("com.mysql.jdbc.Driver");
        conn =DriverManager.getConnection("url","","");
        Statement stmt =conn.createStatement();
        ResultSet rs =stmt.executeQuery("....");
    } catch(Exception e){//异常日志
    } finally {
        // 1.关闭结果集 Statement
        // 2.关闭声明的对象 ResultSet
        // 3.关闭连接 Connection
    }
}
复制代码

(5)变量不合理的作用域

变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为 null,很有可能导致内存泄漏的发生。

public class UsingRandom {
    private String msg;
    public void receiveMsg(){
        readFromNet();//从网络中接受数据保存到msg中
        saveDB();//把msg保存到数据库中
    }
}
复制代码

如上面这个伪代码,通过 readFromNet 方法把接受的消息保存在变量 msg 中,然后调用 saveDB 方法把 msg 的内容保存到数据库中,此时 msg 已经就没用了,由于 msg 的生命周期与对象的生命周期相同,此时 msg 还不能回收,因此造成了内存泄漏。实际上这个 msg 变量应该放在 receiveMsg 方法内部,当方法使用完,那么 msg 的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。

(6)改变哈希值

改变哈希值,当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。

否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏。

这也是 String 为什么经常被当作HashMap的Key的原因,由于String不可变,我们可以放心地把 String 当做 HashMap 的 key 值;

当我们想把自己定义的类保存到散列表的时候,需要保证对象的 hashCode 不可变。

/**
 * 例1
 */
public class ChangeHashCode {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        Person p1 = new Person(1001, "AA");
        Person p2 = new Person(1002, "BB");
​
        set.add(p1);
        set.add(p2);
​
        p1.name = "CC";//导致了内存的泄漏
        set.remove(p1); //删除失败
​
        System.out.println(set);
​
        set.add(new Person(1001, "CC"));
        System.out.println(set);
​
        set.add(new Person(1001, "AA"));
        System.out.println(set);
​
    }
}
​
class Person {
    int id;
    String name;
​
    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }
​
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
​
        Person person = (Person) o;
​
        if (id != person.id) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }
​
    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }
​
    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + ''' +
                '}';
    }
}
复制代码

(7)缓存泄露

内存泄漏的常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。对于这个问题,可以使用 WeakHashMap 代表缓存,当发生GC的时候,如果WeakHashMap里面的key只有map本身引用时,就会将key对应的Entry清除掉。

public class MapTest {
    static Map wMap = new WeakHashMap();
    static Map map = new HashMap();
​
    public static void main(String[] args) {
        init();
        testWeakHashMap();
        testHashMap();
    }
​
    public static void init() {
        String ref1 = new String("obejct1");
        String ref2 = new String("obejct2");
        String ref3 = new String("obejct3");
        String ref4 = new String("obejct4");
        wMap.put(ref1, "cacheObject1");
        wMap.put(ref2, "cacheObject2");
        map.put(ref3, "cacheObject3");
        map.put(ref4, "cacheObject4");
        System.out.println("String引用ref1,ref2,ref3,ref4 消失");
​
    }
​
    public static void testWeakHashMap() {
        System.out.println("WeakHashMap GC之前");
        for (Object o : wMap.entrySet()) {
            System.out.println(o);
        }
        try {
            System.gc();
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("WeakHashMap GC之后");
        for (Object o : wMap.entrySet()) {
            System.out.println(o);
        }
    }
​
    public static void testHashMap() {
        System.out.println("HashMap GC之前");
        for (Object o : map.entrySet()) {
            System.out.println(o);
        }
        try {
            System.gc();
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("HashMap GC之后");
        for (Object o : map.entrySet()) {
            System.out.println(o);
        }
    }
​
}
复制代码

上面代码和图示主演演示 WeakHashMap 如何自动释放缓存对象,当 init 函数执行完成后,局部变量字符串引用 weakd1,weakd2,d1,d2 都会消失,此时只有静态 map 中保存中对字符串对象的引用,可以看到,调用 gc 之后,HashMap 的没有被回收,而 WeakHashMap 里面的缓存被回收了。

三、拓展篇

3.1 Arthas

为什么有了之前提到过的工具还需要Arthas呢?

之前提到过的图形化工具有一个不可避免的缺点,都必须在服务端项目进程中配置相关的监控参数。然后工具通过远程连接到项目进程,获取相关的数据。这样就会带来一些不便,比如大部分线上环境的网络是隔离的,本地的监控工具根本连不上线上环境。

而Arthas,这款工具就不需要远程连接,也不需要配置监控参数,同时也提供了丰富的性能监控数据。

3.1.1 介绍

Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。

当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

  1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  5. 是否有一个全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到JVM的实时运行状态?
  7. 怎么快速定位应用的热点,生成火焰图?
  8. 怎样直接从JVM内查找某个类的实例?

Arthas支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。

此处不过多介绍,详情请看:

Arthas教程

Arthas官网

如果有感兴趣的读者,可以在评论区回复想学习Arthas,后续可以出一篇大厂内部Arthas实战指南。

3.2 火焰图

为什么需要火焰图?

当我们的程序被越来越多的人访问的时候,我们此时就需要对程序的性能进行优化,来支撑更多的用户请求和达到更快的用户响应,这个时候我们用之前提到的JVM监控分析工具就不太行了,我们只能够知道相应的内存分布,线程的执行情况,但是无法从

什么是火焰图?

火焰图(Flame Graph)是由 Linux 能优化师 Brendan Gregg 发明的,和所有其他的 profiling 方法不同的是,火焰图以一个全局的视野来看待时间分布,它从底部往顶部,列出所有可能导致性能瓶颈的调用栈。它主要是用来进行分析性能优化

此处不过多介绍,详情请看:

如何读懂火焰图——阮一峰

巧用火焰图快速分析链路性能

3.3 GC日志分析 ⭐️

什么是GC日志分析?

GC日志指JVM在进行YGC和FullGC时的一些日志信息,通过分析GC日志,我们可以清楚在发生GC期间JVM的内存变化,从而针对具体的场景做一些GC相关的优化。

3.3.1 GC分类

针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

  • 部分收集(Partial GC):不是完整收集整个 Java 堆的垃圾收集。其中又分为:

    • 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0, S1)的垃圾收集
    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有 CMS GC 会有单独收集老年代的行为。注意,很多时候 Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。
  • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有 G1 GC 会有这种行为

  • 整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。

3.3.2 GC日志相关JVM参数

  • -XX: +PrintGC ——表示打开简化的GC日志
  • -XX: +PrintGCDetails —— 打印内存回收详细的日志,并在进程退出时输出当前内存各区域分配情况
  • -XX: +PrintGCTimeStamps ——输出GC发生的时间戳,需要配合1、2使用
  • -XX: +PrintGCDateStamps ——输出GC发生的时间戳(以日期的形式),需配合1、2使用
  • -XX: +PrintHeapAtGC —— 输出每一次GC前后的堆信息,可以独立使用
  • -Xloggc:<file> —— 把GC日志写入到一个文件中去,而不是打印到标准输出中

下面就针对前两个重要的命令来分析一下GC日志的信息。

3.3.3 GC日志分类

由于PrintGCDetails打印的信息更加全面,所以一般通过该命令来进行GC日志分析。

(1)MinorGC(YGC)

下图表示了一个经典的MinorGC日志的信息和其信息代表的含义。

  • GC类型

  • 新生代

    • GC前后新生代内存占用
    • 新生代总大小(只包括1/2的Survivor区)
  • 堆空间

    • GC前后JVM堆内存占用
    • JVM堆总大小
  • GC耗时

    • user:CPU工作在用户态所花费的时间
    • sys:指的是CPU工作在内核态所花费的时间
    • real:指的是在此次GC事件中所花费的总时间

(2)FullGC

下图表示了一个经典的FullGC日志的信息和其信息代表的含义。

主要包括以下信息:

  • GC类型

  • 新生代

    • GC前后新生代大小
    • 新生代总大小
  • 老年代

    • GC前后老年代大小
    • 老年代总大小
  • 堆空间

    • GC前后堆内存占用
    • JVM堆总大小
  • 元空间

    • GC前后元空间大小
    • 元空间总大小
  • GC耗时

    • user:CPU工作在用户态所花费的时间
    • sys:指的是CPU工作在内核态所花费的时间
    • real:指的是在此次GC事件中所花费的总时间

可以看到,其实GC日志还是有规律的,一般都是按照先新生代,再老年代,再堆区域,再元空间的顺序依次展示,并且每个模块中的信息结构一致。

一般都是:[区域名称:GC 前内存占用-> GC 后内存占用(该区域内存总大小)]
复制代码

3.3.4 GC日志分析工具

除了自己肉眼分析以外,还可以使用一些GC日志分析工具来分析,这里简单介绍一下。

(1)GCEasy

GCEasy 是一款在线的 GC 日志分析器,可以通过 GC 日志分析进行内存泄露检测、GC 暂停原因分析、JVM 配置建议优化等功能,大多数功能是免费的。

官网地址:gceasy.io/

(2)GCViewer

GCViewer 是一款离线的 GC 日志分析器,用于可视化 Java VM 选项 -verbose:gc 和 .NET 生成的数据 -Xloggc:。还可以计算与垃圾回收相关的性能指标(吞吐量、累积的暂停、最长的暂停等)。当通过更改世代大小或设置初始堆大小来调整特定应用程序的垃圾回收时,此功能非常有用。

源码下载:github.com/chewiebug/G…

运行版本下载:github.com/chewiebug/G…

(3)GChisto

  • 官网上没有下载的地方,需要自己从 SVN 上拉下来编译
  • 不过这个工具似乎没怎么维护了,存在不少 bug

(4)HPjmeter

  • 工具很强大,但是只能打开由以下参数生成的 GC log,-verbose:gc -Xloggc:gc.log。添加其他参数生成的 gc.log 无法打开

  • HPjmeter 集成了以前的 HPjtune 功能,可以分析在 HP 机器上产生的垃圾回收日志文件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值