Java虚拟机(JVM)(9)—— 内存泄漏和内存溢出


一、内存溢出和内存泄漏的区别

  • 内存泄漏:(Memory Leak)在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上,这个对象就不会被垃圾回收器回收,如此反复,导致系统内存的浪费,严重时会导致内存溢出。

  • 内存溢出:(out of memory)指的是JVM(Java虚拟机)在尝试为对象分配内存时,堆内存不足以满足分配请求,从而抛出OutOfMemoryError异常。

    内存泄露不会抛出异常,大多数时候程序看起来是正常运行的,只有导致内存溢出会抛出异常。
    

二、内存溢出的类型

栈内存溢出(StackOverflowError):程序的递归调用太深,或方法内的局部变量太多,导致栈空间耗尽。

堆内存溢出(OutOfMemoryError: Java heap space):程序创建的对象过多,存活时间过长,导致堆空间耗尽。

持久代/元空间内存溢出(OutOfMemoryError: PermGen space / Metaspace):在Java 8之前的永久代(PermGen)或之后的元空间(Metaspace)中,类元数据或常量池信息占用超过配置的大小。

关于JVM运行时数据区可以参考博文:Java虚拟机(JVM)(4)——运行时数据区


三、内存溢出产生的原因

在生产过程中,内存溢出无非两种情况:一种是瞬时峰值导致内存溢出,这种只能通过加内存的方式,一种是持续增长导致的内存溢出,其最主要的原因来自于代码上的缺陷导致内存泄漏,最终导致内存溢出。以下为java代码中经常出现的内存泄漏案例:

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

1、equals()和hashCode()

在正常使用HashMap的场景下,如果用对象作为key,HashMap在判断key是否已经存在时,首先调用hash方法计算key的哈希值,hash方法中会使用到key的hashcode方法。根据hash方法的结果决定存放的数组中位置。如果没有元素,直接放入。如果有元素,先判断key是否相等,会用到equals方法,如果key相等,直接替换value;key不相等,走链表或者红黑树查找逻辑,其中也会使用equals比对是否相同。

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

问题2: equals方法实现不正确,会导致key在比对时,即便对象hash值是相同的,也被认为是不同的key。

长时间运行之后HashMap中会保存大量相同id的数据。最终导致OutOfMemoryError

public class Student {
    private String name;
    private Integer id;
    private byte[] bytes = new byte[1024 * 1024];

    public String getName() {
        return name;
    }

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

    public Integer getId() {
        return id;
    }

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

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){
            Student student = new Student();
            student.setId(1);
            student.setName("张三");
            map.put(student,1L);
            Thread.sleep(100);
        }
    }
}

运行之后通过visualvm观察:

在这里插入图片描述
解决方案:

  1. 在定义新实体时,始终重写equals()和hashCode()方法。
  2. 重写时一定要确定使用了唯一标识去区分不同的对象,比如用户的id等。
  3. hashmap使用时尽量使用编号id等数据作为key,不要将整个实体类对象作为key存放。
public class Student {
    private String name;
    private Integer id;
    private byte[] bytes = new byte[1024 * 1024];

    public String getName() {
        return name;
    }

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

    public Integer getId() {
        return id;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Student)) return false;
        Student student = (Student) o;
        return Objects.equals(id, student.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

2、内部类引用外部类

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

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

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 {
        int count = 0;
        ArrayList<Inner> inners = new ArrayList<>();
        while (true){
            inners.add(new Outer().new Inner());
            Thread.sleep(100);
        }
    }
}
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 {
        int count = 0;
        ArrayList<Inner> inners = new ArrayList<>();
        while (true){
            inners.add(new Inner());
            Thread.sleep(100);
        }
    }
}

运行之后通过visualvm观察:

在这里插入图片描述
最终导致:

在这里插入图片描述
解决方案:

  1. 这个案例中,使用内部类的原因是可以直接获取到外部类中的成员变量值,简化开发。如果不想持有外部类对象,应该使用静态内部类。
  2. 使用静态方法,可以避免匿名内部类持有调用者对象。
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 {
        int count = 0;
        ArrayList<Inner> inners = new ArrayList<>();
        while (true){
            inners.add(new Inner());
            Thread.sleep(100);
        }
    }
}
public class Outer {
    private byte[] bytes = new byte[1024 * 1024]; //外部类持有数据
    private static String name  = "测试";
    public static List<String> newList() {
        List<String> list = new ArrayList<String>() {{
            add("1");
            add("2");
        }};
        return list;
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        int count = 0;
        ArrayList<Object> inners = new ArrayList<>();
        while (true){
            inners.add(newList());
            System.out.println(count++);
            Thread.sleep(100);
        }
    }
}

运行之后通过visualvm观察:

在这里插入图片描述

3、ThreadLocal的使用

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

public class Demo3 {
    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) {
            threadPoolExecutor.execute(() -> {
                threadLocal.set(new byte[1024 * 1024]);
            });
            System.out.println(count++);
        }
    }
}

运行之后通过visualvm观察:

在这里插入图片描述
最终导致:

在这里插入图片描述
解决方案:

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

在这里插入图片描述

4、String的intern方法

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

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

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

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

解决方案:

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

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

@Lazy //懒加载
@Component
public class TestLazy {
    private byte[] bytes = new byte[1024 * 1024 * 1024];
}
  1. Spring的Bean中不要长期存放大对象,如果是缓存用于提升性能,尽量设置过期时间定期失效。
public class CaffineDemo {
    public static void main(String[] args) throws InterruptedException {
        Cache<Object, Object> build = Caffeine.newBuilder()
        //设置100ms之后就过期
                 .expireAfterWrite(Duration.ofMillis(100))
                .build();
        int count = 0;
        while (true){
            build.put(count++,new byte[1024 * 1024 * 10]);
            Thread.sleep(100L);
        }
    }
}

6、资源没有正常关闭

1、为了防止出现这类的资源对象泄漏问题,必须在finally块中关闭不再使用的资源。
2、从 Java 7 开始,使用try-with-resources语法可以用于自动关闭资源。

在这里插入图片描述


四、应对措施

1、定位问题

  • 利用Java虚拟机提供的监控工具如JVisualVMJConsole等,或其他专业的内存分析工具,来监控应用程序的内存使用情况,判断是持续增长(可能为内存泄漏)还是瞬间峰值(可能是分配过大),找出内存溢出的源头。
  • 对于内存泄漏,通过分析垃圾收集器根对象(GC Roots)如何引用泄漏对象,定位泄漏源。

2、解决措施

增加内存:

调整JVM参数,如增加堆(-Xms-Xmx)、栈(-Xss)或元空间(-XX:MaxMetaspaceSize)的大小,但根本解决应优化代码,减少内存使用。

代码优化:

  • 检查代码中是否存在死循环或递归调用没有结束条件的情况,这些情况会导致栈内存耗尽,从而发生栈溢出。
  • 检查对数据库查询中,是否有一次获得全部数据的查询。对于大数据量的查询,尽量采用分页查询的方式,避免一次性加载过多数据到内存中。
  • 检查代码中是否有大循环重复产生新对象实体的情况,这种情况会导致堆内存溢出。
  • 尽量减少将对象长时间的保存在静态变量中,如果不再使用,必须将对象删除(比如在集合中)或者将静态变量设置为null
  • 在使用资源对象(如文件、数据库连接等)时,应使用try-finallytry-with-resources语句来确保在不再需要资源时能够正确地关闭。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值