07内存调优

内存泄漏(memory leak):在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上,这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。内存泄漏绝大多数情况都是由堆内存泄漏引起的,所以后续没有特别说明则讨论的都是堆内存泄漏。

少量的内存泄漏可以容忍,但是如果发生持续的内存泄漏,就像滚雪球雪球越滚越大,不管有多大的内存迟早会被消耗完,最终导致的结果就是内存溢出但是产生内存溢出并不是只有内存泄漏这一种原因

1.内存溢出常见场景

1、内存泄漏导致溢出的常见场景是大型的Java后端应用中,在处理用户的请求之后,没有及时将用户的数据删除。随着用户请求数量越来越多,内存泄漏的对象占满了堆内存最终导致内存溢出。这种产生的内存溢出会直接导致用户请求无法处理。重启可以恢复应用,但是在运行一段时间之后依然会出现内存溢出。

如下图所示,如果只调用login创建用户,使用完成后不调用logout,就会内存泄漏。

@RestController
@RequestMapping("/leak2")
public class LeakController2 {
    private static Map<Long,Object> userCache = new HashMap<>();

    /**
     * 登录接口 放入hashmap中
     */
    @PostMapping("/login")
    public void login(String name,Long id){
        userCache.put(id,new byte[1024 * 1024 * 300]);
    }


    /**
     * 登出接口,删除缓存的用户信息
     */

    @GetMapping("/logout")
    public void logout(Long id){
        userCache.remove(id);
    }

}

2、第二种常见场景是分布式任务调度系统如Elastic-job、Quartz等进行任务调度时,被调度的Java应用在调度任务结束中出现了内存泄漏,最终导致多次调度之后内存溢出。这种产生的内存溢出会导致应用执行下次的调度任务执行。同样重启可以恢复应用使用,但是在调度执行一段时间之后依然会出现内存溢出。

如下图所示,使用定时任务,每隔一段时间添加一个数据,最终会内存溢出。

@Component
public class LeakTask {

    private int count = 0;
    private List<Object> list = new ArrayList<>();

    @Scheduled(fixedRate = 100L)
    public void test(){
        System.out.println("定时任务调用" + ++count);
        list.add(new Outer().newList());
    }
}

既然知道了可能出现内存溢出的问题,有什么工具可以用来解决这些问题?

2.监控内存泄漏/溢出的常用工具

2.1 Top命令

top命令是linux下用来查看系统信息的一个命令,它提供给我们去实时地去查看系统的资源,比如执行时的进程、线程和系统参数等信息。进程使用的内存为RES(常驻内存)- SHR(共享内存)

优点:

  • 操作简单
  • 无额外的软件安装

缺点:只能查看最基础的进程信息,无法查看到每个部分的内存占用(堆、方法区、堆外)

2.2 VisualVM

VisualVM是多功能合一的Java故障排除工具并且他是一款可视化工具,整合了命令行 JDK 工具和轻量级分析功能,功能非常强大。这款软件在Oracle JDK 6~8 中发布,但是在 Oracle JDK 9 之后不在JDK安装目录下需要单独下载。下载地址:https://visualvm.github.io/

优点:

  • 功能丰富,实时监控CPU、内存、线程等详细信息
  • 支持Idea插件,开发过程中也可以使用

缺点:

对大量集群化部署的Java进程需要手动进行管理

如果需要进行远程监控,可以通过jmx方式进行连接。在启动java程序时添加如下参数:

-Djava.rmi.server.hostname=服务器ip地址
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9122
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false

右键点击remote

填写服务器的ip地址:

右键添加JMX连接

填写ip地址和端口号,勾选不需要SSL安全验证:

双击成功连接。

2.3 Arthas

以前已经介绍过了:简介 | arthas。这里介绍如何使用阿里arthas tunnel管理所有的需要监控的程序

步骤:

在Spring Boot程序中添加arthas的依赖(支持Spring Boot2),在配置文件中添加tunnel服务端的地址,便于tunnel去监控所有的程序。

