【实战JVM】-实战篇-05-内存泄漏及分析


1 内存溢出和内存泄漏

在这里插入图片描述

在这里插入图片描述

1.1 常见场景

  • 内存泄露导致溢出的常见场景是大型的Java后端应用中,在处理用户请求之后,没有及时将用户数据删除。随着用户请求的数量越来越多,内存泄漏的对象占满了堆内存,最终导致内存溢出。
  • 在这里插入图片描述

1.2 解决内存溢出的方法

在这里插入图片描述

1.2.1 发现问题

1.2.1.1 top

在这里插入图片描述

top

按M选择内存从大到小排序

1.2.1.2 ViusalVM

在这里插入图片描述

启动微服务com.itheima.jvmoptimize.JvmOptimizeApplication

在这里插入图片描述

远程连接查看visualvm

java -jar -Djava.rmi.server.hostname=182.92.117.86 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9122 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false SpringBoot-demo-1.0-SNAPSHOT.jar

访问不成功的话记得添加阿里云的安全组

在这里插入图片描述

生产环境禁止通过visualVM连接!只能在测试环境中查看。

1.2.1.3 arthas

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

启动arthas tutorial

nohup java -jar -Darthas.enable-detail-pages=true arthas-tunnel-server-3.7.1-fatjar.jar &

默认端口号8080,默认访问地址

http://182.92.117.86:8080/apps.html

微服务引入依赖并打包

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

先启动一个

nohup java -jar -Dserver.port=9527 -Darthas.http-port=9528 -Darthas.telnet-port=9529 jvm-optimize-0.0.1-SNAPSHOT.jar &

再启动一个

nohup java -jar -Dserver.port=9530 -Darthas.http-port=9531 -Darthas.telnet-port=9532 jvm-optimize-0.0.1-SNAPSHOT.jar &

在这里插入图片描述

一个tunnel,两个微服务

查看http://182.92.117.86:8080/apps.html

在这里插入图片描述

一点名字就进入arthas的页面

在这里插入图片描述

1.2.1.4 Prometheus+Grafana

在这里插入图片描述

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>

    <exclusions><!-- 去掉springboot默认配置 -->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

暴露所有端口

management:
  endpoint:
    metrics:
      enabled: true #支持metrics
    prometheus:
      enabled: true #支持Prometheus
  metrics:
    export:
      prometheus:
        enabled: true
    tags:
      application: jvm-test #实例名采集
  endpoints:
    web:
      exposure:
        include: '*' #开放所有端口

查询localhost:8881/actuator

在这里插入图片描述

查看所有bean属性

在这里插入图片描述

现在是通过监控springboot中的属性,也可以将jvm中其他的指标暴露出来。

引入依赖

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    <scope>runtime</scope>
</dependency>
management:
  endpoint:
    metrics:
      enabled: true #支持metrics
    prometheus:
      enabled: true #支持Prometheus
  metrics:
    export:
      prometheus:
        enabled: true
    tags:
      application: jvm-test #实例名采集

打开http://localhost:8881/actuator/prometheus

在这里插入图片描述

查看堆信息

在这里插入图片描述

为我们的主机在阿里云中配置Prometheus监控

访问http://182.92.117.86:9527/actuator/prometheus

在这里插入图片描述

添加microMeter,监控jvm

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Micrometer大盘监控成功:

在这里插入图片描述

堆内存使用情况

在这里插入图片描述

堆中具体使用情况

在这里插入图片描述

Prometheus负责将服务器上的信息通过接口的方式收集到,并且把信息传输给Grafana,最后通过Grafana的仪表盘可视化出来

1.2.2 堆内存状况对比

在这里插入图片描述

1.2.3 内存泄漏原因-代码中

在这里插入图片描述

1.2.3.1 equals()-hashCode()

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

解决方法:

  • 在定义新实体类时始终重写equals()和hashCode()方法。
  • 重写时一定要确定使用了唯一的标识去区分不同的对象,比如用户的ID等。
  • hashmap使用时尽量使用编号ID等数据作为key,不要将整个实体类对象作为key存放。
  • 采用@Data来注释实体类避免出现此类情况
