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

简介: 谨防JDK8重复类定义造成的内存泄漏

概述


如今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来验证


1.jpg


Demo


2.jpg

代码很简单,就是通过反射直接调用ClassLoader的defineClass方法来对某个类做重复的定义。


其中在JDK7下跑的JVM参数设置的是:

3.jpg


在JDK8下跑的JVM参数是:

4.jpg

大家可以通过jstat -gcutil <pid> 1000看看JDK7和JDK8下有什么不一样,结果你会发现JDK7下Perm的使用率随着FGC的进行GC前后不断发生着变化,而Metsapce的使用率到一定阶段之后GC前后却一直没有变化


JDK7下的结果:

5.jpg


JDK8下的结果:

6.jpg


重复类定义


重复类定义,从上面的Demo里已经得到了证明,当我们多次调用ClassLoader的defineClass方法的时候哪怕是同一个类加载器加载同一个类文件,在JVM里也会在对应的Perm或者Metaspace里创建多份Klass结构,当然一般情况下我们不会直接这么调用,但是反射提供了这么强大的能力,有些人还是会利用这种写法,其实我想直接这么用的人对类加载的实现机制真的没有全弄明白,包括这次问题发生的场景其实还是吸纳进JDK里的jaxp/jaxws,比如它就存在这样的代码实现com.sun.xml.bind.v2.runtime.reflect.opt.Injector里的inject方法就存在直接调用的情况:


7.jpg


不过从2.2.2这个版本开始这种实现就改变了

8.jpg


所以大家如果还是使用jaxb-impl-2.2.2以下版本的请注意啦,升级到JDK8可能会存在本文说的问题。


重复类定义带来的影响


那重复类定义会带来什么危害呢?正常的类加载都会先走一遍缓存查找,看是否已经有了对应的类,如果有了就直接返回,如果没有就进行定义,如果直接调用类定义的方法,在JVM里会创建多份临时的类结构实例,这些相关的结构是存在Perm或者Metaspace里的,也就是说会消耗Perm或Metaspace的内存,但是这些类在定义出来之后,最终会做一次约束检查,如果发现已经定义了,那就直接抛出LinkageError的异常

9.jpg


这样这些临时创建的结构,只能等待GC的时候去回收掉了,因为它们不可达,所以在GC的时候会被回收,那问题来了,为什么在Perm下能正常回收,但是在Metaspace里不能正常回收呢?


Perm和Metaspace在类卸载上的差异


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


在JDK7 CMS下,Perm的结构其实和Old的内存结构是一样的,如果Perm不够的时候我们会做一次Full GC,这个Full GC默认情况下是会对各个分代做压缩的,包括Perm,这样一来根据对象的可达性,任何一个类都只会和一个活着的类加载器绑定,在标记阶段将这些类标记成活的,并将他们进行新地址的计算及移动压缩,而之前因为重复定义生成的类结构等,因为没有将它们和任何一个活着的类加载器关联(有个叫做SystemDictionary的Hashtable结构来记录这种关联),从而在压缩过程中会被回收掉。

10.jpg


在JDK8下,Metaspace是完全独立分散的内存结构,由非连续的内存组合起来,在Metaspace达到了触发GC的阈值的时候(和MaxMetaspaceSize及MetaspaceSize有关),就会做一次Full GC,但是这次Full GC,并不会对Metaspace做压缩,唯一卸载类的情况是,对应的类加载器必须是死的,如果类加载器都是活的,那肯定不会做卸载的事情了

11.jpg


从上面贴的代码我们也能看出来,JDK7里会对Perm做压缩,然后JDK8里并不会对Metaspace做压缩,从而只要和那些重复定义的类相关的类加载一直存活,那将一直不会被回收,但是如果类加载死了,那就会被回收,这是因为那些重复类都是在和这个类加载器关联的内存块里分配的,如果这个类加载器死了,那整块内存会被清理并被下次重用。


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


在没看GC源码的情况下,有什么办法来证明Perm在FGC下的回收是因为压缩而导致那些重复类被回收呢?大家可以改改上面的测试用例,将最后那个死循环改一下:

12.jpg


在System.gc那里设置个断点,然后再通过jstat -gcutil <pid> 1000来看Perm的使用率是否发生变化,另外你再加上-XX:+ ExplicitGCInvokesConcurrent再重复上面的动作,你看看输出是怎样的,为什么这个可以证明,大家可以想一想,哈哈

相关文章
|
2月前
|
Java Linux
java基础(3)安装好JDK后使用javac.exe编译java文件、java.exe运行编译好的类
本文介绍了如何在安装JDK后使用`javac.exe`编译Java文件,以及使用`java.exe`运行编译好的类文件。涵盖了JDK的安装、环境变量配置、编写Java程序、使用命令行编译和运行程序的步骤,并提供了解决中文乱码的方法。
50 2
|
1月前
|
存储 安全 Java
jdk21的外部函数和内存API(MemorySegment)(官方翻译)
本文介绍了JDK 21中引入的外部函数和内存API(MemorySegment),这些API使得Java程序能够更安全、高效地与JVM外部的代码和数据进行互操作,包括调用外部函数、访问外部内存,以及使用不同的Arena竞技场来分配和管理MemorySegment。
28 1
jdk21的外部函数和内存API(MemorySegment)(官方翻译)
|
11天前
|
监控 数据可视化 Java
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
|
1月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(二)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
2月前
|
安全 Java API
【性能与安全的双重飞跃】JDK 22外部函数与内存API:JNI的继任者,引领Java新潮流!
【9月更文挑战第7天】JDK 22外部函数与内存API的发布,标志着Java在性能与安全性方面实现了双重飞跃。作为JNI的继任者,这一新特性不仅简化了Java与本地代码的交互过程,还提升了程序的性能和安全性。我们有理由相信,在外部函数与内存API的引领下,Java将开启一个全新的编程时代,为开发者们带来更加高效、更加安全的编程体验。让我们共同期待Java在未来的辉煌成就!
62 11
|
1月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(三)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
1月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(一)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
2月前
|
安全 Java API
【本地与Java无缝对接】JDK 22外部函数和内存API:JNI终结者,性能与安全双提升!
【9月更文挑战第6天】JDK 22的外部函数和内存API无疑是Java编程语言发展史上的一个重要里程碑。它不仅解决了JNI的诸多局限和挑战,还为Java与本地代码的互操作提供了更加高效、安全和简洁的解决方案。随着FFM API的逐渐成熟和完善,我们有理由相信,Java将在更多领域展现出其强大的生命力和竞争力。让我们共同期待Java编程新纪元的到来!
88 11
|
2月前
|
监控 Java 大数据
【Java内存管理新突破】JDK 22:细粒度内存管理API,精准控制每一块内存!
【9月更文挑战第9天】虽然目前JDK 22的确切内容尚未公布,但我们可以根据Java语言的发展趋势和社区的需求,预测细粒度内存管理API可能成为未来Java内存管理领域的新突破。这套API将为开发者提供前所未有的内存控制能力,助力Java应用在更多领域发挥更大作用。我们期待JDK 22的发布,期待Java语言在内存管理领域的持续创新和发展。
|
2月前
|
监控 数据可视化 Java
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况