2. 将tunnel服务端程序部署在某台服务器上并启动。

3. 启动java程序

4. 打开tunnel的服务端页面,查看所有的进程列表,并选择进程进行arthas的操作。

pom.xml添加依赖:

<dependency>
    <groupId>com.taobao.arthas</groupId>
    <artifactId>arthas-spring-boot-starter</artifactId>
    <version>3.7.1</version>
</dependency>

application.yml中添加配置:

arthas:
  #tunnel地址,目前是部署在同一台服务器,正式环境需要拆分
  tunnel-server: ws://localhost:7777/ws
  #tunnel显示的应用名称,直接使用应用名
  app-name: ${spring.application.name}
  #arthas http访问的端口和远程连接的端口
  http-port: 8888
  telnet-port: 9999

然后直接就可以把这个项目部署到服务器上,同时还需要启动一个tunnel的官方jar包Releases · alibaba/arthasnohup java -jar -Darthas.enable-detail-pages=true arthas-tunnel-server.3.7.1-fatjar.jar & 命令启动该程序。-Darthas.enable-detail-pages=true参数作用是可以有一个页面展示内容。通过服务器ip地址:8080/apps.html打开页面,查看注册上来任何应用。

注意,若一台服务器上启动多个需要tunnel监测的jar包,第二个及以后的端口需要改变,防止端口冲突。

最终就能看到两个应用:

单击应用就可以进入操作arthas了。

2.4 Prometheus+Grafana

Prometheus+Grafana是企业中运维常用的监控方案,其中Prometheus用来采集系统或者应用的相关数据,同时具备告警功能。Grafana可以将Prometheus采集到的数据以可视化的方式进行展示。

Java程序员要学会如何读懂Grafana展示的Java虚拟机相关的参数。

优点:

  • 支持系统级别和应用级别的监控,比如linux操作系统、Redis、MySQL、Java进程。
  • 支持告警并允许自定义告警指标,通过邮件、短信等方式尽早通知相关人员进行处理

缺点:

环境搭建较为复杂,一般由运维人员完成

如果想要自己搭建,教程:大厂是怎么监控 Java 项目的?保姆级教程 | Prometheus + Grafana 可观测性实战_哔哩哔哩_bilibili

3.正常堆内存图像与非正常堆内存图像

  • 正常情况
    • 处理业务时会出现上下起伏,业务对象频繁创建内存会升高,触发MinorGC之后内存会降下来。
    • 手动执行FULL GC之后,内存大小会骤降,而且每次降完之后的大小是接近的。
    • 长时间观察内存曲线应该是在一个范围内。
  • 出现内存泄漏
    • 处于持续增长的情况,即使Minor GC也不能把大部分对象回收
    • 手动FULL GC之后的内存量每一次都在增长
    • 长时间观察内存曲线持续增长

4.代码中的内存泄漏

总结了6种产生内存泄漏的原因,均来自于java代码的不当处理:

  • equals()和hashCode(),不正确的equals()和hashCode()实现导致内存泄漏
  • ThreadLocal的使用,由于线程池中的线程不被回收导致的ThreadLocal内存泄漏
  • 内部类引用外部类,非静态的内部类和匿名内部类的错误使用导致内存泄漏
  • String的intern方法,由于JDK6中的字符串常量池位于永久代,intern被大量调用并保存产生的内存泄漏
  • 通过静态字段保存对象,大量的数据在静态变量中被引用,但是不再使用,成为了内存泄漏
  • 资源没有正常关闭,由于资源没有调用close方法正常关闭,导致的内存溢出

4.1 equals()和hashCode()导致的内存泄漏

在定义新类时没有重写正确的equals()和hashCode()方法。在使用HashMap的场景下,如果使用这个类对象作为key,HashMap在判断key是否已经存在时会使用这些方法,如果重写方式不正确,会导致相同的数据被保存多份。

正常情况:

