谨防JDK8重复类定义形成的内存泄漏

本文来自: PerfMa技术社区

PerfMa(笨马网络)官网算法

概述

现在JDK8成了主流,你们都紧锣密鼓地进行着升级,享受着JDK8带来的各类便利,然而有时候升级并无那么顺利?好比说今天要说的这个问题。咱们都知道JDK8在内存模型上最大的改变是,放弃了Perm,迎来了Metaspace的时代。若是你对Metaspace还不熟,以前我写过一篇介绍Metaspace的文章,你们有兴趣的能够看看我前面的那篇文章。缓存

咱们以前通常在系统的JVM参数上都加了相似-XX:PermSize=256M -XX:MaxPermSize=256M的参数,升级到JDK8以后,由于Perm已经没了,若是还有这些参数JVM会抛出一些警告信息,因而咱们会将参数进行升级,好比直接将PermSize改为MetaspaceSizeMaxPermSize改为MaxMetaspaceSize,可是咱们后面会发现一个问题,常常会看到MetaspaceOutOfMemory异常或者GC日志里提示Metaspace致使的Full GC,此时咱们不得不将MaxMetaspaceSize以及MetaspaceSize调大到512M或者更大,幸运的话,发现问题解决了,后面没再出现OOM,可是有时候也会很不幸,仍然会出现OOM。此时你们是否是很是疑惑了,代码彻底没有变化,可是加载类貌似须要更多的内存?性能优化

以前我其实并无仔细去想这个问题,碰到这类OOM的问题,都以为主要是Metaspace内存碎片的问题,由于以前帮人解决过相似的问题,他们构建了成千上万个类加载器,确实也是由于Metsapce碎片的问题致使的,由于Metaspace并不会作压缩,解决的方案主要是调大MetaspaceSizeMaxMetaspaceSize,并将它们设置相等。而后此次碰到的问题并非这样,类加载个数并很少,然而却抛出了Metaspace的OutOfMemory异常,而且Full GC一直持续着,并且从jstat来看,Metaspace的GC先后使用状况基本不变,也就是GC先后基本没有回收什么内存。网络

经过咱们的内存分析工具看到的现象是同一个类加载器竟然加载了同一个类多遍,内存里有多份类实例,这个咱们能够经过加上-verbose:class的参数也能获得验证,要输出以下日志,那只有在不判定义某个类才会输出,因而想构建出这种场景来,因而简单地写了个demo来验证
image.png工具

Demo

image.png
代码很简单,就是经过反射直接调用ClassLoader的defineClass方法来对某个类作重复的定义。
其中在JDK7下跑的JVM参数设置的是:
image.png
在JDK8下跑的JVM参数是:
image.png
你们能够经过jstat -gcutil <pid> 1000看看JDK7和JDK8下有什么不同,结果你会发现JDK7下Perm的使用率随着FGC的进行GC先后不断发生着变化,而Metsapce的使用率到必定阶段以后GC先后却一直没有变化性能

JDK7下的结果:
image.png
JDK8下的结果:
image.png学习

重复类定义

重复类定义,从上面的Demo里已经获得了证实,当咱们屡次调用ClassLoader的defineClass方法的时候哪怕是同一个类加载器加载同一个类文件,在JVM里也会在对应的Perm或者Metaspace里建立多份Klass结构,固然通常状况下咱们不会直接这么调用,可是反射提供了这么强大的能力,有些人仍是会利用这种写法,其实我想直接这么用的人对类加载的实现机制真的没有全弄明白,包括此次问题发生的场景其实仍是吸纳进JDK里的jaxp/jaxws,好比它就存在这样的代码实现com.sun.xml.bind.v2.runtime.reflect.opt.Injector里的inject方法就存在直接调用的状况:
image.png
不过从2.2.2这个版本开始这种实现就改变了
image.png
因此你们若是仍是使用jaxb-impl-2.2.2如下版本的请注意啦,升级到JDK8可能会存在本文说的问题。测试

重复类定义带来的影响

那重复类定义会带来什么危害呢?正常的类加载都会先走一遍缓存查找,看是否已经有了对应的类,若是有了就直接返回,若是没有就进行定义,若是直接调用类定义的方法,在JVM里会建立多份临时的类结构实例,这些相关的结构是存在Perm或者Metaspace里的,也就是说会消耗Perm或Metaspace的内存,可是这些类在定义出来以后,最终会作一次约束检查,若是发现已经定义了,那就直接抛出LinkageError的异常
image.png
这样这些临时建立的结构,只能等待GC的时候去回收掉了,由于它们不可达,因此在GC的时候会被回收,那问题来了,为何在Perm下能正常回收,可是在Metaspace里不能正常回收呢?优化

Perm和Metaspace在类卸载上的差别

这里我主要拿咱们目前最经常使用的GC算法CMS GC举例。spa

在JDK7 CMS下,Perm的结构其实和Old的内存结构是同样的,若是Perm不够的时候咱们会作一次Full GC,这个Full GC默认状况下是会对各个分代作压缩的,包括Perm,这样一来根据对象的可达性,任何一个类都只会和一个活着的类加载器绑定,在标记阶段将这些类标记成活的,并将他们进行新地址的计算及移动压缩,而以前由于重复定义生成的类结构等,由于没有将它们和任何一个活着的类加载器关联(有个叫作SystemDictionary的Hashtable结构来记录这种关联),从而在压缩过程当中会被回收掉。
image.png
在JDK8下,Metaspace是彻底独立分散的内存结构,由非连续的内存组合起来,在Metaspace达到了触发GC的阈值的时候(和MaxMetaspaceSize及MetaspaceSize有关),就会作一次Full GC,可是此次Full GC,并不会对Metaspace作压缩,惟一卸载类的状况是,对应的类加载器必须是死的,若是类加载器都是活的,那确定不会作卸载的事情了
image.png
从上面贴的代码咱们也能看出来,JDK7里会对Perm作压缩,而后JDK8里并不会对Metaspace作压缩,从而只要和那些重复定义的类相关的类加载一直存活,那将一直不会被回收,可是若是类加载死了,那就会被回收,这是由于那些重复类都是在和这个类加载器关联的内存块里分配的,若是这个类加载器死了,那整块内存会被清理并被下次重用。

如何证实压缩能回收Perm里的重复类

在没看GC源码的状况下,有什么办法来证实Perm在FGC下的回收是由于压缩而致使那些重复类被回收呢?你们能够改改上面的测试用例,将最后那个死循环改一下:
image.png
在System.gc那里设置个断点,而后再经过jstat -gcutil <pid> 1000来看Perm的使用率是否发生变化,另外你再加上-XX:+ ExplicitGCInvokesConcurrent再重复上面的动做,你看看输出是怎样的,为何这个能够证实,你们能够想想,哈哈

一块儿来学习吧

PerfMa KO 系列课之 JVM 参数【Memory篇】

记一次 Java 服务性能优化

相关文章
相关标签/搜索