Java底层知识之JVM深入

72 阅读29分钟

性能监控与调优

先来看看大厂的相关面试题

image-20230108171501900

对于生产环境而言,可能会出现以下问题

image-20230108171543548

我们之所以要调优,是为了解决OOM、预防OOM以及减少Full GC出现的频率,我们调优的考虑阶段一般有三个,分别是上线前、项目运行阶段以及线上出现OOM

我们监控的依据分别是运行日志、异常堆栈、GC日志、线程快照以及堆转储快照,调优的大方向有三,分别是合理地编写代码、充分并合理使用硬件资源以及合理进行JVM调优

性能优化的步骤分为性能监控、性能分析和性能调优

性能监控指的是以一种非强行的或者入侵方式手机或查看应用运营性能数据的活动,其中可能发现的问题包括但不限于GC频繁、cpu load过高、OOM、内存泄露、死锁和程序响应时间较长

image-20230108194832260

性能的分析则是一种以侵入方式收集运行性能数据的活动,做这项的目的在于排查问题,一般排查问题的方法有

打印GC日志,通过GCviewer或者gceasy.io来分析异常信息

灵活运用命令行工具、jstack、jmap、jinfo等

dump出堆文件,使用内存分析工具分析文件

使用阿里Arthas、jconsole、JVisualVM来实时查看JVM状态

jstack查看堆栈信息

image-20230108195330552

性能调优则是解决问题的方法,一般的方法有

适当增加内存,根据业务背景选择垃圾回收器

优化代码,控制内存使用

增加机器,分散节点压力

合理设置线程池线程数量

使用中间件提高程序效率,比如缓存、消息队列等

image-20230108195454516

评价一个项目的性能的指标有以下几种

image-20230108195625879

一般来说,我们比较关注停顿时间和吞吐量,我们主要优化这两个部分,下面是关于响应时间的说明

image-20230108195654161

最后是其它们的关系描述

image-20230108195718878

JVM监控及诊断工具

性能诊断是软件工程师在日常工作中需要经常面对和解决的问题,想要定位这些问题,性能诊断工具必不可少

image-20230108201011447

我们这里先学习使用命令行形式的监控及诊断工具

image-20230108201123709

image-20230108201154346

命令行篇

首先要提的是,一般来说我们实际的项目使用这些命令行诊断工具都是在Linux系统下,我们这里图方便就用windows系统来演示了,这个其实大差不差

Jps

jps可以查看正在运行的Java进程,可以显示指定系统呃逆所有的HotSpot虚拟机进程

image-20230108202828722

基本使用语法为jps,可以使用jps -help来查看对应的参数信息

image-20230108202944472

其中-m能输出虚拟机进程时传递给main()方法的参数,参数包括一些字符串或者jar包

image-20230108203015582

我们也可以远程监控主机上的java程序,这需要安装jstatd,但是这种技术容易收到IP地址欺诈攻击,因此最安全的操作其实是不用这个

image-20230108203229083

Jstat

jstat能查看JVM中各种运行状态信息,可以显示本地或者远程虚拟机进程中的类装载、内存、GC和编译等数据

image-20230109005207115

基本的使用语法如下,我们先来讲option参数

image-20230109005501874

option一共有以下的各种选项,每个选项针对的查看情况都有所区别

image-20230109005548485

interval参数用于指定输出统计数据的周期,单位为毫秒。即:查询间隔,指定对应的时间可以令其在规定的时间内一直打印

count参数用于指定查询的总次数,可以实现打印几次就停止打印的效果

-t参数可以在输出信息前加上一个Timestamp列,显示程序的运行时间。单位:秒

-h参数可以在周期性数据输出时,输出多少行数据后输出一个表头信息,表头信息指的是表格信息中的最起始一行,展示每行的数据属于什么的信息

那么我们写入下面的例子

image-20230109005841482

下面是表头信息所表示意思的解释说明

image-20230109010030217

我们可以计算得出GC时间占运行时间的比例来得知我们的程序现在存在什么问题

image-20230109010138850

如果我们查看进程中发现OU列越来越多且呈现上升趋势,说明老年代中无法回收的对象在不断增加,很可能存在内存泄露

image-20230109010444755

Jinfo

jinfo可以查看虚拟机的配置参数信息,也可以调整虚拟机的配置参数

image-20230109012414788

其基本语法是jinfo [options] pid,进程id是必须要加上的

image-20230109012537525

jinfo -sysprops 进程id命令可以查看由System.getProperties()取得的参数

image-20230109012725473

jinfo -flags 进程id可以查看曾经赋过值的一些参数

image-20230109012740279

jinfo -flag 参数名称 进程id可以查看某个java进程的具体参数信息

image-20230109012917261

只有被标记为manageable的flag支持动态修改,这个修改能力是极其有限的

image-20230109012945573

针对boolean类型的参数的修改,我们使用 jinfo -flag [+|-]参数名称 进程id

image-20230109013107822

针对非boolean类型的参数的修改,我们使用 jinfo -flag 参数名称=参数值 进程id

image-20230109013142711

