JVM系列(二十五) JVM调优实战-内部类外部类引用导致内存泄漏

512 阅读6分钟

内部类外部类引用导致内存泄漏

上一篇文章我们了解了内存泄露的几种场景,其中有一项内部类对外部类的引用导致内存泄露

这点似乎在我们的实际项目中很少遇到,听到这个的时候, 是懵逼的,所以今天就单独抽出章节,对这种内存泄露的方式进行分析

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" image.png

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

静态内部类, 引用外部非静态变量, 错误信息如下: image.png

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 对象引用

image.png

我们修改下代码, 相同的启动类信息 static inner class 静态内部类

static class testInner {
 }

执行结果及Debug,不会出现对象的引用, this$0不存在 image.png

解决方案

  • 不要让其他的地方,持有这个非静态内部类的引用,所有相关业务都在非静态内部类执行
  • 将非静态内部类改为静态内部类。内部类改为静态
  • 变为静态后,它所引用的对象或属性也必须是静态的,所以静态内部类无法获得外部对象的引用

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已使用内存 image.png

  • 堆内存的变化 20M->60M已使用内存,内存上升曲线 斜向上稳定增长,不会回收 image.png

  • 堆内存的变化 6M->8M已使用内存,几乎溢出,但是内存上升曲线 斜向上稳定增长,不会回收 image.png

  • 堆内存达到 100M 设置,出现OOM image.png

  • 老年代 阶梯式增长,不会回收 image.png

  • 年轻代Eden区 会被回收 image.png

-Survivor区也是阶梯增长 image.png

最终导致内存泄漏


本文通过内部类对外部类的引用,分析了内存泄漏的产生过程,我们在实际项目中一定要注意这点,构建内部类的时候一定要谨记 构造成static 静态内部类,防止内存泄漏