java内存泄漏可能存在的原因

Java中的内存泄漏是指程序中已不再使用的对象无法被垃圾收集器回收,从而导致系统可用内存不断减少。

1.静态集合类持有引用(包括静态成员集合和静态代码块)
2.HashMap没有实现LRU缓存策略
3.内部类持有外部引用
4.线程资源未关闭
5.连接资源未正确关闭
6.ThreadLocal变量未清理
7.ThreadLocal变量未清理

以下是一些可能导致Java内存泄漏的常见场景及其示例代码:

1.静态集合类持有引用:

当静态集合(如HashSet、HashMap或ArrayList)存储了大量对象,并且这些对象在程序后续阶段已经不再需要时,如果未从集合中移除它们,就会造成内存泄漏。

public class MemoryLeakExample {
       private static Set<Object> objectSet = new HashSet<>();

       public void addObject() {
           Object obj = new Object();
           // 添加到集合但没有适时移除
           objectSet.add(obj);
       }
   }

如果addObject方法频繁调用并添加对象,而从未清理过objectSet,则即使这些对象在其他地方不再被引用,也会因为集合的静态属性而无法被GC回收。

2.注册监听器/回调未取消:

当注册一个监听器或者回调函数,并将当前类实例作为参数传递时,如果在不再需要该监听器的情况下未将其注销,可能会导致该实例无法被回收。

public class LeakyListener {
    private final EventListener listener;
    public LeakyListener() {
        listener = event -> System.out.println("Handling event...");
        SomeEventManager.register(listener);
    }
    // 在类析构或者不再使用时,如果没有取消注册listener,则存在内存泄漏风险
}

3.内部类持有外部类引用:

如果内部类实例持有了对外部类实例的隐式引用,而在内部类生命周期结束后没有正确释放外部类实例,也可能导致内存泄漏。

   public class OuterClass {
       private String data;
       
       public InnerClass getInner() {
           return new InnerClass();
       }

       class InnerClass {
           // 内部类默认会持有外部类的引用
       }
       
       // 如果InnerClass的实例在某个地方被保存下来,那么OuterClass即使不再使用也无法被回收
   }

   public class Main {
       public static void main(String[] args) {
           OuterClass outer = new OuterClass();
           // 内部类实例保存了下来
           OuterClass.InnerClass inner = outer.getInner();
           outer = null; // 这时outer不能被回收,因为它被inner引用着
       }
   }
   

4.线程资源未关闭:

当创建线程并且在线程执行体中引用了某些对象,若线程一直在运行并未终止,即使主线程不再使用那些对象,它们也不能被回收。

public class ThreadMemoryLeak {
       private final Object resource;

       public ThreadMemoryLeak() {
           resource = new Object();
           new Thread(() -> {
               while (true) {
                   // 引用了resource,但线程永远不会结束
               }
           }).start();
       }
 }
   

5.不正确的缓存设计:

内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,就容易被遗忘。
缓存机制中如果没有及时清理不再使用的对象,也可能导致内存泄漏。
对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对Key的引用外,此key没有其他应用那么此map回自动丢弃此值。

/**

* 演示内存泄漏

*/
   public class CacheManager {
       private Map<String, BigObject> cache = new HashMap<>();

       public void put(String key, BigObject value) {
           cache.put(key, value);
       }

       // 没有实现LRU或其他清理策略,当键值对数量不断增加时,可能导致内存泄漏
   }
   