1、以JDK8为例,首先调用hash方法计算key的哈希值,hash方法中会使用到key的hashcode方法。根据hash方法的结果决定存放的数组中位置。

2、如果没有元素,直接放入。如果有元素,先判断key是否相等,会用到equals方法,如果key相等,直接替换value;key不相等,走链表或者红黑树查找逻辑,其中也会使用equals比对是否相同。

异常情况:

1、hashCode方法实现不正确,会导致相同id的学生对象计算出来的hash值不同,可能会被分到不同的槽中。

2、equals方法实现不正确,会导致key在比对时,即便学生对象的id是相同的,也被认为是不同的key。

3、长时间运行之后HashMap中会保存大量相同id的学生数据。

4.2 内部类引用外部类

1、非静态的内部类默认会持有外部类,尽管代码上不再使用外部类,所以如果有地方引用了这个非静态内部类,会导致外部类也被引用,垃圾回收时无法回收这个外部类。

2、匿名内部类对象如果在非静态方法中被创建,会持有调用者对象,垃圾回收时无法回收调用者。

package com.itheima.jvmoptimize.leakdemo.demo3;

import java.io.IOException;
import java.util.ArrayList;

public class Outer{
    private byte[] bytes = new byte[1024 * 1024]; //外部类持有数据
    private static String name  = "测试";
    class Inner{
        private String name;
        public Inner() {
            this.name = Outer.name;
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
//        System.in.read();
        int count = 0;
        ArrayList<Inner> inners = new ArrayList<>();
        while (true){
            if(count++ % 100 == 0){
                Thread.sleep(10);
            }
            inners.add(new Outer().new Inner());
        }
    }
}
packagecom.itheima.jvmoptimize.leakdemo.demo4;

importjava.io.IOException;
importjava.util.ArrayList;
importjava.util.List;

publicclassOuter {
    privatebyte[] bytes = newbyte[1024 * 1024 * 10];
    publicList<String> newList() {
        List<String> list = newArrayList<String>() {{
            add("1");
            add("2");
        }};
        returnlist;
    }

    publicstaticvoidmain(String[] args) throwsIOException {
        System.in.read();
        intcount = 0;
        ArrayList<Object> objects = newArrayList<>();
        while(true){
            System.out.println(++count);
            objects.add(newOuter().newList());
        }
    }
}

解决方案:

1、这个案例中,使用内部类的原因是可以直接获取到外部类中的成员变量值,简化开发。如果不想持有外部类对象,应该使用静态内部类。

2、使用静态方法,可以避免匿名内部类持有调用者对象。

package com.itheima.jvmoptimize.leakdemo.demo3;

import java.io.IOException;
import java.util.ArrayList;

public class Outer{
    private byte[] bytes = new byte[1024 * 1024]; //外部类持有数据
    private static String name  = "测试";
    static class Inner{
        private String name;
        public Inner() {
            this.name = Outer.name;
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
//        System.in.read();
        int count = 0;
        ArrayList<Inner> inners = new ArrayList<>();
        while (true){
            if(count++ % 100 == 0){
                Thread.sleep(10);
            }
            inners.add(new Inner());
        }
    }
}
packagecom.itheima.jvmoptimize.leakdemo.demo4;

importjava.io.IOException;
importjava.util.ArrayList;
importjava.util.List;

publicclassOuter {
    privatebyte[] bytes = newbyte[1024 * 1024 * 10];
    publicstaticList<String> newList() {
        List<String> list = newArrayList<String>() {{
            add("1");
            add("2");
        }};
        returnlist;
    }

    publicstaticvoidmain(String[] args) throwsIOException {
        System.in.read();
        intcount = 0;
        ArrayList<Object> objects = newArrayList<>();
        while(true){
            System.out.println(++count);
            objects.add(newList());
        }
    }
}

最后提醒一下,内部类引用外部类在开发过程中并不常见,但是在框架开发的过程中可能出现。

4.3 ThreadLocal的使用

如果仅仅使用手动创建的线程,就算没有调用ThreadLocal的remove方法清理数据,也不会产生内存泄漏。因为当线程被回收时,ThreadLocal也同样被回收。但是如果使用线程池就不一定了。

package com.itheima.jvmoptimize.leakdemo.demo5;

import java.util.concurrent.*;

public class Demo5 {
    public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(Integer.MAX_VALUE, Integer.MAX_VALUE,
                0, TimeUnit.DAYS, new SynchronousQueue<>());
        int count = 0;
        while (true) {
            System.out.println(++count);
            threadPoolExecutor.execute(() -> {
                threadLocal.set(new byte[1024 * 1024]);
            });
            Thread.sleep(10);
        }


    }
}

运行一段时间后就报错了,因为创建的线程放入线程池,并没有释放。

解决方案:

线程方法执行完,一定要调用ThreadLocal中的remove方法清理对象。

package com.itheima.jvmoptimize.leakdemo.demo5;

import java.util.concurrent.*;

public class Demo5 {
    public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(Integer.MAX_VALUE, Integer.MAX_VALUE,
                0, TimeUnit.DAYS, new SynchronousQueue<>());
        int count = 0;
        while (true) {
            System.out.println(++count);
            threadPoolExecutor.execute(() -> {
                threadLocal.set(new byte[1024 * 1024]);
                threadLocal.remove();
            });
            Thread.sleep(10);
        }


    }
}

