Java自带了垃圾回收机制(GC),可以不用像C/C++那样去手动管理内存,但是在开发过程中如果操作不当还是不可避免避免的会发生内存泄漏,不过相对于 C/C++ 而言,Java的GC会使内存泄漏的情况大大减少。
下面列举了一些可能会发生内存泄漏的操作
依靠 HashCode 的集合,集合中对象发生改变。
通过底层实现依靠散列表的集合,如 HashSet
、HashTable
、HashMap
等,如果集合中存储的对象其HashCode的值的会因为属性改变而改变的。如果对象存入集合后,属性发生了改变,会删除不了,集合会一直引用着对象,导致内存泄漏。
示例:
public class MemoryLeak {
@Test
public void test01() {
Set<Person> set = new HashSet<>();
Person p1 = new Person("小明", 17);
Person p2 = new Person("大虎", 18);
Person p3 = new Person("小红", 19);
set.add(p1);
set.add(p2);
set.add(p3);
p2.setName("胖虎"); // 改变属性,HashCode也随之改变了
set.remove(p2); // 移除
System.out.println(set.size()); // size = 3, 没又 remove 掉
}
}
Person类:
package memoryleak;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Person() {
super();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
// 重写 HashCode
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
// 重写 Equals 方法
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
注意: Java中默认是以对象的堆地址做HashCode
,改变对象的属性根本不会引起HashCode
改变,这样无需无需担心内存泄漏,但是大部分情况我们都是需要重写equals
与hashCode
方法的,这样我们就不得不注意了。
静态集合类
被 static
修饰的集合属于类,它们的生命周期是跟应用一样长的,如果对象存入了静态集合中,在程序运行期会一直保持对该对象的引用,导致对象的生命周期跟静态变量一样长,造成内存泄漏。
public class MemoryLeak {
static List<Person> list = new ArrayList<>();
@Test
public void test03() {
Person p1 = new Person("小明", 17);
list.add(p1);
p1 = null; // list 还引用了p1, 所以即使 p1指向了null,还是不会被回收
// ...
}
}
最简单的解决方法就是,static List<Person> list = null;
,先声明为 null,在方法体里创建集合对象,这样离开了方法,集合对象就会被回收。
各种连接
Java中的各种连接对象,如 数据连接、网络连接、IO
连接等,使用完了后如果没有调用close
方法也会造成内存泄漏,最好是在 fianlly
中关闭如:
try {
// 获取连接
// 使用连接
} catch (Exception e) {
// 处理异常
} finally {
// 关闭连接
}
这样会保证无论如何连接都会关闭。
单例模式
使用单例模式的时候也有可能导致内存泄漏。因为单例对象初始化后将在JVM的整个生命周期内存在,如果它持有一个外部对象(生命周期比较短)的引用,那么这个外部对象就不能被回收,而导致内存泄漏。如果这个外部对象还持有其它对象的引用,那么内存泄漏会更严重。
总结:以上发生的内存泄漏除了未关闭连接,其余的内存泄漏引发原因本质都是一个生命周期长的对象,引用了生命周期短的,导致本来应该被GC回收的对象,因为被还有引用而没有没回收。