java -XX:+PrintFlagsInitial命令可以查看所有JVM参数启动的初始值

java -XX:+PrintFlagsFinal命令可以查看所有JVM参数的最终值

image-20230109013229674

java -参数名称:+PrintCommandLineFlags命令可以查看那些已经被用户或者JVM设置过的详细的XX参数的名称和值

Jmap

jmap可以打出内存的映像文件或者是查看内存的使用情况,值得注意的是其并不是只有这两个作用,只是这两个作用我们比较经常使用所以我们拿来当代表性作用而已

image-20230109020305324

其可以查看本地的,也可以查看远程的使用情况

image-20230109020346687

下面的这么多命令里,我们比较关注-dump、-heap、-histo这三个命令

image-20230109020427588

导出内存镜像文件分为手动和自动两种方式,其保存的是Java进程在某个时间点的内存快照

image-20230109020611975

手动方式有两种,第一种是记录所有对象的jmap -dump:format=b,file=<filename.hprof> 命令,第二种是只记录堆中存活对象的jmap -dump:live,format=b,file=<filename.hprof> 命令

一般来说我们推荐使用第二种命令

image-20230109020704391

自动的方式则有-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath=<filename.hprof>两种,前者表示启动自动保存,后者指定生成的dump文件的位置

image-20230109020807165

jmap -heap 进程id 命令可以用于展示某个时间点上的JVM中的堆信息,包括每个区域的内存大小,最大大小,已经使用的大小和它们的比例等

image-20230109021304007

jmap -histo 进程id 命令可以查看某一时刻堆中的类、存在的实例数量以及它们的大小

image-20230109021746673

当然还有一些其他的使用,但是无法在PC上演示,还比较小众,因此只做了解

image-20230109021829561

值得一提的是,由于jmap需要借助安全点机制,因此其生成的dump可能与实际结果存在偏差

image-20230109021903643

Jhat

jhat是JDK自带的堆分析工具,其可以分析生成的dump文件,本质是调用jdk中的服务并分析得到网址,得到结果之后我们只要进入对应的网址查看即可

image-20230109024400840

值得一提的是,这个工具比较鸡肋,后续的jdk中也已经移除,只做了解即可

image-20230109024425178

当然我们还有可以设置的参数

image-20230109024501823

Jstack

jstack可以打印JVM中的线程快照,线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合,我们可以利用其来查看各个线程的执行情况,来发现线程执行中的各种问题,比如死锁

image-20230109035235758

其基本的使用语法为jstack option pid

image-20230109035331775

当然,也有对应的各种参数

image-20230109035354329

Jcmd

jcmd是一个多功能工具,其可以实现除了jstat外所有命令的功能

image-20230109041155596

jcmd -l 命令可以列出所有的JVM进程

jcmd 进程号 help 命令可以针对指定的进程,列出支持的所有具体命令

image-20230109041310604

jcmd 进程号 具体命令 可以显示指定进程的指令命令的数据

image-20230109041339269

Jstatd

jstatd是一个RMI服务器程序,可以用于启动远程监控

image-20230109041358974

GUI篇

命令行监控工具存在先天的不足,因此出现了GUI图形化总和诊断工具,JDK自带的诊断工具有Jconsole、Visual VM、JMC,第三方的工具则有MAT、JProfiler等

image-20230109042748173

JConsole

Jconsole是JDK5时退出的java监控和管理控制台

image-20230109130105851

启动方式可以直接打开dos窗口输入jconsole

image-20230109130255271

连接方式有三种,第一种是本地连接方式,使用JConsole连接一个正在本地系统运行的JVM,并且执行程序的和运行JConsole的需要是同一个用户。JConsole使用文件系统的授权通过RMI连接起链接到平台的MBean的服务器上。这种从本地连接的监控能力只有Sun的JDK具有。

image-20230109130425819

image-20230109130446256

image-20230109130502410

第二种连接方式是远程连接方式,其使用下面的URL通过RMI连接器连接到一个JMX代理,service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi。JConsole为建立连接,需要在环境变量中设置mx.remote.credentials来指定用户名和密码,从而进行授权。

第三种连接方式是使用一个特殊的URL连接JMX代理。一般情况使用自己定制的连接器而不是RMI提供的连接器来连接JMX代理,或者是一个使用JDK1.4的实现了JMX和JMX Rmote的应用

作为监控工具,其主要的作用当然是监控JVM中的栈堆使用情况和虚拟机中的各种信息

image-20230109130841361

image-20230109130908556

image-20230109130922357

Visual VM

先来说说jvisualvm和visual vm的区别: visual vm是单独下载的工具,然后将visual vm结合到jdk中就变成了jvisualvm,仅仅是添加了一个j而已,这个j应该是java的用处,所以说jvisualvm其实就是visual vm

其实一个功能强大的多合一故障诊断和性能监控的可视化工具,自JDK6之后作为JDK的一部分发布,推荐学习

image-20230109131704226

Visual VM的一大特定就是支持插件扩展,我们建议安装上VisualGC这款插件,可以通过插件页面下载插件

