首页 > 系统相关 >内存泄漏 与 内存溢出

内存泄漏 与 内存溢出

时间:2024-07-31 16:59:51浏览次数:21  
标签:泄漏 void OuterClass 线程 内存 new public 溢出

1.内存溢出(Memory Overflow)

  • 生活样例:
            
    内存容量就像一个桶,内存就是水,水 溢出 就是水满了。
  • 定义:
            
    内存溢出是指程序试图使用超过其可用内存限制的内存。这种情况通常会导致程序崩溃或异常。内存溢出一般是由于分配了过多内存或者在使用数据结构时超出了其限制。
  • 例子:堆内存溢出
            堆内存用于动态分配对象。当程序尝试分配超过堆内存限制的内存时,就会发生堆内存溢出。
public class HeapMemoryoverflowExample {
    public static void main(string[] args) {
        List<int[]> list = new ArrayList<>();
        while (true) {
            list.add(new int[10000001);//不断分配大块内存
        }
    }
}

常见内存溢出情况及解决方案:

  • 堆内存溢出(Java Heap Space)

    • 原因:长时间运行的应用可能会持续创建对象,如果这些对象没有被及时回收,就可能导致堆内存耗尽。
    • 解决:增加JVM堆内存大小(通过-Xms-Xmx参数设置);优化代码以减少内存使用,比如使用对象池来减少对象创建;分析内存泄漏并修复。
  • 栈溢出(StackOverflowError)

    • 原因:通常是由于递归调用太深或循环创建了大量局部变量。
    • 解决:优化递归逻辑,确保有正确的终止条件;减少方法调用深度;优化循环逻辑,避免创建大量局部变量。
  • 元空间溢出(Metaspace)

    • 原因:Java 8 以后的版本使用元空间代替了永久代,用于存储类的元数据。如果类的元数据消耗过多内存,可能会触发元空间溢出。
    • 解决:增加元空间大小(通过-XX:MetaspaceSize-XX:MaxMetaspaceSize参数设置);优化代码以减少类加载。
  • 大对象处理不当

    • 原因:处理大型对象或集合时,可能会占用大量内存。
    • 解决:优化大对象的处理逻辑,比如分批处理、使用流式处理等。
  • 线程资源管理不当

    • 原因:线程创建过多,每个线程都有自己的栈空间,可能导致内存溢出。
    • 解决:合理管理线程资源,避免创建过多线程;使用线程池来复用线程。

2.内存泄露(Memory Leak)

  • 生活样例:
    桶破了,水漏出去了。桶中的水就相当于内存,慢慢的流失了
  • 定义:
            内存泄露是指程序在运行过程中动态分配内存后,没有正确地释放不再使用的内存,导致这些内存无法被再次分配和使用。长时间运行的程序如果存在内存泄露,会导致内存逐渐耗尽,最终可能导致系统性能下降或者程序崩溃。
  • 例子:
            在 Java 中,虽然有垃圾回收机制,但也可能出现内存泄露。例如,当某个对象不再需要但仍然被引用时,垃圾回收器无法回收该对象的内存。
     
    public class MemoryLeakExamplef
        public static void main(string[l args) {
            List<Object> list = new ArrayList<>();
            while (true) {
                list.add(new 0bject());// 对象不断增加,但没有被释放
            }
        }
    }

2.1 静态属性导致内存泄露

        会导致内存泄露的一种情况就是大量使用static静态变量。在Java中,静态属性的生命周期通常伴随着应用整个生命周期(除非ClassLoader符合垃圾回收的条件)。

public class StaticTest {
    public static List<Double> list = new ArrayList<>();

    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
        Log.info("Debug Point 2");
    }

    public static void main(String[] args) {
        Log.info("Debug Point 1");
        new StaticTest().populateList();
        Log.info("Debug Point 3");
    }
}

如果监控内存堆内存的变化,会发现在打印Point1和Point2之间,堆内存会有一个明显的增长趋势图。

但当执行完populateList方法之后,对堆内存并没有被垃圾回收器进行回收。

针对上述程序,如果将定义list的变量前的static关键字去掉,再次执行程序,会发现内存发生了具体的变化。VisualVM监控信息如下图:

对比两个图可以看出,程序执行的前半部分内存使用情况都一样,但当执行完populateList方法之后,后者不再有引用指向对应的数据,垃圾回收器便进行了回收操作

