永远不要使用双花括号初始化实例,除非你想OOM!

生活中的为难无处不在,有时候你仅仅想简略的装一把,但某些“老同志”总是在不经意之间,给你无情的一脚,踹得你几乎无法呼吸。

但谁让咱年轻呢?吃亏要趁早,前路会更好。

喝了这G p ^ $口温热的鸡汤,咱们来聊聊是怎么回事。

事情是这样的,在一个不大不小的项目中,小王写下了这段代码:

Map<String, String> map = new HashMap() {{
put("map1",l C m "value1");
put("map2", "value2");
put("map3", "value3");
}};
map.forEach((k, v) -> {
Systeb _ mm.out.println("key:i h D" + k + " value:" + v);
});

原本是用它来代替U 8 w l H下面这段代码的:

Map<String, String> map = new HashMap();
map.put("map1", "value1");
map.pg 9 ` a ^ I @ut("map2", "value2");
map.put("map3", "value3");
map.forEach((k, v) -> {
System.out.p/ p Krintln("keyB 9 $:" + k + " value:" + v);
});

两块代码的履行成果也是完全相同的:

key:map3 value:value3

key:map2 value:value2

key:map1 value:value1

所以小王正在满意的把这段代码介绍给部分新来的妹子小甜甜看,却不巧被正在经过的老张也看到了。

老张原本仅仅想给昨日的枸杞X [ ^ j N u P再续上一杯 85 的热水,但说来也巧,刚好撞到了一次能在小甜甜面前秀技能的一波机会d + i ` V ( s u t,所以习惯性的整理 1 V B q #了一下自己稀少的秀发,便敞开了 diA % @ss 模式。

永远不要使用双花括号初始化实例,除非你想OOM!

“小王啊,你这个代码问题很大啊!”

“怎么能用双花括号初始化实例呢?”

此时的小王被问的一脸懵逼,心里有无数个草泥马奔腾而过,心想你这头老牛竟然也和我争这颗嫩草,但心里却有一种不祥的预感,感觉自己要输,瞬间羞涩的不知该说啥,只能红着小脸,悄悄的“嗯?”了一声。

老张:“运用双花括号初始化实例是会导致内存溢出的啦!侬不晓R = M ; p ) B 9得嘛?”

小王沉默了片刻,仅仅凭借着以往的经历g ^ h来看,这“老家伙”仍是有点东西的,所以唐塞! q J !的“哦~”了一声,似乎自己理解了怎么回事相同,? B ` b W 2 v,其实心里仍然迷茫的一匹,为了不让其他同事发现,只得这般作态。

所以片刻的唐塞,待老张离去之后,才悄悄的打开了 Google,默默的搜索了! j 7 J % &一下。

小王:哦,原来~ d $ 4 c . A A 3如此……

双花括号初始化剖析

首要,咱们来看运用` % c u双花括号初始化( f c的本质是什么?

以咱们这段代A ^ & h l F _ j v码为例:

Map<String, String> map = newl j ~ , G HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3"i u o m | ) 0 z, "value3");
}};

这段代码其实V Y 0 是创建了匿名内部类,然后再进行初始化代码块