image-20230109131820811

也可以在IDEA中安装插件

image-20230109132826329

image-20230109132855934

连接方式分为本地连接和远程连接两种,后者比较麻烦,我们就要不演示了

image-20230109132917312

其主要功能生成/读取堆内存快照,也就是生成dump文件

image-20230109133550918

当然,还可以加载dump文件进行分析

image-20230109133623372

还可以查看JVM参数和系统属性以及查看运行中的虚拟机进程

也可以生成/读取线程快照

image-20230109133738184

当然也可以读取线程快照

image-20230109133755956

可以实现对程序资源的实时监控,还有一些其他功能

image-20230109133817865

更多的功能则可以在对应的主界面中进行查看,CPU抽样和内存抽样主要是为了查看CPU和内存的使用情况

Eclipse MAT

MAP是一款功能强大的Java堆内存分析器,可以用于查找内存泄露以及查看内存消耗情况

image-20230109161026153

其是专门用于分析dump文件的工具,推荐用于进行对dump文件的分析

image-20230109161131043

dump文件中一般存在以下内容

image-20230109161210106

MAT并不能处理所有类型的堆存储文件,但是比较主流的格式其都支持。其最吸引人的功能还是能够生成内存泄露报表,方便定位和分析问题

image-20230109161228964

生成dump文件有下面四种方式

image-20230109161328296

利用MAP分析dump文件,可以得到下面的饼状分析图

image-20230109162213152

Mat中的histogram选项可以展示各个类的实例数目以及这些实例的Shallow heap或者Retained heap的总和

image-20230109162331105

thread overview选项可以查看系统中的Java线程和查看局部变量的信息

image-20230109162535358

with outgoing references可以获得一个对象引用了谁的列表

image-20230109162801191

with incoming references则可以获得该对象被谁引用的列表

image-20230109162822866

shallow heap指的是浅堆,浅堆指的是一个对象中的各种属性以及包括其自己的内存,这里不包括引用的其他对象的实际内存,只包括其自己的属性内存

image-20230109162852021

retained heap指的是深堆,Retained Set指的是保留集。一个对象的保留集指的是只能通过该对象直接或间接访问到的所有对象的集合,而深堆则指的是对象的保留集中的所有对象的浅堆大小之和

image-20230109162946953

下面是一个增加理解的两个案例

image-20230109163158319

image-20230109163237717

然后我们再来看一个案例分析,下面是案例代码

/**
 * 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。
 * 它由三个部分组成:Student、WebPage和StudentTrace三个类
 *
 *  -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=c:\code\student.hprof
 * @author shkstart
 * @create 16:11
 */
public class StudentTrace {
    static List<WebPage> webpages = new ArrayList<WebPage>();
​
    public static void createWebPages() {
        for (int i = 0; i < 100; i++) {
            WebPage wp = new WebPage();
            wp.setUrl("http://www." + Integer.toString(i) + ".com");
            wp.setContent(Integer.toString(i));
            webpages.add(wp);
        }
    }
​
    public static void main(String[] args) {
        createWebPages();//创建了100个网页
        //创建3个学生对象
        Student st3 = new Student(3, "Tom");
        Student st5 = new Student(5, "Jerry");
        Student st7 = new Student(7, "Lily");
​
        for (int i = 0; i < webpages.size(); i++) {
            if (i % st3.getId() == 0)
                st3.visit(webpages.get(i));
            if (i % st5.getId() == 0)
                st5.visit(webpages.get(i));
            if (i % st7.getId() == 0)
                st7.visit(webpages.get(i));
        }
        webpages.clear();
        System.gc();
    }
}
​
class Student {
    private int id;
    private String name;
    private List<WebPage> history = new ArrayList<>();
​
    public Student(int id, String name) {
        super();
        this.id = id;
        this.name = name;
    }
​
    public int getId() {
        return id;
    }
​
    public void setId(int id) {
        this.id = id;
    }
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    public List<WebPage> getHistory() {
        return history;
    }
​
    public void setHistory(List<WebPage> history) {
        this.history = history;
    }
​
    public void visit(WebPage wp) {
        if (wp != null) {
            history.add(wp);
        }
    }
}
​
​
class WebPage {
    private String url;
    private String content;
​
    public String getUrl() {
        return url;
    }
​
    public void setUrl(String url) {
        this.url = url;
    }
​
    public String getContent() {
        return content;
    }
​
    public void setContent(String content) {
        this.content = content;
    }
}

下面是分析的图片

结论: elementData数组的浅堆是80个字节,而elementData数组中的所有WebPage对象的深堆之和是1208个字节,所以加在一起就是elementData数组的深堆之和,也就是1288个字节 解释: 我说“elementData数组的浅堆是80个字节”,其中15个对象一共是60个字节,对象头8个字节,数组对象本身4个字节,这些的和是72个字节,然后总和要是8的倍数,所以“elementData数组的浅堆是80个字节” 我说“WebPage对象的深堆之和是1208个字节”,一共有15个对象,其中0、21、42、63、84、35、70不仅仅是7的倍数,还是3或者5的倍数,所以这几个数值对应的i不能计算在深堆之内,这15个对象中大多数的深堆是152个字节,但是i是0和7的那两个深堆是144个字节,所以(13152+1442)-(6*152+144)=1208,所以这也印证了我上面的话,即“WebPage对象的深堆之和是1208个字节” 因此“elementData数组的浅堆80个字节”加上“WebPage对象的深堆之和1208个字节”,正好是1288个字节,说明“elementData数组的浅堆1288个字节”

