内部类外部类引用导致内存泄漏
上一篇文章我们了解了内存泄露的几种场景,其中有一项内部类对外部类的引用导致内存泄露
这点似乎在我们的实际项目中很少遇到,听到这个的时候, 是懵逼的,所以今天就单独抽出章节,对这种内存泄露的方式进行分析
1.内部类作用
Java语言中,非静态内部类的主要作用有以下两个
- 当内部类只在外部类(主类)中使用时,匿名内部类可以让外部不知道它的存在,对于外部来说是不透明的, 从而减少了代码的维护工作
- 当内部类持有外部类时,它就可以直接使用外部类中的变量了,就是小类使用大类中的属性,这样可以很便捷的进行调用开发
除了这两个作用外, 内部类的作用不大,实际的项目中,写出这样的代码是要被骂的, 正常情况下都不会使用嵌套内部类来处理业务逻辑
2.内部类引用外部类属性
内部类分析
- Inner 类只在 Outer的主类中使用, 别人几乎不知道它存在, 对外部透明
- Inner 中的变量name, 可以直接使用外部类Outer的变量 outerName,进行赋值
代码如下:
package com.jzj.tdmybatis.leak;
class Outer {
//外部类属性 outerName
private String outerName = "jzj";
/**
* 内部类定义
*/
class Inner {
private String name;
/**
* 内部类可以直接访问外部类属性信息, 直接获取outerName
*/
public Inner() {
this.name = outerName;
}
}
Inner innerInstance() {
return new Inner();
}
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.innerInstance();
System.out.println(inner.name);
}
}
执行结果成功打印了 外部类中的变量信息, 进行赋值 "jzj"
3.静态内部类无法持有外部类和外部类的非静态字段
下面我们构造一个静态内部类,尝试用该类去获取外部类的静态属性及非静态属性
- 构造static class inner静态内部类
- Outer外部类 有两个属性 outerName 和 静态属性 staticOuterName
- inner的构造函数,初始化inner变量name, 引用外部类 outerName 报错
- 提示 Non-static field 'outerName' cannot be referenced from a static context
- 非静态属性 outerName 不能被引用
- 然后正常引用 staticOuterName 属性
下面我们看下程序, OutClass外部类 及 InnerClass内部类
class Outer {
private String outerName = "jzj";
private static String staticOuterName="AAA";
/**
* 构造静态内部类 inner
*/
static class Inner {
private String name;
public Inner() {
this.name = staticOuterName;
}
public Inner() {
this.name = outerName;
}
}
Inner innerInstance() {
return new Inner();
}
}
静态内部类, 引用外部非静态变量, 错误信息如下:
4.静态内部类对外部的持有 及问题解决方案
我们看下代码,静态内部类对外部类的持有
- Outer外部类
- inner 非静态内部类
- 内部类持有外部类的引用
- Debug 出现了 this$0 的外部对象引用
- 会产生内存泄漏
package com.jzj.jvmtest.leaktest;
class TestOut {
class testInner {
}
testInner createInner() {
return new testInner();
}
}
class TestMain {
public static void main(String[] args) {
TestOut.testInner inner = new TestOut().createInner();
System.out.println(inner);
}
}
执行结果及Debug,出现对外部类的引用 this$0 对象引用
我们修改下代码, 相同的启动类信息 static inner class 静态内部类
static class testInner {
}
执行结果及Debug,不会出现对象的引用, this$0不存在
解决方案
- 不要让其他的地方,持有这个非静态内部类的引用,所有相关业务都在非静态内部类执行
- 将非静态内部类改为静态内部类。内部类改为静态
- 变为静态后,它所引用的对象或属性也必须是静态的,所以静态内部类无法获得外部对象的引用
5.内存泄漏程序测试
设置JVM参数
-verbose:gc -XX:+UseG1GC -Xms100M -Xmx100M -XX:+PrintGC -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:SurvivorRatio=8
外部类及内部类信息, 外部类构造了一个大对象
package com.jzj.jvmtest.leaktest;
public class OutClass {
private byte[] obj;
public OutClass() {
//10K 对象
this.obj = new byte[20 * 1024];
}
/**
* 内部类
*/
public class InClass {
}
/**
* 内部类 的实例
*/
public InClass instanceInner() {
return new InClass();
}
}
Main函数测试,循环10W次,构建对象
package com.jzj.jvmtest.leaktest;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class LeakOuterInnerTest {
public static void main(String[] args) throws Exception {
List<OutClass.InClass> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
//外部类
OutClass outClass = new OutClass();
list.add(outClass.instanceInner());
log.info("========" + i++);
Thread.sleep(1);
}
}
}
6.内存泄漏执行结果
6.1 OOM内存溢出
执行Main函数,查看结果, 打印到9982 次循环的时候就出现了问题
Allocation Failure FullGC 内存泄漏导致 内存溢出
23:48:35.323 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========9974
23:48:35.324 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========9976
23:48:35.325 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========9978
23:48:35.327 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========9980
23:48:35.328 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========9982
[GC pause (G1 Evacuation Pause) (young) (to-space exhausted), 0.0031774 secs]
[Parallel Time: 1.4 ms, GC Workers: 10]
...
...
...
[Full GC (Allocation Failure) 98M->1239K(100M), 0.0042052 secs]
[Eden: 0.0B(5120.0K)->0.0B(53.0M) Survivors: 0.0B->0.0B Heap: 99.0M(100.0M)->1239.5K(100.0M)], [Metaspace: 5321K->5321K(1056768K)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[GC concurrent-mark-abort]
Heap
garbage-first heap total 102400K, used 1239K [0x00000000f9c00000, 0x00000000f9d00320, 0x0000000100000000)
region size 1024K, 1 young (1024K), 0 survivors (0K)
Metaspace used 5352K, capacity 5500K, committed 5760K, reserved 1056768K
class space used 578K, capacity 596K, committed 640K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.jzj.jvmtest.leaktest.OutClass.<init>(OutClass.java:8)
at com.jzj.jvmtest.leaktest.LeakOuterInnerTest.main(LeakOuterInnerTest.java:15)
6.2 改静态内部类后
解决方案就是 给innerClass 加上static 关键字, 变为静态内部类
/**
* 内部类
*/
public static class InClass {
}
/**
* 内部类 的实例
*/
public static InClass instance() {
return new InClass();
}
再次执行 Main函数,测试结果
代码执行到99998次循环, 依旧正常运行,不存在内存泄漏
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99976
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99978
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99980
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99982
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99984
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99986
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99988
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99990
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99992
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99994
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99996
00:35:39.803 [main] INFO com.jzj.jvmtest.leaktest.LeakOuterInnerTest - ========99998
Heap
garbage-first heap total 102400K, used 45231K [0x00000000f9c00000, 0x00000000f9d00320, 0x0000000100000000)
region size 1024K, 44 young (45056K), 2 survivors (2048K)
Metaspace used 5221K, capacity 5356K, committed 5504K, reserved 1056768K
class space used 568K, capacity 596K, committed 640K, reserved 1048576K
Process finished with exit code 0
7. 启动过程 Jprofiler分析
-
使用Jprofiler 启动Main函数,可以看到 堆内存的变化 20M已使用内存
-
堆内存的变化 20M->60M已使用内存,内存上升曲线 斜向上稳定增长,不会回收
-
堆内存的变化 6M->8M已使用内存,几乎溢出,但是内存上升曲线 斜向上稳定增长,不会回收
-
堆内存达到 100M 设置,出现OOM
-
老年代 阶梯式增长,不会回收
-
年轻代Eden区 会被回收
-Survivor区也是阶梯增长
最终导致内存泄漏
本文通过内部类对外部类的引用,分析了内存泄漏的产生过程,我们在实际项目中一定要注意这点,构建内部类的时候一定要谨记 构造成static 静态内部类,防止内存泄漏