这一点咱们能够运用命令 javac 将代码编译成字节码之后发! T a i 3现,咱们发现之前的一个类被编译成两个字节码(& c 2 f.ch ` y + 0 6 lass)文件,如下图所示:

永远不要使用双花括号初始化实例,除非你想OOM!

咱们运用 Idea 打开 DoubleBracket$9 4 q p x C1.classE c ) . q % : ~ 文件发现:

import java.util.HashMap;
class Dou! 9 ( x h X AbleBracket$1 extends HashMap {F W 0 P h k x
DoubleBracker & H [ 0 N t$1(DoubleBracket var1) {
this.this$0 = var1;
this.put("map1", "value1");
this.put("map * w2", "value2");
}
}

此时@ c 4 X F咱们能够承认,它就是一个匿名内部类。那么问题来了,匿名内部类为什么会导致& c Z O k _ 内存溢出呢?

匿名内部类的“锅”

Java 言语中非静态内部类会持有外部类的引证,然后导致 GC 无法回收这部分代码的引证,以至于形成内6 = X 8存溢出。

思考 1:为什么要持有外部类?

这个就要从匿名内部类的J w g ^ I o $ W规划说起了,在 Java 言语中,非静态匿名内部类的主要效果有两个。

1、当匿名内部L K r类只在外部类(主类)中运用时,匿名内部类能够让u 9 外部不知道它的存在,然后减少了代码的维护工作。

2、当匿名内部类持有外部类时,它就能够直接运用外部类中的变量了,这样能够很便利的完结调用,p * x C ) . W 4如下代码所示:

publ) & i K x U (ic claF Q P K Kss DoubleBracket {e Q d c p e a
private static String userName = "磊哥"( b e ^ ^  /;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Map<String, String> map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
put(userName, userName);
}};
}
}

从上述代码能t } ` A = D u c [够看出在 HashMap 的办法内部,能够直接运用外部类的变量 userName

思考 2:它是怎么持q # i 6 e h有外部类的?

关于匿名内部类是怎么耐久外部目标的,咱们能够经过检查匿名内部类的字节码得知,咱们运用 javap -c DoubleBracket$1.clasE [ o V +s 命令进A u o f g _ 7 c行检查,其间 $1 为以匿名类的字7 C H U ! K节码,字节码的内容如下;

javap -c DoubleBracket$1.class
Compiled fre 7 ^ ; @ A Q Y uom x o 0 ? + Y"DoubleBracket.java"
class com.example.DoubleBracket$1 extends java.util.HashMap {
final com.example.DoubleBracket this$0;
com.example.DoubO s ~ A ( H _ ; ZleBracket$1(com.example.Doub9 $ p _leBracket);
Code:
0: aload_0
1: aload_1
2: putfield      #1                  // Field this$0:Lcom/example/DoubleBracket;
5: aload_0
6: invokespecial #7                  // Method java/util/HashMap."<init>":()V
9: aload_0
10: ldc           #13                 // String map1
12: ldc           #15                 // String value1
14: invokevirtual #17                 // Method put:(LjX 1 ` I X x X 6ava/lang/Object;Ljv 2 Kava/lang/O{ 6 %bject;)Ljava/lang/Object;
17: pop
18:9 V n N Q _ ! l ] aload_0
19: ldc           #21                 // String map2
21: ldc           #23                 // String value2
23: invokevirtual #17                 // Method put:(Ljava/lJ J 8 $ 3 b @ | Iang/Object;Ljava/lang/Object;)Ljava/lang/Object;
26: pop
27: return
}

其间,要害代码的在 putfield 这一行,此行表明有一个对 DoubleBracket 的引证被存入到 this$0 中,也就是说这个匿名内部类持有了外部类的引证。

如果您] u h觉得以上字节码不够直观,不要紧,咱们用下面的实践的代码来证明一下:

import javaU I ) {.lang.reflect.h A d / N e G m YFip $ s [ 9 B (eld;
import: s Q a m % java.util.HashMap;
import java.util.Map;
public class DoubleBr@ P x S Y #acket {
public st] ) M  & R Q latic void main(String[] args) throws NoSuchFieldException, Illegall 2 _ 3 6 a ] 6AccessException {
Map map = new DoubleBracket().createMap();
// 获取一个类的所有字段
Field field = map.getClass().getDeclaredField("this$0");
// 设置答应m | w | O z *办法私有的 pS u 7rivate 润饰的变量
field.setAccessO m O ;ible(true);
System.out.println(field.get(map).getClass());
}
public Map createMap() {
// 双w Z Z u花括号初始化
Map map = new HashMap() {{
put("map1", "valD : t q due1~ z X j X");x l s  b K % h
put("map2", "value2");
put("map3", "valueW u o3");
}};
return map;
}
}

当咱们敞开调试模式时,能够看出 map 中持有了外部目标 DoubleBracket,如下图所示:

永远不要使用双花括号初始化实例,除非你想OOM!

以上代码的Q R 1履行成果为:

claR , 9 ss com.example.DoubleB4 | 0 c ~ +racket

从以上程序输出成果能够看出:匿名g I 1 V ! ~ h + 3内部类持有了外部类的引证,因此咱们才能够运用 $0 正常获取到外部类,并输出相关的类信息

什么情况会导致内存走漏?

当咱们把以下正常的代码(无返回值):

public void createMap() {
Map map = new HashMap() {{
put("map1@ 2 T z u", "value1");
put("map2", "value2");
put("map3", "value3");
}};
// 业务处理....
}