在对象引用图中,所有指向B的路径都要经过A,那么A支配对象B,离B最近的支配对象认为是直接支配者,而由直接支配者与被支配者关系描述的树状图就是支配树

image-20230109164345030

支配树的概念能用于计算一个对象能释放的空间,因为如果一个对象的直接支配者被释放,那么该对象必然也会被释放

image-20230109164510039

最后我们来说一个Tomcat的堆溢出案例分析

image-20230109164550339

首先我们查看dump文件,可以看到有一个最大的对象,最大的对象往往是嫌疑最大的引起OOM的对象,因此我们先查看该对象,查看其引用了什么对象。顺带一提Size指的是堆内存的大小

image-20230109164625429

我们能够查看到其中存在sessions对象,占用了许多空间

image-20230109164749807

我们继续点击,会发现其中存在许多实例,并且都有较大的值,所以问题很可能出在这里

image-20230109164822050

所以我们构造一个搜索语句,搜索所有的session对象,发现这种对象有许多个,总计大小占堆大小的一半

image-20230109164907793

然后我们点击session的数据值,查看其存活时间,根据session总数和时间计算出每秒的平均压力,可以推断发生堆移除的原因是因为Tomcat在短时间内接受了大量请求导致的OOM

image-20230109165014018

MAT支持一种类似SQL的查询语言,我们称之为OQL,其可以在堆中进行对象的查找和筛选

image-20230109215212814

下面是案例的示例代码

/**
 * 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。
 * 它由三个部分组成:Student、WebPage和StudentTrace三个类
 *
 *  -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=c:\code\student.hprof
 * @author shkstart
 * @create 16:11
 */
public class StudentTrace {
    static List<WebPage> webpages = new ArrayList<WebPage>();
​
    public static void createWebPages() {
        for (int i = 0; i < 100; i++) {
            WebPage wp = new WebPage();
            wp.setUrl("http://www." + Integer.toString(i) + ".com");
            wp.setContent(Integer.toString(i));
            webpages.add(wp);
        }
    }
​
    public static void main(String[] args) {
        createWebPages();//创建了100个网页
        //创建3个学生对象
        Student st3 = new Student(3, "Tom");
        Student st5 = new Student(5, "Jerry");
        Student st7 = new Student(7, "Lily");
​
        for (int i = 0; i < webpages.size(); i++) {
            if (i % st3.getId() == 0)
                st3.visit(webpages.get(i));
            if (i % st5.getId() == 0)
                st5.visit(webpages.get(i));
            if (i % st7.getId() == 0)
                st7.visit(webpages.get(i));
        }
        webpages.clear();
        System.gc();
    }
}
​
class Student {
    private int id;
    private String name;
    private List<WebPage> history = new ArrayList<>();
​
    public Student(int id, String name) {
        super();
        this.id = id;
        this.name = name;
    }
​
    public int getId() {
        return id;
    }
​
    public void setId(int id) {
        this.id = id;
    }
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    public List<WebPage> getHistory() {
        return history;
    }
​
    public void setHistory(List<WebPage> history) {
        this.history = history;
    }
​
    public void visit(WebPage wp) {
        if (wp != null) {
            history.add(wp);
        }
    }
}
​
​
class WebPage {
    private String url;
    private String content;
​
    public String getUrl() {
        return url;
    }
​
    public void setUrl(String url) {
        this.url = url;
    }
​
    public String getContent() {
        return content;
    }
​
    public void setContent(String content) {
        this.content = content;
    }
}

那么我们可以写入的查询语句有下

image-20230109215354310

当然也有对应的语法规则,分别是SELECT、FROM、WHERE和内置的对象和方法

image-20230109215440725

image-20230109215451450

image-20230109215502957

image-20230109215514399

再谈内存泄露

内存泄露指的是一个对象已经不再被使用,但是仍然存在被指向,导致GC无法回收的情况。不过宽泛来说,由于代码写的烂,导致对象的生命周期变得很长甚至导致OOM,也可以称为是内存泄露

image-20230109203059495

内存泄露指的是对象无用却无法手机,而内存溢出值得是空间不够导致的溢出,两者存在一定的因果关系

image-20230109203221372

内存泄露有八种情况下面我们来一一讲解

1-静态集合类

将变量设置会静态会这导致该变量的生命周期与JVM相同,可能会导致内存泄露

image-20230109203505745

2-单例模式

单例模式同样会因为静态属性的原因导致内存泄露

image-20230109203835759

3-内部类持有外部类