因此,我们要十分留意static的变量,如果集合或大量的对象定义为static的,它们会停留在整个应用程序的生命周期当中。而它们所占用的内存空间,本可以用于其他地方。

那么如何优化呢?第一,进来减少静态变量;第二,如果使用单例,尽量采用懒加载。

2.2 未关闭的资源

无论什么时候当我们创建一个连接或打开一个流,JVM都会分配内存给这些资源。比如,数据库链接、输入流和session对象。

忘记关闭这些资源,会阻塞内存,从而导致GC无法进行清理。特别是当程序发生异常时,没有在finally中进行资源关闭的情况。

这些未正常关闭的连接,如果不进行处理,轻则影响程序性能,重则导致OutOfMemoryError异常发生。

如果进行处理呢?

  • 第一,始终记得在finally中进行资源的关闭;
  • 第二,关闭连接的自身代码不能发生异常;
  • 第三,Java7以上版本可使用try-with-resources代码方式进行资源关闭。

2.3 不当的equals方法和hashCode方法实现

当我们定义个新的类时,往往需要重写equals方法和hashCode方法。在HashSet和HashMap中的很多操作都用到了这两个方法。如果重写不得当,会造成内存泄露的问题。

下面来看一个具体的实例:

public class Person {
    public String name;
    
    public Person(String name) {
        this.name = name;
    }
}

现在将重复的Person对象插入到Map当中。我们知道Map的key是不能重复的。

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map<Person, Integer> map = new HashMap<>();
    for(int i=0; i<100; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertFalse(map.size() == 1);
}

上述代码中将Person对象作为key,存入Map当中。理论上当重复的key存入Map时,会进行对象的覆盖,不会导致内存的增长。

但由于上述代码的Person类并没有重写equals方法,因此在执行put操作时,Map会认为每次创建的对象都是新的对象,从而导致内存不断的增长。

VisualVM中显示信息如下图:

当重写equals方法和hashCode方法之后,Map当中便只会存储一个对象了。方法的实现如下:

public class Person {
    public String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Person)) {
            return false;
        }
        Person person = (Person) o;
        return person.name.equals(name);
    }
    
    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        return result;
    }
}

经过上述修改之后,Assert中判断Map的size便会返回true。

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map<Person, Integer> map = new HashMap<>();
    for(int i=0; i<2; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertTrue(map.size() == 1);
}

重写equals方法和hashCode方法之后,堆内存的变化如下图:

在这里插入图片描述

另外的例子就是当使用ORM框架,如Hibernate时,会使用equals方法和hashCode方法进行对象的的分析和缓存操作。

如果不重写这些方法,则发生内存泄漏的可能性非常高,因为Hibernate将无法比较对象(每次都是新对象),然后不停的更新缓存。

如何进行处理?

  • 第一,如果创建一个实体类,总是重写equals方法和hashCode方法;
  • 第二,不仅要覆盖默认的方法实现,而且还要考虑最优的实现方式;

2.4 外部类引用内部类

这种情况发生在非静态内部类(匿名类)中,在类初始化时,内部类总是需要外部类的一个实例。

每个非静态内部类默认都持有外部类的隐式引用。如果在应用程序中使用该内部类的对象,即使外部类使用完毕,也不会对其进行垃圾回收。

public class OuterClass {
    private String importantData;

    public OuterClass(String importantData) {
        this.importantData = importantData;
    }

    public void doSomething() {
        // 创建并启动线程,使用静态匿名内部类
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程运行中..." + importantData);
            }
        });
        thread.start();
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass("重要数据");
        outerClass.doSomething();
        
        //尝试释放outerClass对象
        outerClass = null;
        
        //...其他业务代码
    }
}

         这段代码中main方法执行 outerClass = null; 如果匿名内部类开启的线程没有执行结束,outerClass由于还被引用,不会被垃圾回收!

         在这个例子中,内存泄漏的原因在于非静态匿名内部类(实现了Runnable接口的类)隐式地持有对其外部类实例OuterClass的引用。这个引用是通过importantData字段访问外部类的成员变量时建立的。即使在main方法中将outer变量设置为null,外部类实例OuterClass也不能被垃圾回收,因为匿名内部类中的线程仍然持有对它的引用。也就是说如果这个线程没有结束,引用就一直存在。

这里我们只需要拷贝一份局部变量,就可以解除这个引用,从而避免内存泄漏的问题。

public class OuterClass {
    private String importantData;

