内存泄露问题
内存泄漏是指不使用的对象持续占有内存使得内存得不到释放,从而造成内存空间的浪费。
内存泄露导致问题:
- 最明显问题频繁GC,从而STW次数增加,导致用户体验变差;
- 如果内存泄露问题严重,会导致OOM,直接导致程序不能正常运行;
内存泄露是很严重的问题,在出现内存泄露的情况下,想要解决是肯定要修改代码的,所以在编写代码的时候要避免出现内存泄露。
内存泄露原因
大多数内存泄露的原因是,长生命周期的对象引用了短生命周期的对象。例如,A对象引用B对象,A对象的生命周期(t1-t4)比B对象的生命周期(t2-t3)长的多。当B对象没有被应用程序使用之后,A对象仍然在引用着B对象。这样,垃圾回收器就没办法将B对象从内存中移除,从而导致内存泄露问题。
变量作用域不合理导致内存泄露
一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏
public class UsingRandom {
private String msg;
public void receiveMsg(){
readFromNet();//从网络中接受数据保存到msg中
saveDB();//把msg保存到数据库中
}
}
解决办法,将msg定义在receiveMsg方法中或将msg重置为null。
public class UsingRandom {
private String msg;
public void receiveMsg(){
readFromNet();//从网络中接受数据保存到msg中
saveDB();//把msg保存到数据库中
msg = null;
}
}
向静态集合添加数据导致内存泄露
HashMap、LinkedList 等集合类,如果这些集合是静态的并且向集合中添加了对象,这些对象就算不再使用,也不会被GC主动回收的,它们的生命周期与JVM程序一致,容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。
public class MyTest {
static Map<String, User> map = new HashMap<>();
public static void main(String[] args) throws InterruptedException {
User user = new User();
map.put("01",user);
}
}
解决办法是将使用完后的集合和对象重置为null,或将集合替换成弱引用集合(只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存)。
static Map<String, User> map = new HashMap<>();
public static void main(String[] args) throws InterruptedException {
User user = new User();
map.put("01",user);
user = null;
map = null;
System.gc();
Thread.sleep(1000);
}
static Map<String, User> map = new WeakHashMap<>();
public static void main(String[] args) throws InterruptedException {
User user = new User();
map.put("01",user);
}
内部类持有外部类引用导致内存泄露
非静态内部类,自动生成的构造方法,默认的参数是外部类的类型,因此使用非内部内部类的时候会保留一个外部类的引用。如果换成静态内部类则不会生成默认的构造方法。
public class MyClass {
public static void main(String[] args) throws Throwable {
}
public class A{
public void methed1(){
}
}
public static class B{
public void methed1(){
}
}
}
如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
数据结构中移除元素导致内存泄露
- 链表中移除元素,没有将移除的元素置为null导致的内存泄露问题
Java容器LinkedList移除元素方法核心源码:
//删除指定节点并返回被删除的元素值
E unlink(Node<E> x) {
//获取当前值和前后节点
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next; //如果前一个节点为空(如当前节点为首节点),后一个节点成为新的首节点
} else {
prev.next = next;//如果前一个节点不为空,那么他先后指向当前的下一个节点
x.prev = null;
}
if (next == null) {
last = prev; //如果后一个节点为空(如当前节点为尾节点),当前节点前一个成为新的尾节点
} else {
next.prev = prev;//如果后一个节点不为空,后一个节点向前指向当前的前一个节点
x.next = null;
}
x.item = null; // help gc
size--;
modCount++;
return element;
}
除了修改节点间的关联关系,我们还要做的就是赋值为null的操作,不管GC何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象。
- 在栈中将该元素出栈,没有将出栈的元素置为null导致的内存泄露问题
public E pop(){
if(size == 0)
return null;
else{
E e = (E) elementData[--size];
elementData[size] = null; // help gc
return e;
}
}
- 改变hash表中对象的属性值,再将该元素移除导致的内存泄露问题
因为改变了对象属性的值相当于改变了改对象的hash值,删除的时候是根据对象的hash值来删除的,删除对象的时候找不到对应的hash值,所以不能删除,最终导致内存泄露。
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 + '\'' +
'}';
}
}
连接未释放
如数据库连接、网络连接和IO连接等。当不再使用时,需要调用close方法来释放连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在连接过程中,对一些对象不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
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 {
// 1.关闭结果集 Statement
// 2.关闭声明的对象 ResultSet
// 3.关闭连接 Connection
}
}