改为下面这个样子时(返回了 M+ ` a n { p Qap 调集),或许会形成内存走漏:

public Map createMap() {
Map map = new HashMa: ( & T I Jp() {{
put("ms A h Nap1", "value1");
put("map2", "value2");
put("map3",R L + X s L [ 4 G s 6 ! p"value3");
}};
return map;
}

为什么用了「X B 6 F x P : E或许」而不是「必定」会形成内存走漏?

永远不要使用双花括号初始化实例,除非你想OOM!

这是由于当此 map 被赋值为其他类特点时,或许会导致 GC 搜集时不清理此目标,这时候~ k W – Z p才会导致内存走漏。能够重视我「Java中文社群」后面会专门写一篇关于此问题的文章。

怎么确保内存不走漏?

要想确保双花扣号不走漏,办法也很简略,只需求将R w n . – & } map 目标声明为 static 静态类型的就能够了,代码如下:

public static Map createMap() {
Map mak  R H vp = new HashMap() {{
put("map1; & $ g 5 D 0 R ?", "value1");
put("map2", "value2");
put("map3", "value3");
}};
return map;
}

什么?你不相信!

永远不要使用双花括号初始化实例,除非你想OOM!

不要紧,咱们用事实说话,运用以上代码,咱们重新编译一份字节码,检查t U @ G ` p , l %匿名类的内容如下:

javap -c  DoubleBracket$1.class
Compiled from "DoubleBracket.javo = aa"
class com.example.DoubleBracke$ + A h a } @ _t$1 extends java.util.HashMap {
com.example.DoubleBracket$1();
Code:
0: al@ $ moad_0
1: invokespecZ 3 - M Sial #1                  // Method java/util/HashMap."<. r e p;init>":()V
4: a1 c t + U S l Kload_0
5: ldc           #7                  // String map1
7: ldc           #9                  // String value1
9: invo^ p akevirtual #11                 // Method put:(Ljava/lang/Object;Ljava/l4 F :an% w D N P g/Object;)Ljava/lang/Object;
12:} x G r pop
13: aload_0
14: ldc           #17                 /~ $ ; V q // String map2
16: ldc           #19                 // String value2
18:C ~ y ; b T ~ invokevirtuY E !al #11                 // Method put:(Ljava/lang/Object;LjavS z q Ia/lang/Object;)Ljava/lang/Object;
21: pop
22: aload_0
23: ldc           #21                 // String map3
25: ldc           #23                 // String value3
27: invokevirtual #11                 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljav9 ( : Ga/lang/Object;
30: pop
31: return
}

从这次的代码咱们能够看出,现已没有 putfi3 ` e y 2 -eld 要害字x , @ L这一行了,也就是说# ] g * W a $静态r _ ; B N c 5匿名类不会持有外部目标的引证了L P F $ 3 7 ( X s

为什么静态内部类不会持8 I * # D ( I有外部类的引证?

原因其实很简略,由于匿名内部类是静态的之后,它所引证的目标或特点也有必要是静态的了,因此就能够直接从 JVM 的 Method Area(办法区)获取到引证而无需耐久外部目标了。

双花括号的代替计划

即使声明为静态的变量能够防止内存走漏,但仍旧不建议这样运用,为什么呢?

原因很简l 4 ( . & l c B $略,项目一般都是需求团队协作的,假如那位` @ C :老兄在不知情的情况下把你的 stat@ I M ;ic 给删掉呢?这就相当于设置了一个隐形的“坑”,其他不知5 ] Z f b H e 4道的人,一不小心就跳进去了,所以咱们能够尝试一些其他的计划,比如 Java8 中的 Strm T H F x Beam API 和 Java9 中的调集工厂等。

代替计划 1:Stream

运用 Java8 中的 Stream API 代替,示例如下。原代码:

List<String> list = new ArrayLb D ~ Uist() {{
add(1 & b 1 $ 0"Java");
add("Redis");
}};

代代替码:

List<Strin] ^ ; H 0g> list = Stream.of("Java", "$ D k R G I ( ?Redis").collect(Coll) N P R 1 | Gectors.toList());

代替计划 2:调集工厂

运用调集Z M l工厂的 of 办法代替,示例如3 s 0 ( # ? o r下。原代码:

Map map = new HashMap() {{
put("map1", "value1");
put("map2* * K ^ E", "value2");
}};

代代替码:

Map map = Map.of("map1", "JavaH { e S B q r N !", "map2", "Redis");

明显运用 Java9 中E – V N ( 3的计划非常适合咱们,简略又酷炫,只可惜咱们还在用 Java 6…6…6… 心碎了一地。

永远不要使用双花括号初始化实例,除非你想OOM!

总结

本文q H y { N 1 ~咱们讲了双花括号初始化由于会持有外部类的引证,然后能够会导致内存走漏的问题,还从字节码以及反射的层面演示了这个问题。

要想确保双花L T – ? N s S ` 7括号初始化不会出现内存走漏的办法也很简略,只需求被 static 润饰即可,但这样做仍是存在潜在的危险,或许会被某人不小心删除去,所以咱们+ h 9另寻它道,发现了能够运用 Java8 中的 Stream 或h j r Java9 中的调集工厂 of 办法代替“{{”。

最终的话

原创不易,点个「」再走呗p ^ : Q

参考 & 道谢

www.ripjava.com3 ] G 4 o/article/129…

cloud.tencent.cE ~ L & E bom/developer/a…

hacpai.com/article/149…

重视大w 3 [ + ] b /众号「Java中文社群」回复“干货”,获取 50 篇原创干货 Top 榜

永远不要使用双花括号初始化实例,除非你想OOM!