4.4 String的intern方法

JDK6中字符串常量池位于堆内存中的Perm Gen永久代中,如果不同字符串的intern方法被大量调用,字符串常量池会不停的变大超过永久代内存上限之后就会产生内存溢出问题。

package com.itheima.jvmoptimize.leakdemo.demo6;

import org.apache.commons.lang3.RandomStringUtils;

import java.util.ArrayList;
import java.util.List;

public class Demo6 {
    public static void main(String[] args) {
        while (true){
            List<String> list = new ArrayList<String>();
            int i = 0;
            while (true) {
                //String.valueOf(i++).intern(); //JDK1.6 perm gen 不会溢出
                list.add(String.valueOf(i++).intern()); //溢出
            }
        }
    }
}

解决方案:

1、注意代码中的逻辑,尽量不要将随机生成的字符串加入字符串常量池

2、增大永久代空间的大小,根据实际的测试/估算结果进行设置-XX:MaxPermSize=256M

4.5 通过静态字段保存对象

问题:

如果大量的数据在静态变量中被长期引用,数据就不会被释放,如果这些数据不再使用,就成为了内存泄漏。

解决方案:

1、尽量减少将对象长时间的保存在静态变量中,如果不再使用,必须将对象删除(比如在集合中)或者将静态变量设置为null。

2、使用单例模式时,尽量使用懒加载,而不是立即加载。

packagecom.itheima.jvmoptimize.leakdemo.demo7;

importorg.springframework.context.annotation.Lazy;
importorg.springframework.stereotype.Component;

@Lazy //懒加载
@Component
publicclassTestLazy {
    privatebyte[] bytes = newbyte[1024 * 1024 * 1024];
}

3、Spring的Bean中不要长期存放大对象,如果是缓存用于提升性能,尽量设置过期时间定期失效。

packagecom.itheima.jvmoptimize.leakdemo.demo7;

importcom.github.benmanes.caffeine.cache.Cache;
importcom.github.benmanes.caffeine.cache.Caffeine;

importjava.time.Duration;

publicclassCaffineDemo {
    publicstaticvoidmain(String[] args) throwsInterruptedException {
        Cache<Object, Object> build = Caffeine.newBuilder()
        //设置100ms之后就过期
                 .expireAfterWrite(Duration.ofMillis(100))
                .build();
        intcount = 0;
        while(true){
            build.put(count++,newbyte[1024 * 1024 * 10]);
            Thread.sleep(100L);
        }
    }
}

4.6 资源没有正常关闭

连接和流这些资源会占用内存,如果使用完之后没有关闭,这部分内存不一定会出现内存泄漏,但是会导致close方法不被执行。