1.2.3.2 内部类引用外部类

在这里插入图片描述

问题一:

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

public class Outer{
    private byte[] bytes = new byte[1024 * 1024]; //外部类持有数据
    private String name  = "测试";
    class Inner{
        private String name;
        public Inner() {
            this.name = Outer.this.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());
        }
    }
}

如果要用就要改为静态的内部类,并且引用外部类的静态属性

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 class Outer {
    private byte[] bytes = new byte[1024];
    public List<String> newList() {
        List<String> list = new ArrayList<String>() {{
            add("1");
            add("2");
        }};
        return list;
    }

    public static void main(String[] args) throws IOException {
        System.in.read();
        int count = 0;
        ArrayList<Object> objects = new ArrayList<>();
        while (true){
            System.out.println(++count);
            objects.add(newList());
        }
    }
}

使用内部类或匿名类,尽量改为静态类或静态方法

public class Outer {
    private byte[] bytes = new byte[1024];
    public static List<String> newList() {
        List<String> list = new ArrayList<String>() {{
            add("1");
            add("2");
        }};
        return list;
    }
1.2.3.3 ThreadLocal的使用

在这里插入图片描述

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

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            new Thread(() -> {
                threadLocal.set(new byte[1024 * 1024 * 10]);
            }).start();
            Thread.sleep(10);
        }
    }
}

单纯使用 new Thread(() -> {创建线程即使不会回收也不会内存泄漏,但是使用线程池就不一样了。

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);
        }
    }
}

使用完线程池的ThreadLocal之后记得remove移除

1.2.3.4 String的intern方法

在这里插入图片描述

1.2.3.5 通过静态字段保存对象

在这里插入图片描述

public class CaffineDemo {
    public static void main(String[] args) throws InterruptedException {
        Cache<Object, Object> build = Caffeine.newBuilder()
                .build();
        int count = 0;
        while (true){
            build.put(count++,new byte[1024 * 1024 * 10]);
            Thread.sleep(100L);
        }
    }
}

为缓存设置一个时间 .expireAfterWrite(Duration.ofMillis(100))

1.2.3.6 资源没有正常关闭

在这里插入图片描述

1.2.4 内存泄漏原因-并发请求

在这里插入图片描述

在这里插入图片描述

1.2.4.1 内存溢出

在Jmeter中添加线程组,每秒触发100次http请求

在这里插入图片描述

增加http请求

在这里插入图片描述

@GetMapping("/test")
public void test1() throws InterruptedException {
    byte[] bytes = new byte[1024 * 1024 * 100];//100m
    Thread.sleep(10 * 1000L);
}

在这里插入图片描述

修改启动参数

在这里插入图片描述

启动后大量报错

在这里插入图片描述

1.2.4.2 内存泄漏

在这里插入图片描述

在这里插入图片描述

粘贴到id的值那一行。

name同理,选择随机字符串RandomString,长度1000,哪些字符用于生成:26个字母

在这里插入图片描述

启动测试,每秒能处理8000个

在这里插入图片描述

2 诊断内存

2.1 使用MAT诊断内存

先把环境变量的jdk设为17再启动memoryanalyzer

在这里插入图片描述

在这里插入图片描述

添加虚拟机参数

-Xmx256m -Xms256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\File\StudyJavaFile\JavaStudy\JVM\shizhan\day05\resource\dump\test1.hprof

然后启动时用jmeter测试,控制台输出

在这里插入图片描述

用mat打开这个内存快照

在这里插入图片描述

在这里插入图片描述

点击detail查看详情

在这里插入图片描述

在这里插入图片描述

综合报告中同样能看到此类信息

在这里插入图片描述

2.2 MAT检测内存泄漏原理

在这里插入图片描述

在这里插入图片描述

2.2.1 生成内存快照观察深堆浅堆

在这里插入图片描述