public class MapTest {
static Map wMap = new WeakHashMap();
static Map map = new HashMap();


public static void main(String[] args) {
init();
testWeakHashMap();
testHashMap();
}


public static void init() {
String ref1 = new String("obejct1");
String ref2 = new String("obejct2");
String ref3 = new String("obejct3");
String ref4 = new String("obejct4");
wMap.put(ref1, "cacheObject1");
wMap.put(ref2, "cacheObject2");
map.put(ref3, "cacheObject3");
map.put(ref4, "cacheObject4");
System.out.println("String引用ref1,ref2,ref3,ref4 消失");
}


public static void testWeakHashMap() {
System.out.println("WeakHashMap GC之前");
for (Object o : wMap.entrySet()) {
System.out.println(o);
}
try {
System.gc();
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("WeakHashMap GC之后");
for (Object o : wMap.entrySet()) {
System.out.println(o);
}
}


public static void testHashMap() {
System.out.println("HashMap GC之前");
for (Object o : map.entrySet()) {
System.out.println(o);
}
try {
System.gc();
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("HashMap GC之后");
for (Object o : map.entrySet()) {
System.out.println(o);
}
}
}

6.资源未关闭(如数据库连接、套接字连接等):

当程序打开了一些系统级别的资源,如数据库连接或网络套接字,如果在使用完毕后没有正确关闭这些资源,可能会因为这些资源内部持有的Java对象引用而导致内存泄漏。

   public class ConnectionMemoryLeak {
       private Connection connection;

       public void openConnection() throws SQLException {
           connection = DriverManager.getConnection("...");
       }

       // 如果在某个操作后忘记调用connection.close()
       // 则即使不再需要该连接,也可能因JDBC驱动库内部持有连接对象而造成内存泄漏
   }
   

7.弱引用与软引用的不当使用:

虽然弱引用和软引用不会阻止GC回收它们所引用的对象,但如果程序设计不当,比如过度依赖于它们来维持对象生命周期,可能导致大量应该被回收的对象堆积在引用队列中无法释放。

   import java.lang.ref.WeakReference;

   public class WeakReferenceMemoryLeak {
       private static final List<WeakReference<Object>> weakRefs = new ArrayList<>();

       public void addWeakRef(Object obj) {
           weakRefs.add(new WeakReference<>(obj));
       }

       // 如果不清理并移除已经失效的WeakReference,尽管它们指向的对象可以被GC回收,
       // 但weakRefs集合本身会持续增长,占用不必要的内存空间
   }
   

8.ThreadLocal变量未清理:

ThreadLocal为每个线程维护了一个副本,如果不显式清理其内容,尤其是在线程池场景下,线程结束时ThreadLocal中的对象可能无法被回收,导致内存泄漏。

   public class ThreadLocalMemoryLeak {
       public static final ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();

       public void setThreadLocalData() {
           BigObject bigObj = new BigObject();
           threadLocal.set(bigObj);
           // 如果在线程执行完任务后没有清除threadLocal的内容,则bigObj可能无法被回收
       }
   }

   // 在线程结束后或不再需要ThreadLocal数据时,应确保调用threadLocal.remove()
   

9.匿名内部类持有外部类实例:

类似于之前的内部类示例,匿名内部类同样会隐式持有外部类实例的引用,如果没有正确处理匿名内部类生命周期,也会导致外部类实例无法被回收。

   public class AnonymousInnerClassMemoryLeak {
       private String data;

       public Runnable getRunnable() {
           return new Runnable() {
               @Override
               public void run() {
                   System.out.println(data);
               }
           };
       }

       // 如果将返回的Runnable对象保存在一个全局的地方,并且在不再需要时没有将其设为null,
       // 则即使AnonymousInnerClassMemoryLeak实例的其他引用都消失,它也无法被回收
   }
   

10.非静态内部类加载外部类静态变量:

即使是静态内部类,在其定义中引用了外部类的非静态成员时,仍然会导致对外部类实例的间接引用。

    public class OuterClass {
        private String nonStaticField;
        
        public static class StaticInnerClass {
            private final String value = outerInstance.nonStaticField; // 引用了外部类实例
        }
        
        // 如果StaticInnerClass的实例被创建并持久化,那么OuterClass即使不再使用也不能被回收
    }
    

11.变量不合理的作用域:

变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有即使地把对象设置未null,很有可能导致内存泄漏的发生

public class UsingRandom {
					private String msg;
					public void receivemsg(){
										// String msg; // 可以将 msg 放在方法内
										readFromNet(); // 从网络中接收数据保存到 msg 中
										saveDB(); // 把 msg 保存到数据库中
										msg = null; // 加这个防止内存泄漏
					}
}

如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内从保存到数据库中,此时msg已经就没有用了,由于msg的声明周期与对象的声明周期相同,此时msg还不能回收,因此造成了内存泄漏。

实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的声明周期也就结束了,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置成null,这样立即回收器也会回收。

12.改变哈希值

改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象的哪些参与计算哈希值的字段了。
否则,对象修改后的哈希值与最初存储进HashSet集合中的和细致就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HshSet集合中建锁对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。
这也是String为什么被设置成不可变类型,我们可以放心的把String存入HashSet,或者把String当作HashMap的Key值。
当我们想把自己定义的类型保存到散列表的时候,需要保证对象的HashCode不可变。

/**

* 演示内存泄漏

*/
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 + '\'' +
'}';
}
}
/**

* 演示内存泄漏

*/

public class ChangeHashCode1 {
public static void main(String[] args) {
HashSet hs = new HashSet();
Point cc = new Point();
cc.setX(10); // hashCode = 41

hs.add(cc);


cc.setX(20); // 修改了计算 hash 的成员值。hashCode = 51  此行为导致了内存的泄漏



System.out.println("hs.remove = " + hs.remove(cc)); // 检索不到,返回 false

hs.add(cc);
System.out.println("hs.size = " + hs.size()); // size = 2

System.out.println(hs);
}
}


class Point {
int x;


public int getX() {
return x;
}


public void setX(int x) {
this.x = x;
}


@Override

public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
return result;
}


@Override

public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Point other = (Point) obj;
if (x != other.x) return false;
return true;
}


@Override

public String toString() {
return "Point{" +
"x=" + x +
'}';
}
}

解决内存泄漏问题的关键在于理解对象的生命周期,并确保在不需要对象时能够适时地解除对它们的所有引用。

如何理顺内存泄漏

静态方法与内存泄漏:

1.局部变量: 如果静态方法内部创建的对象是局部变量,并且这个局部变量的作用域仅限于该方法内部,在方法执行完毕后,若没有其他任何对象对该局部变量所引用的对象持有引用,则垃圾回收器会在适当的时候回收这个对象,因此不会造成内存泄漏。

static void someMethod() {
    SomeObject localObject = new SomeObject(); // 局部变量,方法结束后会被自动回收
    // ... 对localObject的使用
}

2.静态变量引用: 然而,如果静态方法内部创建的对象被赋值给一个静态变量,那么由于静态变量的生命周期贯穿整个应用,只要静态变量还持有对这个对象的引用,即使程序的其他部分不再需要该对象,也会导致内存泄漏。

private static SomeObject staticObject;
static {
    staticObject = new SomeObject(); // 静态变量引用,可能导致内存泄漏
}
final修饰的静态变量引用

1.不会导致内存泄漏的情况:如果final静态变量引用的是不可变对象(如String、基本类型的包装类等),并且这些对象的内容在整个应用生命周期内始终是需要的,则这不是内存泄漏,因为它们的持久存在是有意义的。

public static final String CONSTANT = "A constant value";

可能导致内存泄漏的情况:当final静态变量引用了一个大的、可变的或者与UI相关的对象,并且这个对象在其生命周期结束后不再使用,但因为该静态引用的存在,使得垃圾回收器无法回收它,这就形成了内存泄漏。

public class MyClass {
    public static final MyLargeObject STATIC_FINAL_OBJECT = new MyLargeObject();
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值