当前位置: 移动技术网 > IT编程>开发语言>Java > 详解Java内存泄露的示例代码

详解Java内存泄露的示例代码

2019年07月19日  | 移动技术网IT编程  | 我要评论

在定位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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网