public class HeapDemo {
    public static void main(String[] args) {
        TestClass a1 = new TestClass();
        TestClass a2 = new TestClass();
        TestClass a3 = new TestClass();
        String s1 = "itheima1";
        String s2 = "itheima2";
        String s3 = "itheima3";

        a1.list.add(s1);

        a2.list.add(s1);
        a2.list.add(s2);

        a3.list.add(s3);

        //System.out.print(ClassLayout.parseClass(TestClass.class).toPrintable());
        s1 = null;
        s2 = null;
        s3 = null;
        System.gc();
    }
}

class TestClass {
    public List<String> list = new ArrayList<>(10);
}

在这里插入图片描述

转换为支配树

在这里插入图片描述

添加虚拟机参数

-XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=D:/File/StudyJavaFile/JavaStudy/JVM/shizhan/day05/resource/dump/mattest.hprof

启动,控制台输出

Dumping heap to D:/File/StudyJavaFile/JavaStudy/JVM/shizhan/day05/resource/dump/mattest.hprof ...
Heap dump file created [2507919 bytes in 0.010 secs]

用mat打开,并且打开支配树

在这里插入图片描述

打印类的信息

引入

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
public class StringSize {
    public static void main(String[] args) {
        //使用JOL打印String对象
        System.out.print(ClassLayout.parseClass(String.class).toPrintable());
    }
}

