【开发篇】二、代码中导致内存泄漏的错误写法

发布时间:2024年01月12日


内存泄漏 --> 压测或者时间积累 --> OOM

1、equals和hashCode方法

以下代码中,将实体对象做为map的key:

public class HeapDemo1 {

    public static void main(String[] args) throws Exception{
        int count = 0;
        HashMap<Students, String> map = new HashMap<>();
        while (true) {
            //被100整除时休眠10ms,是让一下CPU时间片给Visual VM,不然一直while true,Visual分不到CPU时间片,会监控不到,处于一种假死状态
            if(count++ % 100 == 0){
                Thread.sleep(10);
            }
            Students students = new Students();
            //学生的ID相同,按理说是同一个对象
            students.setId(1L);
            students.setName("甲");
            map.put(students,"test");

        }
    }
}

而这个实体类没有正确重写equals+hashcode:

@Setter
@Getter
public class Students {

    private Long id;

    private String name;

    private byte[] info = new byte[1024 * 1024];

}

运行,OOM,Visual VM观察堆内存走向:

在这里插入图片描述

原因分析:

哈希表的数据结构是数组+单向链表的组合体,上面map.put,底层则是先调用key的hashcode找位置,然后再调key的equals看是否覆盖这个位置已有的key。因此,对上面这个以实体类为key的写法:

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

在这里插入图片描述

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

在这里插入图片描述

因此,最后就会有大量对象id相同的学生对象(同一个对象)被挂在hashmap里,最终OOM。

在这里插入图片描述

修改实体类,重写equals和hashcode:

@Setter
@Getter
public class Students {

    private Long id;

    private String name;

    private byte[] info = new byte[1024 * 1024];  //byte属性让对象大一点,好测试

    /**
     * 只比id属性即可,ID相同,则同一个对象
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Students students = (Students) o;
        return Objects.equals(id, students.id);
    }

    /**
     * id属性
     */
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

此时,Visual VM观察堆内存走向:

在这里插入图片描述

注意避坑:

  • 实体类注意重写equals+hashcode(@Data包括get、set、toString、equals、hashcode)
  • 重写时一定要确定使用了唯一标识去区分不同的对象,比如上面用学生的id等
  • key的选择尽量选择ID等字段,不要把整个对象当key

2、内部类引用外部类

public class Outer {

    private byte[] bytes = new byte[1024 * 100];

    private String name = "test";

    class Inner {
        private String name;

        //创建内部类对象时,获取外部类的name属性,赋值给内部类的name属性
        public Inner() {
            this.name = Outer.this.name;
        }
    }

    public static void main(String[] args) throws Exception{
        int count = 0;
        ArrayList<Inner> innerArrayList = new ArrayList<>();
        while (true) {
            //同上,给Visual VM一点CPU时间片
            if (count++ % 100 == 0) {
                Thread.sleep(10);
            }
            innerArrayList.add(new Outer().new Inner());
        }
    }
}

Visual查看,Inner对象放集合了,可达,没被回收是合理的,但Outer对象不用了,属于泄漏:

原因分析:

非静态的内部类(static),默认持有外部类,创建完的内部类对象会关联到外部类,导致外部类对象即使代码中不用了,也不可被回收。Dubug验证:

在这里插入图片描述

解决办法:改为静态内部类

在这里插入图片描述

3、匿名内部类

public class Outer {

    private byte[] bytes = new byte[1024 * 1024];

    public List<String> newList(){
        List<String> list = new ArrayList<String>(){{  //匿名内部类在非静态方法中被创建
            add("1");
            add("2");
        }};
        return list;
    }




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

运行,OOM

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

Outer对象不能被回收:
在这里插入图片描述

解决办法:使用静态方法,可以避免匿名内部类持有调用者对象

在这里插入图片描述

4、ThreadLocal不remove

如果是new的线程,不remove也行,因为当线程被回收时,ThreadLocal也同样被回收。

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

    public static void main(String[] args) throws Exception{
        while (true) {
            new Thread(() -> {
                threadLocal.set(new byte[1024 * 1024 * 100]);
            }).start();
            Thread.sleep(10);  //歇会儿,别加太快,以至于GC都C不及
        }
    }
}

换成线程池,线程被复用,不remove则内存泄漏:

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

    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(Integer.MAX_VALUE, Integer.MAX_VALUE, 0, TimeUnit.DAYS, new SynchronousQueue<>());
        int count = 1;
        while (true) {
            System.out.println(count++);
            pool.execute(() -> {
                threadLocal.set(new byte[1024 * 1024 * 100]);
                //remove后解决了内存泄漏
                //threadLocal.remove();
            });
            Thread.sleep(10);
        }
    }
}

在这里插入图片描述

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

5、String的intern方法

大量把字符串手动加到字符串常量池,导致OOM。JDK6下,字符串常量池在永久代,调小永久代,以便快速看到效果,JDK8,字符串常量池在堆里,同理可调小堆:

-XX:MaxPermSize=15M
-Xmx10M -Xms10M

Demo:

public class HeapLeak {
    public static void main(String[] args) {
        int i = 0;
        ArrayList<String> list = new ArrayList<>();
        while (true) {
            //String.valueOf(i++).intern();
            list.add(String.valueOf(i++).intern());
        }
    }
}

建议:不要将随机生成的字符串加入字符串常量

6、通过静态字段保存对象

静态变量的生命周期长(类的生命周期),因此将对象保存在静态变量中,长时间不被释放,后面又不再用的话,白占地方,内存泄漏。

在这里插入图片描述

优化点:

  • 对象存集合中,不用了就remove
  • 及时将静态变量设置为null,别白占地方

同类型的注意点,还有:

  • 使用单例模式时,尽量使用懒加载,而不是立即加载
  • Spring的Bean中不要长期存放大对象,如果是缓存用于提升性能,尽量设置过期时间定期失效

如下,设置-Xmx500m,一个Bean,懒加载时微服务启动不受影响

@Lazy
@Component
public class TestLazy{

	private byte[] bytes = new byte[1024 * 1024 * 1024];
}
ConfigurableApplicationContext run = SpringApplication.run(SystemApplication.class,args);
//如果程序运行过程中,这个类一直没被用到,那这个对象就不会被创建
//这里获取,就OOM
TestLaxy bean = run.geteBean(TestLazy.class);

给大对象设置过期时间:
在这里插入图片描述

7、资源没有被正常关闭

tay-catch-finally,没有关闭资源,不一定就内存泄漏,但:

  • finally关闭
  • Java 7 开始,使用try-with-resources语法可以用于自动关闭资源
try (Connection conn = DriverManager.getConnection(DB_URLUSER, PASS)){
	//....
}
文章来源:https://blog.csdn.net/llg___/article/details/135471880
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。