本文最后更新于 1028 天前,其中的信息可能已经有所发展或是发生改变。
Double Brace Initialization should not be used
前言
最近在修改sonar问题时,发现有人使用双花括号初始化集合,提示可能发生内存泄漏。这种初始化方式倒是见过,只知道是使用了匿名内部类,但没有意识到这个问题。
实测
A
提供两种Map的初始化方法,为了观察是否被回收,重写了finalize方法。
public class A {
private String name;
public A(String name) {
this.name = name;
}
public Map<String, String> getMap() {
return new HashMap<String, String>() {
private static final long serialVersionUID = -3309655755403147761L;
{
put("name", name);
}};
}
public Map<String, String> getMap1() {
return new HashMap<String, String>();
}
@Override
protected void finalize() throws Throwable {
System.out.println("Thread name: " + Thread.currentThread().getName() + " Object: " + this.name + " Gc happen");
super.finalize();
}
}
B
有个map成员变量
public class B {
private Map<String, String> map;
public B(Map<String, String> map) {
this.map = map;
}
}
Test
public class Test {
public static void main(String[] args) throws InterruptedException {
A a = new A("bob");
final B b = new B(a.getMap());
System.out.println("访问外部类对象的属性:" + b.getMap().get("name"));
a = null;
System.gc();
Thread.sleep(1000);
A a1 = new A("sandy");
final B b1 = new B(a1.getMap1());
a1 = null;
System.gc();
Thread.sleep(1000);
}
}
输出
访问外部类对象的属性:bob
Thread name: Finalizer Object: sandy Gc happen
分析
匿名内部类持有外部类对象引用
双花括号初始化时,可以直接使用外部类对象的成员name
。当然这只是表面,接下来我们从字节码的层面看看到底怎么回事。
使用了匿名内部类
- 双花括号初始化的方法
- 非双花括号初始化的方法
编译后产生的文件
可以发现,多了一个内部类:A$1.class。
查看内部类字节码
- 拥有一个外部类的成员变量
- 通过构造方法传入了外部类对象的引用
- 将外部类对象的引用赋值给成员变量
- 执行我们写的put方法
- 内部类继承于HashMap
发生了内存泄漏
使用非双花括号初始化map的sandy被回收了,而使用双花括号初始化map的bob却没有被回收。原因是b1的map和a1没啥关系,a1=null后,根不可达,所以被回收了。b的map持有a的引用,所以a=null后,还有强引用,依然根可达,不能回收,最终发生内存泄漏。
阿里老兵带我飞