JVM内存泄露与溢出

内存泄漏和内存溢出

内存泄露:申请的内存空间没有被正确释放,导致内存空间被占用,并且之后也不会使用。

内存溢出:申请的内存空间超过了空闲内存空间,即内存不够使用。

所以说,内存泄漏可能会导致内存溢出,我们需要注意有可能会导致内存泄漏的情况。

常见的内存泄露原因:

  • 静态变量引用:
    在JDK8中,没有永久代的概念了,静态集合也都有可能被垃圾回收。但是通常,静态集合被设计为缓存数据或者维护全局状态,其内容可能会随着应用的运行而动态变化,并且可能被多个线程同时访问和修改。由于缓存数据的特点,如果使用不当就会导致内存泄露例子。比如下面这个例子,即使这些元素已经不再需求,list中元素也没有被清空或者移除,它们仍然会一直存在与list,从而占用内存。

    import java.util.ArrayList;
    import java.util.List;
    
    public class StaticListLeakExample {
        private static List list = new ArrayList<>();
    
        public static void add(String value) {
            list.add(value);
        }
    
        public static int size() {
            return list.size();
        }
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 1000000; i++) {
                add("value" + i);
                if (size() % 100 == 0) {
                    System.out.println("size: " + size());
                    Thread.sleep(1000); // 模拟其他操作
                }
            }
        }
    }
    

    为了避免这种情况,我们可以在不再需要静态集合中的元素时,手动将其从集合中移除,或者使用弱引用等更加安全和灵活的解决方案,以便及时释放内存并提高应用程序的性能。

    以下是使用弱引用解决内存泄漏问题的示例代码:

    import java.lang.ref.WeakReference;
    import java.util.ArrayList;
    import java.util.List;
    
    public class WeakListExample {
        private static List> list = new ArrayList<>();
    
        public static void add(String value) {
            list.add(new WeakReference<>(value));
        }
    
        public static int size() {
            return list.size();
        }
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 1000000; i++) {
                add("value" + i);
                if (size() % 100 == 0) {
                    System.out.println("size: " + size());
                    Thread.sleep(1000); // 模拟其他操作
                }
            }
        }
    }
    

    在上述示例中,我们使用 java.lang.ref.WeakReference类来包装 String类型的元素,将其添加到静态集合 list中。因为 WeakReference是一种弱引用,即只要该对象没有被强引用所持有,垃圾回收器就可以随时回收它。这意味着,如果 list中的某个元素不再被任何强引用所持有,那么它就会被自动回收。

    需要注意的是,由于弱引用可能会被垃圾回收器提前回收,因此当我们从 WeakReference对象中获取元素时,需要先判断该元素是否已经被回收,以避免出现 NullPointerException等异常。具体来说,我们可以使用 WeakReference类的 get()方法来获取元素,并判断其返回值是否为null。

    总之,使用弱引用可以避免静态集合导致的内存泄漏问题,但也需要注意弱引用可能被提前回收的特点,并在代码中做好相应的处理。

  • 单例模式

    单利模式是一种常见的设计模式,它确保一个类只有一个实例,并提供全局访问点。但是如果这个单例对象持有了其他对象的引用,而这些被引用的对象不再使用却仍然存在于内存,就会导致内存泄露。

  • 数据库连接、IO、Socket等连接

    创建的连接不再使用时,需要调用close方法关闭连接,只有连接被关闭,GC才会回收对应的对象(Connection,Statement、ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被GC回收。

    try {
        Connection conn = null;
        Class.forName("com.mysql.jdbc.Driver");
        conn = DriverManager.getConnection("url", "", "");
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("....");
      } catch (Exception e) {
    
      }finally {
        //不关闭连接
      }
    
  • 变量不合理的作用域

    一个变量的定义作用于大于其使用范围,很可能存在内存泄露;或不再将使用对象设置为null,很可能导内存泄露的发生。

    public class Simple {
        Object object;
        public void method1(){
            object = new Object();
            //...其他代码
            //由于作用域原因,method1执行完成之后,object 对象所分配的内存不会马上释放
            object = null;
        }
    }
    
  • ThreadLocal使用不当

    JVM内存泄露与溢出_第1张图片

    从上图可以看出来,hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在一个外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null,而value还存在着强引用,只有thread线程退出以后,value的强引用链条才会断掉。如果当前线程一直不结束掉,那么这些key为null的Entry的value就会一直存在一条强引用链:

    Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

    永远无法回收,造成内存泄漏。

    key 使用强引用:

    当hreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

    key 使用弱引用:

    当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

    因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

    正确使用:

    1. 及时清理ThreadLocal的引用

      当一个线程结束时,其中的 ThreadLocal 副本会被释放,但它所持有的对象可能仍然存在于内存中。因此,为了避免内存泄漏,需要及时清理对应的 ThreadLocal 引用。这通常可以通过在使用完毕后将 ThreadLocal 变量设为 null 来实现。

    2. 避免创建过多的ThreadLocal对象

      每个ThreadLocal对象都会占用一定的内存空间,并且会在每个线程上创建一个副本。因此如果创建过多的ThreadLocal对象,就会占用大量的内存空间,甚至导致内存溢出问题。可以考虑将多个相关的变量整合到同一个ThreadLocal对象中减少对象数据。

    3. 使用静态的ThreadLcoal变量,要注意不要直接将其定义为静态成员变量,因为这样会让该变量跨越多个类加载器而导致内存泄露。可以考虑使用 ThreadLocal.withInitial()方法来创建静态的ThreadLocal变量。

      Java 虚拟机中,每个类加载器都有自己的命名空间(namespace),不同的类加载器之间可以加载同名但不同版本的类。当一个线程在一个类加载器中创建了静态的 ThreadLocal 变量,并将其定义为该类的静态成员变量时,如果这个线程后续在另一个类加载器中加载了同名但不同版本的类,并且访问了该类的静态成员变量(包括 ThreadLocal 变量),就会出现内存泄漏问题。

      这是因为,在多个类加载器中加载的同名类,虽然名称相同但实际上是不同的类类型,它们拥有各自独立的静态成员变量和 ThreadLocal 变量副本。而对于一个线程来说,它只能访问到在同一类加载器中加载的同名类的静态成员变量和 ThreadLocal 变量副本,而无法访问其他类加载器中的变量副本。因此,如果在一个类加载器中创建了静态的 ThreadLocal 变量,并且直接将其定义为静态成员变量,就会导致该变量跨越多个类加载器而产生内存泄漏问题

    4. 避免在循环中重复创建 ThreadLocal 对象。ThreadLocal应当设计为全局使用。

  • Hash值发生变化

    对象 Hash 值改变,使用 HashMap、HashSet 等容器中时候,由于对象修改之后的 Hah 值和存储进容器时的 Hash 值不同,所以无法找到存入的对象,自然也无法单独删除了,这也会造成内存泄漏。说句题外话,这也是为什么 String 类型被设置成了不可变类型。

    在 Java 中,HashMapHashSet 的元素的 hash 值是由元素的 keyvalue 共同决定的。具体来说,对于一个 HashMapHashSet 中的元素,其 hash 值的计算方式如下:

    1. 如果该元素的 keyvaluenull,那么它的 hash 值也为 0
    2. 如果该元素的 keyvalue 不为 null,首先会调用它们的 hashCode() 方法计算出它们各自的 hash 值。
    3. 然后将两个 hash 值通过异或运算(^)组合起来作为该元素的最终 hash 值。

    这种方式可以保证 HashMapHashSet 中的每个元素都有唯一的 hash 值,并且能够尽量避免碰撞(即不同元素拥有相同的 hash 值)。

你可能感兴趣的:(java,jvm)