• Java内存泄露的例子

    发布:51Code 时间: 2017-12-12 13:10

  • 在定位JVM性能问题时可能会遇到内存泄露导致JVM OutOfMemory的情况,在使用Tomcat容器时如果设置了reloadable=true这个参数,在频繁热部署应用时也有可能会遇到内存溢出的情况。Tomcat的热部署...

  • 在定位JVM性能问题时可能会遇到内存泄露导致JVM OutOfMemory的情况,在使用Tomcat容器时如果设置了reloadable=”true”这个参数,在频繁热部署应用时也有可能会遇到内存溢出的情况。Tomcat的热部署原理是检测到WEB-INF/classes或者WEB-INF/lib目录下的文件发生了变更后会把应用先停止然后再启动,由于Tomcat默认给每个应用分配一个WebAppClassLoader,热替换的原理就是创建一个新的ClassLoader来加载类,由于JVM中一个类的唯一性由它的class文件和它的类加载器来决定,因此重新加载类可以达到热替换的目的。当热部署的次数比较多会导致JVM加载的类比较多,如果之前的类由于某种原因(比如内存泄露)没有及时卸载就可能导致永久代或者MetaSpace的OutOfMemory。这篇文章通过一个Demo来简要介绍下ThreadLocal和ClassLoader导致内存泄露最终OutOfMemory的场景。

    类的卸载

    在类使用完之后,满足下面的情形,会被卸载:

    1、该类在堆中的所有实例都已被回收,即在堆中不存在该类的实例对象。

    2、加载该类的classLoader已经被回收。

    3、该类对应的Class对象没有任何地方可以被引用,通过反射访问不到该Class对象。

    如果类满足卸载条件,JVM就在GC的时候,对类进行卸载,即在方法区清除类的信息。

    场景介绍

    上一篇文章我介绍了ThreadLocal的原理,每个线程有个ThreadLocalMap,如果线程的生命周期比较长可能会导致ThreadLocalMap里的Entry没法被回收,那ThreadLocal的那个对象就一直被线程持有强引用,由于实例对象会持有Class对象的引用,Class对象又会持有加载它的ClassLoader的引用,这样就会导致Class无法被卸载了,当加载的类足够多时就可能出现永久代或者MetaSpace的内存溢出,如果该类有大对象,比如有比较大的字节数组,会导致Java堆区的内存溢出。

    源码介绍

    这里定义了一个内部类Inner,Inner类有个静态的ThreadLocal对象,主要用于让线程持有Inner类的强引用导致Inner类无法被回收,定义了一个自定义的类加载器去加载Inner类,如下所示:

    public class MemoryLeak {

    public static void main(String[] args) {

    //由于线程一直在运行,因此ThreadLocalMap里的Inner对象一直被Thread对象强引用

    new Thread(new Runnable() {

    @Override

    public void run() {

    while (true) {

    //每次都新建一个ClassLoader实例去加载Inner类

    CustomClassLoader classLoader = new CustomClassLoader

    ("load1", MemoryLeak.class.getClassLoader(), "com.ezlippi.MemoryLeak$Inner", "com.ezlippi.MemoryLeak$Inner$1");

    try {

    Class<?> innerClass = classLoader.loadClass("com.ezlippi.MemoryLeak$Inner");

    innerClass.newInstance();

    //帮助GC进行引用处理

    innerClass = null;

    classLoader = null;

    Thread.sleep(10);

    } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | InterruptedException e) {

    e.printStackTrace();

    }

    }

    }

    }).start();

    }

    //为了更快达到堆区

    public static class Inner {

    private  byte[] MB = new byte[1024 * 1024];

    static ThreadLocal<Inner> threadLocal = new ThreadLocal<Inner>() {

    @Override

    protected Inner initialValue() {

    return new Inner();

    }

    };

    //调用ThreadLocal.get()才会调用initialValue()初始化一个Inner对象

    static {

    threadLocal.get();

    }

    public Inner() {

    }

    }

    //源码省略

    private static class CustomClassLoader extends ClassLoader {}

    堆区内存溢出

    为了触发堆区内存溢出,我在Inner类里面设置了一个1MB的字节数组,同时要在静态块中调用threadLocal.get(),只有调用才会触发initialValue()来初始化一个Inner对象,不然只是创建了一个空的ThreadLocal对象,ThreadLocalMap里并没有数据。

    JVM参数如下:

    -Xms100m -Xmx100m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintClassHistogram -XX:+HeapDumpOnOutOfMemoryError

    最后执行了814次后JVM堆区内存溢出了,如下所示:

    java.lang.OutOfMemoryError: Java heap space

    Dumping heap to java_pid11824.hprof ...

    Heap dump file created [100661202 bytes in 1.501 secs]

    Heap

    par new generation   total 30720K, used 30389K [0x00000000f9c00000, 0x00000000fbd50000, 0x00000000fbd50000)

    eden space 27328K,  99% used [0x00000000f9c00000, 0x00000000fb6ad450, 0x00000000fb6b0000)

    from space 3392K,  90% used [0x00000000fb6b0000, 0x00000000fb9b0030, 0x00000000fba00000)

    to   space 3392K,   0% used [0x00000000fba00000, 0x00000000fba00000, 0x00000000fbd50000)

    concurrent mark-sweep generation total 68288K, used 67600K [0x00000000fbd50000, 0x0000000100000000, 0x0000000100000000)

    Metaspace       used 3770K, capacity 5134K, committed 5248K, reserved 1056768K

    class space    used 474K, capacity 578K, committed 640K, reserved 1048576K

    Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space

    at com.ezlippi.MemoryLeak$Inner.<clinit>(MemoryLeak.java:34)

    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)

    at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)

    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)

    at java.lang.reflect.Constructor.newInstance(Unknown Source)

    at java.lang.Class.newInstance(Unknown Source)

    at com.ezlippi.MemoryLeak$1.run(MemoryLeak.java:20)

    at java.lang.Thread.run(Unknown Source)

    可以看到JVM已经没有内存来创建新的Inner对象,因为堆区存放了很多个1MB的字节数组,这里我把类的直方图打印出来了(下图是堆大小为1024M的场景),省略了一些无关紧要的类,可以看出字节数组占了855M的空间,创建了814个 com.ezlippi.MemoryLeak$CustomClassLoader 的实例,和字节数组的大小基本吻合:

    num     #instances         #bytes  class name

    ----------------------------------------------

    1:          6203      855158648  [B

    2:         13527        1487984  [C

    3:           298         700560  [I

    4:          2247         228792  java.lang.Class

    5:          8232         197568  java.lang.String

    6:          3095         150024  [Ljava.lang.Object;

    7:          1649         134480  [Ljava.util.HashMap$Node;

    11:           813          65040  com.ezlippi.MemoryLeak$CustomClassLoader

    12:           820          53088  [Ljava.util.Hashtable$Entry;

    15:           817          39216  java.util.Hashtable

    16:           915          36600  java.lang.ref.SoftReference

    17:           543          34752  java.net.URL

    18:           697          33456  java.nio.HeapCharBuffer

    19:           817          32680  java.security.ProtectionDomain

    20:           785          31400  java.util.TreeMap$Entry

    21:           928          29696  java.util.Hashtable$Entry

    22:          1802          28832  java.util.HashSet

    23:           817          26144  java.security.CodeSource

    24:           814          26048  java.lang.ThreadLocal$ThreadLocalMap$Entry

    Metaspace溢出

    为了让Metaspace溢出,那就必须把MetaSpace的空间调小一点,要在堆溢出之前加载足够多的类,因此我调整了下JVM参数,并且把字节数组的大小调成了1KB,如下所示:

    private  byte[] KB = new byte[1024];

    -Xms100m -Xmx100m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintClassHistogram -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m

    从GC日志可以看出在Meraspace达到GC阈值(也就是MaxMetaspaceSize配置的大小时)会触发一次FullGC:

    java.lang.OutOfMemoryError: Metaspace

    <<no stack trace available>>

    {Heap before GC invocations=20 (full 20):

    par new generation   total 30720K, used 0K [0x00000000f9c00000, 0x00000000fbd50000, 0x00000000fbd50000)

    eden space 27328K,   0% used [0x00000000f9c00000, 0x00000000f9c00000, 0x00000000fb6b0000)

    from space 3392K,   0% used [0x00000000fb6b0000, 0x00000000fb6b0000, 0x00000000fba00000)

    to   space 3392K,   0% used [0x00000000fba00000, 0x00000000fba00000, 0x00000000fbd50000)

    concurrent mark-sweep generation total 68288K, used 432K [0x00000000fbd50000, 0x0000000100000000, 0x0000000100000000)

    Metaspace       used 1806K, capacity 1988K, committed 2048K, reserved 1056768K

    class space    used 202K, capacity 384K, committed 384K, reserved 1048576K

    [Full GC (Metadata GC Threshold) [CMS

    Process finished with exit code 1

    通过上面例子可以看出如果类加载器和ThreadLocal使用的不当确实会导致内存泄露的问题,完整的源码在 github

    本文原作者:Lippi-浮生志

    文章来源:推酷


  • 上一篇:深度解析Java线程池的异常处理机制

    下一篇:10个JavaScript常见BUG及修复方法

相关资讯
网站导航
Copyright(C)51Code软件开发网 2003-2018 , 沪ICP备16012939号-1