package com.itheima.jvmoptimize.leakdemo.demo1;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.sql.*;

//-Xmx50m -Xms50m
public class Demo1 {

    // JDBC driver name and database URL
    static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";
    static final String DB_URL = "jdbc:mysql:///bank1";

    //  Database credentials
    static final String USER = "root";
    static final String PASS = "123456";

    public static void leak() throws SQLException {
        //Connection conn = null;
        Statement stmt = null;
        Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);

        // executes a valid query
        stmt = conn.createStatement();
        String sql;
        sql = "SELECT id, account_name FROM account_info";
        ResultSet rs = stmt.executeQuery(sql);

        //STEP 4: Extract data from result set
        while (rs.next()) {
            //Retrieve by column name
            int id = rs.getInt("id");
            String name = rs.getString("account_name");

            //Display values
            System.out.print("ID: " + id);
            System.out.print(", Name: " + name + "\n");
        }

    }

    public static void main(String[] args) throws InterruptedException, SQLException {
        while (true) {
            leak();
        }
    }
}

这段代码会不会产生内存泄漏,应该是不会的。但是这个结论不是确定的,所以建议编程时养成良好的习惯,尽量关闭不再使用的资源。

解决方案:

1、为了防止出现这类的资源对象泄漏问题,必须在finally块中关闭不再使用的资源。

2、从 Java 7 开始,使用try-with-resources语法可以用于自动关闭资源。

5.并发请求导致的内存溢出

通过发送请求向Java应用获取数据,正常情况下Java应用将数据返回之后,这部分数据就可以在内存中被释放掉。

接收到请求时创建对象,响应返回之后,对象就可以被回收掉。

并发请求问题指的是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于内存中,最终超过了内存的上限,导致内存溢出。这类问题的处理思路和内存泄漏类似,首先要定位到对象产生的根源。

那么怎么模拟并发请求呢?

使用Apache Jmeter软件可以进行并发请求测试。

Apache Jmeter是一款开源的测试软件,使用Java语言编写,最初是为了测试Web程序,目前已经发展成支持数据库、消息队列、邮件协议等不同类型内容的测试工具。

jmeter教程:zublog.xin/archives/188,下图是我创建的测试用例

发送大量请求后很快内存溢出。

这就是并发情况下导致内存溢出的情况。

6.内存泄漏/溢出的诊断方法

我们讲了常见的内存泄漏/溢出的情况了。但是如果在真实开发过程中出现了内存泄漏/溢出,我们该如何定位到底是哪里出了问题呢?

诊断通常分为两步:内存溢出时快照/内存未溢出时快照、MAT内存泄漏检测。

MAT下载地址:Eclipse Memory Analyzer | projects.eclipse.org

6.1 内存溢出时快照

当堆内存溢出时,需要在堆内存溢出时将整个堆内存保存下来,生成内存快照(Heap Profile )文件。

使用MAT打开hprof文件,并选择内存泄漏检测功能,MAT会自行根据内存快照中保存的数据分析内存泄漏的根源。

生成内存快照的Java虚拟机参数:

-XX:+HeapDumpOnOutOfMemoryError:发生OutOfMemoryError错误时,自动生成hprof内存快照文件。

-XX:HeapDumpPath=<path>:指定hprof文件的输出路径。

使用MAT打开hprof文件,并选择内存泄漏检测功能,MAT会自行根据内存快照中保存的数据分析内存泄漏的根源。

使用MAT打开hprof文件,首页就展示了MAT检测出来的内存泄漏问题原因。

点击Details查看详情,这个线程持有了大量的字节数组:

继续往下来,还可以看到溢出时线程栈,通过栈信息也可以怀疑下是否是因为这句代码创建了大量的对象:

MAT内存泄漏检测的原理

MAT提供了称为支配树(Dominator Tree)的对象图。支配树展示的是对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。