输出

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               VALUE
      0    12          (object header)                           N/A
     12     4   char[] String.value                              N/A
     16     4      int String.hash                               N/A
     20     4          (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

0-11 是类头,12-15是char[] String.value,16-19是int String.hash,因为用的是64位系统,所以需要和8字节对齐,所以20-23是补齐8字节。

在这里插入图片描述

在这里插入图片描述

2.3 导出运行中内存的快照并分析

2.3.1 jmap导出

在这里插入图片描述

服务器中有3个,一个是arthas,两个是jvm-optimize

在这里插入图片描述

运行jmap命令输出9527的内存快照

jmap -dump:live,format=b,file=/home/jvm/dump/jvm-optimize-jmap.hprof 29317

在这里插入图片描述

2.3.2 arthas导出

访问182.92.117.86:8080/apps.html

随便进入一个

在这里插入图片描述

输入

heapdump --live /home/jvm/dump/jvm-optimize-arthas.hprof

在这里插入图片描述

在这里插入图片描述

得到俩,下载到本地后用mat打开

在这里插入图片描述

再看柱状图

在这里插入图片描述

要是一个对象的深堆过于的大的时候就可能发生内存泄漏。现在都在一个数量级。还算正常

2.4 分析超大堆内存快照

在这里插入图片描述

3 实战

在这里插入图片描述

3.1 分页查询文章接口的内存溢出

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.1.1 设置参数重启jar包

重新添加参数启动9527

nohup java -jar -Dserver.port=9527 -Darthas.http-port=9528 -Darthas.telnet-port=9529 -Xmx512m -Xms512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/jvm/dump/jvm9527.hprof jvm-optimize-0.0.1-SNAPSHOT.jar & 

打开Prometheus查看运行情况,现在只有100M还算ok

在这里插入图片描述

3.1.2 准备数据

在docker的mysql中生成了11w数据,方便后面查询

在这里插入图片描述

在这里插入图片描述

3.1.3 jmeter中导入测试脚本

在这里插入图片描述

在这里插入图片描述

这样等于150个线程×10000条数据保存到内存中

先查询一个

在这里插入图片描述

没什么问题,启动jmeter测试脚本

在这里插入图片描述

运行了五分钟都没溢出。

在这里插入图片描述

3.1.4 定位问题

直接看老师的视频吧,用mat分析内存结构,打开直方图和支配树,占用大的一个是rowData,一个是tomcat的线程。

先从线程入手,选择线程

在这里插入图片描述

在这里插入图片描述

发现是DemoQueryController在执行queryByPage这个方法

@GetMapping
public ResponseEntity<Page<TbArticle>> queryByPage(TbArticle tbArticle, int page,int size) {
	//size = Math.min(100,size);
    return ResponseEntity.ok(this.articleService.queryByPage(tbArticle, PageRequest.of(page,size)));
}

很多TbArticle对象只是被创建了,但是还没有返回,大量存在于内存中,在开发环境重新复现这个问题。

3.1.5 解决思路

在这里插入图片描述

选用一进行解决。

size = Math.min(100,size);

限制查询个数

3.2 Mybatis导致的内存溢出

在这里插入图片描述

3.2.1 定位问题

用3.1一样的分析方法,HandlerMethod->List->with outgoing references,定位到这里

在这里插入图片描述

@GetMapping
public ResponseEntity countIfAbsent(int size) {
    //随机生成批量id
    List<Integer> ids = new Random().ints(0, 1000000).
            limit(size).boxed().collect(Collectors.toList());

    return ResponseEntity.ok(this.articleService.countIfAbsent(ids));
}

会生成一个供mybatis去foreach的hashmap数组,占据大量空间

3.2.2 解决思路

在这里插入图片描述

3.3 导出大文件内存溢出

在这里插入图片描述

3.3.1 k8s部署

k8s搞不定,光听算了

3.3.2 解决思路

在这里插入图片描述

3.4 ThreadLocal使用时占用大量内存

在这里插入图片描述

3.4.1 定位问题

在这里插入图片描述

每次拦截用户信息都存在threadlocal中

public class UserInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserDataContextHolder.userData.set(new UserDataContextHolder.UserData());
         return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserDataContextHolder.userData.remove();
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

但是如果出现异常,postHandle中remove方法无法执行,用户信息就永远留在threadlocal中,所以需要吧remove移到afterCompletion中。

并且和tomcat的配置也有关系,最低保留100个线程进行处理,即使没有请求也有很多线程并不能被回收。

server:
  port: 8881
  tomcat:
    threads:
      min-spare: 100
      max: 500

修改为min-spare: 10,最小值为10,不能为0。

3.4.2 解决思路

在这里插入图片描述

3.5 文章内容审核接口的内存问题

在这里插入图片描述

3.5.1 定位问题

3.5.1.1 线程池

如果使用线程池处理异步请求

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

3.5.1.2 生产者-消费者使用队列

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

3.5.1.3 消息中间件

在这里插入图片描述

在这里插入图片描述

4 诊断和解决问题

在这里插入图片描述

4.1 离线分析

为jmeter添加插件
在这里插入图片描述

添加插件后,查看响应时间

在这里插入图片描述

大概30s就能响应

在这里插入图片描述

通过

jmap -dump:live,format=b,file=/home/jvm/dump/offline-jamp.hprof 28865

会导致较高的响应时间

4.2 在线分析-arthas

在这里插入图片描述

jmap -histo:live 28865 > /home/jvm/dump/online-jamp-histo.txt

在这里插入图片描述

监控一下UserEntity,在arthas中输入

stack com.itheima.jvmoptimize.entity.UserEntity -n 1

4.3 在线分析-btrace追踪

在这里插入图片描述

4.3.1 添加依赖

<dependencies>
    <dependency>
        <groupId>org.openjdk.btrace</groupId>
        <artifactId>btrace-agent</artifactId>
        <version>${btrace.version}</version>
        <scope>system</scope>
        <systemPath>D:\Software\software_with_code\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:\Software\software_with_code\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:\Software\software_with_code\btrace-v2.2.4-bin\libs\btrace-client.jar</systemPath>
    </dependency>
</dependencies>

4.3.2 编写btrace脚本

@BTrace
public class TracingUserEntity {
        @OnMethod(
            clazz="com.itheima.jvmoptimize.entity.UserEntity",
            method="/.*/")
        public static void traceExecute(){
                jstack();
        }
}
  • method="/.*/"/表示开始和结束,.*表示监控clazz的所有方法
  • jstack();当调用当前类时,会打印当前所有栈信息

4.3.3 上传btrace工具及其脚本

上传btrace工具及其脚本,并且把他的bin目录放到环境变量中。

在这里插入图片描述

btrace 3785 TracingUserEntity.java 

在这里插入图片描述

已经挂载到当前进程中,我们用jmeter发送请求。

在这里插入图片描述

但是加了btrace后,响应时间不是一般的长,平均都在300ms以上

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bblb

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值