    public OuterClass(String importantData) {
        this.importantData = importantData;
    }

    public void doSomething() {
        // 创建并启动线程,使用静态匿名内部类
        String data = this.importantData; //定义个局部的final变量
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程运行中..." + data);
            }
        });
        thread.start();
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass("重要数据");
        outerClass.doSomething();
    }
}

这样的话,匿名内部类中就不存在对外部类实例的引用。线程就不再直接引用OuterClass实例的成员变量,而是引用了一个局部变量的副本。因此,即使线程还在运行,一旦main方法中将outerClass变量设置为nullOuterClass的实例就可以被垃圾回收了。

标签:泄漏,void,OuterClass,线程,内存,new,public,溢出
From: https://blog.csdn.net/qq_64064246/article/details/140822438

相关文章

  • Unity引擎字符串内存布局
      Unity引擎的字符串有三种存储方式:堆:分配在堆上内嵌:一个栈上的内存数据。默认25字节,可以放长度最多24的字符串。这个长度定义为STACK_LENGTH. 外部  重点主要是前两种,这是一种优化方法,对于非常短的字符串,可以直接使用栈数据而不需要再次内存分配。C++伪代......
  • JVM内存结构划分
    JVM内存结构划分JVM(Java虚拟机)的内存结构主要划分为以下几个部分:堆(Heap)概述:堆是JVM中最大的一块内存区域,用于存储对象实例和数组。堆内存是垃圾收集器管理的主要区域,因此也被称为“GC堆”。细分:堆内存可以分为年轻代(YoungGeneration)和老年代(OldGeneration)。年轻代又进一......
  • JVM内存区域的划分
    程序计数器程序计数器是一块较小的内存空间,它可以看作当前线程所执行的字节码的行号指示器,在虚拟机的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖于程序计数器来完成。......
  • JVM内存结构划分
    栈"栈"(Stack)是一种遵循后进先出(LastInFirstOut,LIFO)原则的抽象数据类型。以下是栈的一些基本特点和操作:特点:LIFO原则:最后加入栈的元素将是第一个被移除的元素。动态大小:栈的大小可以根据需要动态变化。线性结构:元素存储在栈中的方式是线性的,但只能从一端(栈顶)访问。基......
  • 数组及数组JVM内存划分day4
    java中第一个存储数据的容器:数组特点:1、数组的长度大小是固定的2、同一个数组中,存储的元素数据类型是一样的数组的定义语句格式:数据类型[]数组名;举例:int[]arr;//定义了一个可以存储int类型的一维数组,数组名叫做arr......
  • 达梦数据库体系结构(物理结构、逻辑结构、内存结构、线程结构)
    达梦数据库体系结构(物理结构、逻辑结构、内存结构、线程结构) DM目录数据库安装目录下图展示为DM8数据库目录。  /dm8/bin 目录存放DM数据库的可执行文件,例如disql命令、dminit命令、dmrman工具等。  /dm8/desktop 存放DM数据库各个工具的桌面图标......
  • ThreadLocal和内存泄漏原理
    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录前言一、ThreadLocal原理二、ThreadLocal内存泄漏三、为什么使用弱引用?总结前言复杂事简单说:ThreadLocal一、ThreadLocal原理每一个线程绑定一个ThreadLocalMap,里面存放该线程自己的数据,......
  • 动态内存管理
    ⽬录1.为什么要有动态内存分配2.malloc和free3.calloc和realloc4.常⻅的动态内存的错误5.动态内存经典笔试题分析6.柔性数组7.总结C/C++中程序内存区域划分正⽂开始1.为什么要有动态内存分配我们已经掌握的......
  • 合并两个数据帧时的内存问题
    我对倒数第二句话一无所知。错误是:numpy.core._exceptions.MemoryError:无法为形状为(7791676634)和数据类型为int64的数组分配58.1GiB我的想法是将约1200万条记录的数据帧与另一个数据帧合并多3-4列应该不是什么大问题。请帮帮我。完全被困在这里了。谢谢Select_Emp_df......
  • 编写java程序,自动监控程度,dump内存文件
    步骤1:编写Java程序首先,编写一个Java程序,当内存使用达到11GB时生成heapdump文件,并以日期命名。将以下代码保存为MemoryMonitor.java文件:importcom.sun.management.HotSpotDiagnosticMXBean;importjavax.management.MBeanServer;importjava.lang.managemen......