内存泄漏产生的原因

1.代码中的内存泄漏

equals和hashCode

不正确的equals和hashCode实现导致内存泄漏

java 复制代码
package org.xyg;

public class Student {
    int id;
    String name;

    public void setId(int id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }
}
java 复制代码
package org.xyg;

import java.util.HashMap;
import java.util.Map;

public class demo1 {
    public static long count = 0;
    public static Map<Student,Long> map = new HashMap<>();

    public static void main(String[] args) throws InterruptedException {
        while(true){
            if(count++ % 100 ==0){
                Thread.sleep(10);
            }
            Student student = new Student();
            student.setId(1);
            student.setName("张三");
            //我们的本意是想用后面插入的对象覆盖前面的对象
            //由于Student类没有重写equals和hashCode方法,
            //在插入student对象时,由于每个对象的地址都不一样
            //所以这些对象都保留在了map中,GC无法回收,导致内存泄漏
            map.put(student,1L);
        }
    }
}

解决方案:

1.定义新实体类时,始终重写equals和hashCode方法

2.重写时一定要确定使用了唯一标识去区分不同的对象,比如用户id等

3.hashmap使用时尽量使用编号id等数据作为key,不要将整个实体类对象作为key存放

java 复制代码
package org.xyg;

public class Student {
    int id;
    String name;

    public void setId(int id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if(this == o){
            return true;
        }

        if(o == null || getClass() != o.getClass()) {
            return false;
        }

        Student student = (Student) o;

        return new EqualsBuilder().append(id,student.id).isEquals();
    }
    
    @Override
    public int hashCode() {
        return new HashCodeBuilder(17,37).append(id).toHashCode();
    }
}

内部类引用外部类

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

java 复制代码
package org.xyg;

import java.util.ArrayList;

public class Outer {
    private byte[] bytes = new byte[1024];
    private String name = "测试";
    class Inner{
        private String name;
        public Inner(){
            this.name = Outer.this.name;
        }
    }

    public static void main(String[] args) {
        int count = 0;
        ArrayList<Inner> inners = new ArrayList<>();
        while(true){
            System.out.println(++count);
            //我们的想法是想添加一个inner,但是inner使用了outer中的name属性,
            //所以inner内部包含了一个outer,导致GC无法回收outer,造成了内存泄漏
            inners.add(new Outer().new Inner());
        }
    }
}

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

java 复制代码
package com.itheima.jvmooptimize.leakdemo.demo4;

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

public class Outer {
    private byte[] bytes = new byte[1024 * 1024 * 1024];

    public List<String> newList() {
//1. `new ArrayList<String>() {{}}` 是**匿名内部类**,匿名内部类会隐式持有外部类`Outer`实例的引用。
//2. 每次`new Outer()`创建外部对象,它包含 1GB 大字节数组`bytes`;返回的 List 持有 Outer 对象引用。
//3. `objects`全局集合持续存放所有 List,List 一路强引用 Outer,所有 Outer 对象无法被 GC 回收,内存不断暴涨,最终 OOM。
        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(new Outer().newList());
        }
    }
}

ThreadLocal的使用

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

解决方法:

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

不会出现内存泄漏;

java 复制代码
package com.itheima.jvmooptimize.leakdemo.demo5;

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

出现线程泄漏:

  1. 全局静态threadLocal强引用常驻内存;
  2. 个线程自线程池线程长期存活,每带ThreadLocalMap
  3. 每次任务执行set存入 1MB 字节数组,没有调用 remove
  4. 线程被重复使用:上次任务存入的数组还留在线程的 ThreadLocalMap 中,不会自动清空;
  5. 不断提交任务,大量线程不断存入新数组,旧数据无法释放,内存只涨不跌,最终堆溢出。
java 复制代码
package com.itheima.jvmooptimize.leakdemo.demo5;

import java.util.concurrent.*;

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

    public static void main(String[] args) {
//线程池**线程复用**,线程不会执行完就销毁,会长期存活反复被拿来执行任务。
//本线程池配置:核心线程、最大线程数无限,空闲存活时间永久,SynchronousQueue 无缓冲队列,每来任务必新建 / 复用线程。
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(Integer.MAX_VALUE, Integer.MAX_VALUE,
                0L, TimeUnit.DAYS, new SynchronousQueue<>());
        int count = 0;
        while (true) {
            System.out.println(++count);
            threadPoolExecutor.execute(() -> {
                threadLocal.set(new byte[1024 * 1024]);
                //threadLocal.remove();
            });
        }
    }
}

String的intern()方法

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

java 复制代码
import java.util.ArrayList;
import java.util.List;

public class Demo6_2 {
    public static void main(String[] args) {
        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.尽量减少将对象长时间的保存在静态变量中,如果不再使用,必须将对象删除或者将静态变量设置为null.

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

java 复制代码
package com.itheima.jvmooptimize.leakdemo.demo7;

import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

@Lazy //懒加载
@Component
public class TestLazy {
    private byte[] bytes = new byte[1024 * 1024 * 1024];
}

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

java 复制代码
package com.itheima.jvmooptimize.leakdemo.demo7;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;

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

资源没有正常关闭

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

解决方法:

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

2.从java7开始,使用try-with-resources语法可以用于自动关闭资源

2.并发请求问题

并发请求问题指的是用户通过发送请求向 Java 应用获取数据,正常情况下 Java 应用将数据返回之后,这部分数据就可以在内存中被释放掉。但是由于用户的并发请求量有可能很大 ,同时处理数据的时间 很长,导致大量的数据存在于内存中 ,最终超过了内存的上限,导致内存溢出。这类问题的处理思路和内存泄漏类似,首先要定位到对象产生的根源

相关推荐
码智社2 小时前
Java实现RSA密钥生成、加密解密、加签验签
java
xixingzhe22 小时前
spring ai简单使用skills
数据库·人工智能·spring
wear工程师2 小时前
ThreadLocal 在线程池里为什么会串数据:别只会答内存泄漏
java·后端
Iruoyaoxh2 小时前
栈和队列~
java·开发语言
审小匠OpenCPAi3 小时前
银行流水核查怎么自动化?单边匹配、双向勾稽与图聚类异常检测的工程对比
java·前端·人工智能·python·审计
码上上班3 小时前
docker课程
java·docker·容器
腻害兔3 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:CRM 客户关系模块深度解析——从线索到回款,一套完整的 B2B 销售闭环是怎么搭的?
java·前端·javascript·vue.js·产品经理·ai编程
达子6663 小时前
第9章_HarmonyOs图解 用Java开发UI
java·ui·harmonyos
龙虾PRO5 小时前
能源与股票量化核心差异,AI全链路落地实操手册
java·面试·职场和发展