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();
}