如果一个外部类的实例对象返回一个内部的实例对象,而内部类被长期引用,那么即使外部类不再被使用,但由于内部类持有外部类的引用,会导致外部类无法被回收

image-20230109211226635

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

各种连接没有显性关闭,也会导致内存泄露

image-20230109211500214

5-变量不合理的作用域

比如如果将容器类设置为成员变量,那么往其中添加对象将会导致这些对象无法被回收,解决的方法有两种,第一种是将容器类设置局部变量,第二种是每次出了方法之后将容器置空

image-20230109211535448

6-改变哈希值

当初的对象被存储进HashSet中,但是如果我们修改了对象参与哈希计算的字段,就会导致哈希结果的改变,这样就无法删除原来的对象了,会让这个无法删除的对象一直不会被回收而发生内存泄露

image-20230109213956195

下面是哈希值内存泄露的案例演示

 * 演示内存泄漏
 *
 * @author shkstart
 * @create 14:43
 */
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 + ''' +
                '}';
    }
}
例2/**
 * 演示内存泄漏
 * @author shkstart
 * @create 14:47
 */
public class ChangeHashCode1 {
    public static void main(String[] args) {
        HashSet<Point> hs = new HashSet<Point>();
        Point cc = new Point();
        cc.setX(10);//hashCode = 41
        hs.add(cc);
​
        cc.setX(20);//hashCode = 51  此行为导致了内存的泄漏
​
        System.out.println("hs.remove = " + hs.remove(cc));//false
        hs.add(cc);
        System.out.println("hs.size = " + hs.size());//size = 2
​
        System.out.println(hs);
    }
​
}
​
class Point {
    int x;
​
    public int getX() {
        return x;
    }
​
    public void setX(int x) {
        this.x = x;
    }
​
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        return result;
    }
​
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        Point other = (Point) obj;
        if (x != other.x) return false;
        return true;
    }
​
    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                '}';
    }
}

7-缓存泄露

对象放入到缓存中容易被遗忘,日积月累会导致缓存的对象特别多,会导致项目启动奇慢,因为项目启动时要加载缓存数据

image-20230109214322006

解决的方法是使用WeakHashMap的弱引用来进行缓存

下面是缓存的内存泄露的案例演示

 * 演示内存泄漏
 *
 * @author shkstart
 * @create 14:53
 */
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);
        }
    }
​
}

结果: String引用ref1,ref2,ref3,ref4 消失 WeakHashMap GC之前 obejct2=cacheObject2 obejct1=cacheObject1 WeakHashMap GC之后 HashMap GC之前 obejct4=cacheObject4 obejct3=cacheObject3 Disconnected from the target VM, address: '127.0.0.1:51628', transport: 'socket' HashMap GC之后 obejct4=cacheObject4 obejct3=cacheObject3

image-20230109214558506

8-监听器和回调

监听器和其他回调函数也容易出现内存泄露,解决的方法仍然是使用弱引用

image-20230109214704008

下面我们来看一个内存泄露的案例,先来看看案例代码

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
​
    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
​
    public void push(Object e) { //入栈
        ensureCapacity();
        elements[size++] = e;
    }
​
    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }
​
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}
​

上面的代码由于出栈时并没有取出引用,导出引用未置空,这样即使进行gc,栈中的对象也是不会释放的

image-20230109214854440

image-20230109214943655

解决方法当然是将代码置空,就是这么简单

image-20230109215002072

JProfiler

JProfiler是能在IDEA中使用的分析工具

image-20230111023149322

其具有许多优良特新

image-20230111023226356

其主要功能有以下四种

image-20230111023312127

下面是其安装和配置过程

image-20230111023419562

首先是在Jprofiler中配置IDEA

image-20230111023508825

image-20230111023537992

image-20230111023546891

然后是在IDEA中集成JProfiler

image-20230111023619893

image-20230111023629002

下面是其具体使用的一些操作说明

image-20230111023702353

image-20230111023714597

image-20230111023730786

image-20230111023747590

其数据的采集方式有两种,分别是instrumentation重构模式和Sampling抽样模式,我们推荐使用后者

image-20230111023802761

遥感监测 Telemetries可以简单查看程序中的各种情况

image-20230111023842954

内存视图 Live Memory可以看到内存中的各个信息,也可以选择自己内存信息的展示方式,一般用于分析内存中的类有哪些存在问题

image-20230111024253025

堆遍历 heap walker

堆遍历可以在内存中分析出哪个类不能进行垃圾回收时用于进一步分析

image-20230111024314404

image-20230111024330410

cpu视图 cpu views

CPU视图可以查看具体的方法占用的时间和进行方法统计,由于执行该分析会下降项目的性能,因此默认不开启

image-20230111024613123

image-20230111024622519

线程视图 threads

在此中可以查看线程的阻塞情况,也可以创建线程的dump文件

image-20230111024850532

image-20230111024859575

监视器&锁 Monitors&locks

可以查看死锁或者是当前使用的监视器等

image-20230111024933763

下面我们来看一个案例,下面是源码

public class MemoryLeak {
    public static void main(String[] args) {
        while (true) {
            ArrayList beanList = new ArrayList();
            for (int i = 0; i < 500; i++) {
                Bean data = new Bean();
                data.list.add(new byte[1024 * 10]);//10kb
                beanList.add(data);
            }
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
​
}
​
class Bean {
    int size = 10;
    String info = "hello,atguigu";
    static ArrayList list = new ArrayList();
}

下面是分析过程

image-20230111025058020

image-20230111025111801

这款工具非常不错,不过是要收费的

Arthas

上面介绍的工具虽然好,但是其需要在服务端项目配置相关监控参数,然后远程连接监控,还需要收费,比较麻烦,因此出现了Arthas,其是一项命令行界面的项目问题排查工具

image-20230111040921813

image-20230111040934371

阿尔萨斯能够实现在线排查问题,无需重启项目

image-20230111041030237

其基于各种开发项目总和开发而来

image-20230111041107506

这是阿尔萨斯的官方使用文档的地址arthas.aliyun.com/doc/quick-s…

下面是其安装方式

image-20230111041205925

工程目录的说明

image-20230111041222179

其启动方式

image-20230111041233867

查看进程的命令是jps,查看日志的命令是cat ~/logs/arthas/arthas.log,查看帮助的命令是java -jar arthas-boot.jar -h

其还支持在网页端访问,不过也是控制台的形式

image-20230111041347566

有两种退出方式

image-20230111041406249

下面是其基础命令,标蓝的命令比较重要

image-20230111041417552

JVM相关的命令

image-20230111041507876

class/classloader相关

image-20230111041547675

monitor/watch/trace相关

image-20230111041601479

其他命令

image-20230111041614734

具体的命令里还有说明,但是呢,这些不是很重要,需要的时候自己去看阿尔萨斯的说明吧

Java Misssion Control

Java Misssion Control简称JMC,也是Oracle提供的工具之一,先来看看其历史

image-20230111043708619

其启动方式可以直接在JDK中点击

image-20230111043739722

下面是简介

image-20230111043754892

其可以实时监控JVM运行时的状态

image-20230111043808770

JFR是JMC中的一个组件,其可以用极低的性能手机Java虚拟机的性能数据

image-20230111043829830

JFR的事件类型一共分为四种,分别是瞬时事件、持续事件、计时事件和取样事件

image-20230111043911483

JFR的启动方式有三种,第一种是方式1-XX:StartFlightRecording=参数

image-20230111044019197

第二种是使用jcmd的JFR.*子命令

image-20230111044035891

第三种方式是直接在界面中启动JFR

image-20230111044108182

image-20230111044149918

image-20230111044201480

image-20230111044209247

image-20230111044214645

注意取样之前必须要在IDEA上添加对应的参数

image-20230111044232399

取样成功之后我们可以得到下面的展示内容

image-20230111044253341

下面是我们用于飞行记录仪上的项目代码

/**
 * -Xms600m -Xmx600m -XX:SurvivorRatio=8
 * @author shkstart  shkstart@126.com
 * @create 2020  21:12
 */
public class OOMTest {
    public static void main(String[] args) {
        ArrayList<Picture> list = new ArrayList<>();
        while(true){
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new Picture(new Random().nextInt(100 * 50)));
        }
    }
}
​
class Picture{
    private byte[] pixels;
​
    public Picture(int length) {
        this.pixels = new byte[length];
    }
​
    public byte[] getPixels() {
        return pixels;
    }
​
    public void setPixels(byte[] pixels) {
        this.pixels = pixels;
    }
}
​

下面是结果

image-20230111044355623

image-20230111044428121

image-20230111044436991

image-20230111044446347

其他工具

Flame Graphs(火焰图)可以非常直观地让我们了解cpu在整个生命周期过程中时间是如何分配的

image-20230111044518490

Tprofiler可以让我们快速定位性能代码,不过现在已经不再更新了

image-20230111044552336

Btrace是一个Java平台的安全的动态追踪工具

image-20230111044619536

Spring Insight在我们的项目是用Spring开发的时候推荐使用

JVM参数选项

参数选项可以分为三种,分别是标准参数选项、-X参数选项和-XX参数选项

标准参数选项比较稳定,后续版本基本不会变化

image-20230111050606816

下面是各种标准参数选项

-d32          使用 32 位数据模型 (如果可用)
-d64          使用 64 位数据模型 (如果可用)
-server       选择 "server" VM
               默认 VM 是 server.
 
-cp <目录和 zip/jar 文件的类搜索路径>
-classpath <目录和 zip/jar 文件的类搜索路径>
               用 ; 分隔的目录, JAR 档案
               和 ZIP 档案列表, 用于搜索类文件。
-D<名称>=<值>
               设置系统属性
-verbose:[class|gc|jni]
               启用详细输出
-version      输出产品版本并退出
-version:<值>
               警告: 此功能已过时, 将在
               未来发行版中删除。
               需要指定的版本才能运行
-showversion  输出产品版本并继续
-jre-restrict-search | -no-jre-restrict-search
               警告: 此功能已过时, 将在
               未来发行版中删除。
               在版本搜索中包括/排除用户专用 JRE
-? -help      输出此帮助消息
-X            输出非标准选项的帮助
-ea[:<packagename>...|:<classname>]
-enableassertions[:<packagename>...|:<classname>]
               按指定的粒度启用断言
-da[:<packagename>...|:<classname>]
-disableassertions[:<packagename>...|:<classname>]
               禁用具有指定粒度的断言
-esa | -enablesystemassertions
               启用系统断言
-dsa | -disablesystemassertions
               禁用系统断言
-agentlib:<libname>[=<选项>]
               加载本机代理库 <libname>, 例如 -agentlib:hprof
               另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<选项>]
               按完整路径名加载本机代理库
-javaagent:<jarpath>[=<选项>]
               加载 Java 编程语言代理, 请参阅 java.lang.instrument
-splash:<imagepath>
               使用指定的图像显示启动屏幕
 
​

还可以选择HotSpot虚拟机的模式

image-20230111050706497

第二种是-X的参数选项,其仍然是比较稳定,但是可能会在后续版本中变更

image-20230111050758993

下面是-X选项参数的各种选项

-Xmixed        混合模式执行 (默认)
-Xint             仅解释模式执行
-Xcomp        仅采用即时编译器模式
-Xbootclasspath:<用 ; 分隔的目录和 zip/jar 文件>
                   设置搜索路径以引导类和资源
-Xbootclasspath/a:<用 ; 分隔的目录和 zip/jar 文件>
                   附加在引导类路径末尾
-Xbootclasspath/p:<用 ; 分隔的目录和 zip/jar 文件>
                   置于引导类路径之前
-Xdiag            显示附加诊断消息
-Xnoclassgc       禁用类垃圾收集
-Xincgc           启用增量垃圾收集
-Xloggc:<file>    将 GC 状态记录在文件中 (带时间戳)
-Xbatch           禁用后台编译
-Xms<size>        设置初始 Java 堆大小
-Xmx<size>        设置最大 Java 堆大小
-Xss<size>        设置 Java 线程堆栈大小
-Xprof            输出 cpu 配置文件数据
-Xfuture          启用最严格的检查, 预期将来的默认值
-Xrs              减少 Java/VM 对操作系统信号的使用 (请参阅文档)
-Xcheck:jni       对 JNI 函数执行其他检查
-Xshare:off       不尝试使用共享类数据
-Xshare:auto      在可能的情况下使用共享类数据 (默认)
-Xshare:on        要求使用共享类数据, 否则将失败。
-XshowSettings    显示所有设置并继续
-XshowSettings:all
                   显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
                   显示所有属性设置并继续
-XshowSettings:locale
                   显示所有与区域设置相关的设置并继续
 
-X 选项是非标准选项,如有更改,恕不另行通知
 

-XX参数选项是使用最多的参数类型,但是其改动比较频繁,主要用于开发和调试JVM

image-20230111051022710

Boolean类型格式的命令举例

image-20230111051137853

数值类型格式的命令举例

image-20230111051155069

非数值类型的格式命令举例

image-20230111051209392

如何添加JVM参数

首先是IDEA中的方式

image-20230111051814273

其次我们可以在控制台上直接运行对应的jar包,只要运行前加上对应的参数选项即可比如命令java -Xms50m -Xmx50m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -jar demo.jar

我们可以在Tomcat或者运行过程中进行参数的设置

image-20230111051907957

常用的JVM参数选项

首先是打印设置的XX选项及值

image-20230111052829164

对于-XX:+PrintFlagsFinal命令的补充说明

image-20230111053229282

堆、栈、方法区等内存大小都有对应的命令

先来看看栈的相关命令

image-20230111053042064

然后是堆内存的相关命令

image-20230111053311212

关于-XX:SurvivorRatio=8命令的说明

只有显示使用Eden区和Survivor区的比例,才会让比例生效,否则比例都会自动设置,至于其中的原因,请看下面的-XX:+UseAdaptiveSizePolicy中的解释,最后推荐使用默认打开的-XX:+UseAdaptiveSizePolicy设置,并且不显示设置-XX:SurvivorRatio

关于-XX:+UseAdaptiveSizePolicy说明

1、分析
默认开启,将会导致Eden区和Survivor区的比例自动分配,因此也会引起我们默认值-XX:SurvivorRatio=8失效,所以真实比例可能不是8,比如可能是62、如何设置Eden区和Survivor区的比例
-XX:SurvivorRatio=8
显示使用Eden区和Survivor区的比例,那就使用我自己的
没有显示使用Eden区和Survivor区的比例,无论打开或者关闭-XX:+UseAdaptiveSizePolicy,都会自动设置Eden区和Survivor区的比例
 
结论:
只有显示使用Eden区和Survivor区的比例,才会让比例生效,否则比例都会自动设置,最后推荐使用默认打开的-XX:+UseAdaptiveSizePolicy设置,并且不显示设置-XX:SurvivorRatio

-XX:NewRatio=2命令最好根据实际情况进行设置,主要根据对象生命周期来进行分配,如果对象生命周期很长,那么让老年代大一点,否则让新生代大一点

-XX:PretenureSizeThreadshold=1024命令的问题在于不好控制

-XX:MaxTenuringThreshold=15命令使用较少,一般使用默认值

接着是方法区的相关命令

image-20230111053122169

最后是直接内存的相关命令

image-20230111053144468

各类的垃圾回收也有对应的参数

查看默认的垃圾回收器的命令如下

image-20230111055600928

Serial回收器的命令

image-20230111055620750

Parnew回收器的命令

image-20230111055634227

Parallel回收器的命令

image-20230111055650327

CMS回收器的命令

image-20230111055708721

image-20230111055717366

image-20230111055724791

G1回收器的命令

image-20230111055741033

Mixed GC的调优参数

image-20230111055756457

怎么选择垃圾收集器

image-20230111055902115

OutOfMemory相关的选项如下

image-20230111053609070

-XX:+HeapDumpOnOutMemoryError(在出现OOM的时候 生成dump文件)和-XX:+HeapDumpBeforeFullGC(在出现Full GC的时候生成dump文件)只能设置1个 ,如果不设置-XX:HeapDumpPath=,那么将会在当前目录下生成dump文件,如果设置的话,将会在指定位置生成dump文件

当然还有GC日志的相关参数命令

image-20230111053735105

最后是一些其他参数

image-20230111053804986

通过Java代码获取JVM参数

我们可以通过Java代码来获取JVM的相关参数

image-20230111053856572

可以利用这些特性来做一些阈值报警处理,下面是这么做的例子

/**
 *
 * 监控我们的应用服务器的堆内存使用情况,设置一些阈值进行报警等处理
 *
 * @author shkstart
 * @create 15:23
 */
public class MemoryMonitor {
    public static void main(String[] args) {
        MemoryMXBean memorymbean = ManagementFactory.getMemoryMXBean();
        MemoryUsage usage = memorymbean.getHeapMemoryUsage();
        System.out.println("INIT HEAP: " + usage.getInit() / 1024 / 1024 + "m");
        System.out.println("MAX HEAP: " + usage.getMax() / 1024 / 1024 + "m");
        System.out.println("USE HEAP: " + usage.getUsed() / 1024 / 1024 + "m");
        System.out.println("\nFull Information:");
        System.out.println("Heap Memory Usage: " + memorymbean.getHeapMemoryUsage());
        System.out.println("Non-Heap Memory Usage: " + memorymbean.getNonHeapMemoryUsage());

        System.out.println("=======================通过java来获取相关系统状态============================ ");
        System.out.println("当前堆内存大小totalMemory " + (int) Runtime.getRuntime().totalMemory() / 1024 / 1024 + "m");// 当前堆内存大小
        System.out.println("空闲堆内存大小freeMemory " + (int) Runtime.getRuntime().freeMemory() / 1024 / 1024 + "m");// 空闲堆内存大小
        System.out.println("最大可用总堆内存maxMemory " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "m");// 最大可用总堆内存大小

    }
}

上篇我们就做过通过Runtime获取对应的JVM参数的例子

image-20230111053948659

GC日志参数

GC日志也有对应的参数

image-20230111062058675

GC日志格式

先来复习下GC的分类以及GC会发生的情况

image-20230111062206239

不同GC分类的GC细节不同,下面是演示的代码

/**
 *  -XX:+PrintCommandLineFlags
 *
 *  -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代使用Serial Old GC
 *
 *  -XX:+UseParNewGC:标明新生代使用ParNew GC
 *
 *  -XX:+UseParallelGC:表明新生代使用Parallel GC
 *  -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC
 *  说明:二者可以相互激活
 *
 *  -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同时,年轻代会触发对ParNew 的使用
 * @author shkstart
 * @create 17:19
 */
public class GCUseTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();

        while(true){
            byte[] arr = new byte[1024 * 10];//10kb
            list.add(arr);
//            try {
//                Thread.sleep(5);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
        }
    }
} 

老年代使用CMS GC的结果如下

image-20230111062321075

新生代使用Serial GC的结果如下

image-20230111062340766

GC日志分为MinorGC和FullGC两种

image-20230111062430509

image-20230111062438511

前者的示意图比后者的短,不过实际上将他们的格式都是由同一个规律的字段排序组成的

然后来做GC日志的结构剖析,首先是垃圾收集器的日志分析

image-20230111062553854

然后是GC前后情况

image-20230111062631647

最后是GC时间

image-20230111062647755

下面是YGC,也就是Minor GC的日志的内容解析

image-20230111062734588

这是日志原文

image-20230111062806227

这是Full GC的

image-20230111062832964

这是其日志原文

image-20230111062846853

GC日志分析工具

不过我们肯定不会自己慢慢看日志的,我们有日志分析工具

GCEasy是一款在线分析GC日志的工具,部分功能需要需要收费

image-20230111064948169

GCViewer可以实现离线分析GC日志,就是界面真的挺丑

image-20230111065016411

下载和安装的方法

image-20230111065039036

最后是一些其他工具