如下图,A引用B、C,B、C引用D, C引用E,D、E引用F,转成支配树之后。由于E只有C引用,所以E挂在C上。接下来B、C、D、F都由其他至少1个对象引用,所以追溯上去,只有A满足支配它们的条件。

支配树中对象本身占用的空间称之为浅堆(Shallow Heap)。

支配树中对象的子树就是所有被该对象支配的内容,这些内容组成了对象的深堆(Retained Heap),也称之为保留集( Retained Set ) 。深堆的大小表示该对象如果可以被回收,能释放多大的内存空间。

如下图:C自身包含一个浅堆,而C底下挂了E,所以C+E占用的空间大小代表C的深堆。

6.2 内存溢出时快照(案例)

背景:小李负责的新闻资讯类项目采用了微服务架构,其中有一个文章微服务,这个微服务在业务高峰期出现了内存溢出的现象。

解决思路:

1、服务出现OOM内存溢出时,生成内存快照。

2、使用MAT分析内存快照,找到内存溢出的对象。

3、尝试在开发环境中重现问题,分析代码中问题产生的原因。

4、修改代码。

5、测试并验证结果。

首先将项目打包,放到服务器上,同时使用如下启动命令启动。设置了最大堆内存为512m,同时堆内存溢出时会生成hprof文件:

编写JMeter脚本进行压测,size数据量一次性获取10000条,线程150,每个线程执行10次方法调用:

执行之后可以发现服务器上已经生成了hprof文件:

将其下载到本地,通过MAT分析发现是Mysql返回的ResultSet存在大量的数据:

通过支配树,可以发现里边包含的数据,如果数据有一些特殊的标识,其实就可以判断出来是哪个接口产生的数据:

如果想知道每个线程在执行哪个方法,先找到spring的HandlerMethod对象:

接着去找引用关系:

通过描述信息就可以看到接口:

通过直方图的查找功能,也可以找到项目里哪些对象比较多:

问题根源:

文章微服务中的分页接口没有限制最大单次访问条数,并且单个文章对象占用的内存量较大,在业务高峰期并发量较大时这部分从数据库获取到内存之后会占用大量的内存空间。

解决思路:

1、与产品设计人员沟通,限制最大的单次访问条数。

以下代码,限制了每次访问的最大条数为100条

2、分页接口如果只是为了展示文章列表,不需要获取文章内容,可以大大减少对象的大小。

把文章内容去掉,减少对象大小:

3、在高峰期对微服务进行限流保护。

6.3 内存未溢出时快照

刚才我们的程序都是已经出现了内存溢出的,然后去查看快照,接下来我们要做到防范于未然,一旦看到内存大量增长就去分析内存快照,那此时内存还没溢出,怎么样去获得内存快照文件呢?

背景:小李的团队通过监控系统发现有一个服务内存在持续增长,希望尽快通过内存快照分析增长的原因,由于并未产生内存溢出所以不能通过HeapDumpOnOutOfMemoryError参数生成内存快照。

思路:

导出运行中系统的内存快照,比较简单的方式有两种,注意只需要导出标记为存活的对象:

通过JDK自带的jmap命令导出,格式为:

jmap -dump:live,format=b,file=文件路径和文件名 进程ID

通过arthas的heapdump命令导出,格式为:

heapdump –live 文件路径和文件名

先使用jps或者ps -ef查看进程ID:

通过jmap命令导出内存快照文件,live代表只保存存活对象,format=b用二进制方式保存:

也可以在arthas中输出heapdump命令:

接下来下载到本地分析即可。

大文件的处理

在程序员开发用的机器内存范围之内的快照文件,直接使用MAT打开分析即可。但是经常会遇到服务器上的程序占用的内存达到10G以上,开发机无法正常打开此类内存快照,此时需要下载服务器操作系统对应的MAT。下载地址:https://eclipse.dev/mat/downloads.php 通过MAT中的脚本生成分析报告:

./ParseHeapDump.sh 快照文件路径 org.eclipse.mat.api:suspects org.eclipse.mat.api:overview org.eclipse.mat.api:top_components

