内存泄露的理解和分类

可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)。

内存泄漏(Memory Leak)的理解

严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。

但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。

对象X引用Y,X的生命周期比Y的生命周期长;

那么当Y生命周期结束的时候,X依然引用着Y,这时候,垃圾回收期是不会回收对象Y的;

如果对象X还引用着生命周期比较短的A、B、C,对象A又引用着对象a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。

内存泄漏与内存溢出的关系

内存泄漏(Memory Leak)

申请了内存用完了不释放,比如一共有1024M的内存,分配了512M的内存一直不回收,那么可以用的内存只有512M了,仿佛泄漏掉了一部分。

内存溢出(Out of Memory)

申请内存时,没有足够的内存可以使用。

两者关系

内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出。

泄漏的分类

经常发生:发生内存泄漏的代码会被多次执行,每次执行,泄漏一块内存

偶然发生:在某些特定情况下才会发生

一次性:发生内存泄漏的方法只会执行一次

隐式泄漏:一直占着内存不释放,直到执行结束;严格的说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽。

内存泄漏的情况

第一种:静态集合类

静态集合类,如HashMap、LinkedList等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是长生命周期对象持有它的引用而导致不能被回收。

 public class MemoryLeak {
     static List list = new ArrayList<>();
 ​
     public static void oomTests() {
         Object obj = new Object();
         list.add(obj);
     }
 }

第二种:单例模式

单例模式,和静态集合导致内存泄漏的原因类似,因为单例的静态特性,它的生命周期和JVM的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

第三种:内部类持有外部类

内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。

这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

第四种:各种连接

如数据库、网络连接、IO连接等。

在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。

否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性的关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

 public static void main(String[] args) {
     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 {
         //关闭结果集Statement
         //关闭声明的对象ResultSet
         //关闭链接Connection
     }
 }

第五种:改变哈希值

当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。

否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。

这也是String为什么被设置成了不可变类型,我们可以放心的把String存入HashSet,或者把String当作HashMap的key值。

当我们想把自己定义的类保存到散列表的时候,需要保证对象的hashCode不可变。

代码

 package com.gy.memoryLeak;
 ​
 import java.util.HashSet;
 ​
 /**
  * 演示内存泄漏
  */
 public class ChangeHashCode {
     public static void main(String[] args) {
         HashSet set = new HashSet();
         Person p1 = new Person(1001, "AA");
         Person p2 = new Person(1002, "BB");
 ​
         set.add(p1);
         set.add(p2);
 ​
         p1.name = "CC";//导致了内存的泄漏
         set.remove(p1); //删除失败
 ​
         System.out.println(set);
 ​
         set.add(new Person(1001, "CC"));
         System.out.println(set);
 ​
         set.add(new Person(1001, "AA"));
         System.out.println(set);
 ​
     }
 }
 ​
 class Person {
     int id;
     String name;
 ​
     public Person(int id, String name) {
         this.id = id;
         this.name = name;
     }
 ​
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
         if (!(o instanceof Person)) return false;
 ​
         Person person = (Person) o;
 ​
         if (id != person.id) return false;
         return name != null ? name.equals(person.name) : person.name == null;
     }
 ​
     @Override
     public int hashCode() {
         int result = id;
         result = 31 * result + (name != null ? name.hashCode() : 0);
         return result;
     }
 ​
     @Override
     public String toString() {
         return "Person{" +
                 "id=" + id +
                 ", name='" + name + '\'' +
                 '}';
     }
 }
 ​

运行结果

 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]
 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}]
 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]

分析

第六种:缓存泄漏

内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易忘记。

对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身对key的引用外,此key没有其他引用那么此map会自动丢弃此值。