注意:默认MAT分析时只使用了1G的堆内存,如果快照文件超过1G,需要修改MAT目录下的MemoryAnalyzer.ini配置文件调整最大堆内存。

最终会生成报告文件:

将这些文件下载到本地,解压之后打开index.html文件:

同样可以看到类似的报告:

7.在线定位解决内存泄漏/溢出

上面讲的都是使用内存快照来实现的,下面本文讲解实时检测内存情况的方法

7.1 Jmeter实时检测插件

jmeter下TPS插件的安装_jmeter-plugins-cmn-jmeter.jar-CSDN博客

插件下载好后重启就可以在监听器中看到三个选项,分别是活跃线程数、响应时间RT、每秒事务数TPS。

7.2 Arthas stack命令在线定位步骤

1、使用jmap -histo:live 进程ID > 文件名 命令将内存中存活对象以直方图的形式保存到文件中,这个过程会影响用户的时间,但是时间比较短暂。

2、分析内存占用最多的对象,一般这些对象就是造成内存泄 打开1.txt文件,从图中可以看到,有一个UserEntity对象占用非常多的内存。

这个就是泄漏的原因。

3、使用arthas的stack命令,追踪对象创建的方法被调用的调用路径,找到对象创建的根源。也可以使用btrace工具编写脚本追踪方法执行的过程。

接下来启动jmeter脚本,会发现有大量的方法调用这样不利于观察。

加上 -n 1 参数,限制只查看一笔调用:

这样就定位到了是login接口中创建的对象:

7.3 btrace在线定位问题步骤

相比较arthas的stack命令,btrace允许我们自己编写代码获取感兴趣的内容,灵活性更高。

BTrace 是一个在Java 平台上执行的追踪工具,可以有效地用于线上运行系统的方法追踪,具有侵入性小、对性能的影响微乎其微等特点。 项目中可以使用btrace工具,打印出方法被调用的栈信息。 使用方法: 1、下载btrace工具, 官方地址:Release v2.2.6 · btraceio/btrace · GitHub

2、编写btrace脚本,通常是一个java文件 依赖:

<dependencies>
        <dependency>
            <groupId>org.openjdk.btrace</groupId>
            <artifactId>btrace-agent</artifactId>
            <version>${btrace.version}</version>
            <scope>system</scope>
            <systemPath>D:\tools\btrace-v2.2.4-bin\libs\btrace-agent.jar</systemPath>
        </dependency>

        <dependency>
            <groupId>org.openjdk.btrace</groupId>
            <artifactId>btrace-boot</artifactId>
            <version>${btrace.version}</version>
            <scope>system</scope>
            <systemPath>D:\tools\btrace-v2.2.4-bin\libs\btrace-boot.jar</systemPath>
        </dependency>

        <dependency>
            <groupId>org.openjdk.btrace</groupId>
            <artifactId>btrace-client</artifactId>
            <version>${btrace.version}</version>
            <scope>system</scope>
            <systemPath>D:\tools\btrace-v2.2.4-bin\libs\btrace-client.jar</systemPath>
        </dependency>
    </dependencies>

代码非常简单,就是打印出栈信息。clazz指定类,method指定监控的方法。

import org.openjdk.btrace.core.annotations.*;

import static org.openjdk.btrace.core.BTraceUtils.jstack;
import static org.openjdk.btrace.core.BTraceUtils.println;

@BTrace
public class TracingUserEntity {
        @OnMethod(
            clazz="com.itheima.jvmoptimize.entity.UserEntity",
            method="/.*/")
        public static void traceExecute(){
                jstack();
        }
}

3、将btrace工具和脚本上传到服务器,在服务器上运行 btrace 进程ID 脚本文件名

配置btrace环境变量,与JDK配置方式基本相同:

在服务器上运行 btrace 进程ID 脚本文件名:

4、观察执行结果。 启动jmeter之后,同样获取到了栈信息:

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注