概述

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

我们之前一般在系统的JVM参数上都加了类似-XX:PermSize=256M -XX:MaxPermSize=256M的参数,升级到JDK8之后,因为Perm已经没了,如果还有这些参数JVM会抛出一些警告信息,于是我们会将参数进行升级,比如直接将PermSize改成MetaspaceSize,MaxPermSize改成MaxMetaspaceSize,但是我们后面会发现一个问题,经常会看到Metaspace的OutOfMemory异常或者GC日志里提示Metaspace导致的Full GC,此时我们不得不将MaxMetaspaceSize以及MetaspaceSize调大到512M或者更大,幸运的话,发现问题解决了,后面没再出现OOM,但是有时候也会很不幸,仍然会出现OOM。此时大家是不是非常疑惑了,代码完全没有变化,但是加载类貌似需要更多的内存?

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

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

[Loaded ResponseVO$JaxbAccessorM_getDescription_setDescription_java_lang_String from __JVM_DefineClass__]
[Loaded ResponseVO$JaxbAccessorM_getDescription_setDescription_java_lang_String from __JVM_DefineClass__]
[Loaded ResponseVO$JaxbAccessorM_getDescription_setDescription_java_lang_String from __JVM_DefineClass__]

Demo

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;/*** Created by nijiaben on 2017/3/7.*/
public class B {public static void main(String args[]) throws Throwable {Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{String.class, byte[].class, int.class, int.class});defineClass.setAccessible(true);File file = new File("/Users/nijiaben/BBBB.class");byte[] bcs = new byte[(int) file.length()];FileInputStream in = null;try {in = new FileInputStream(file);while ((in.read(bcs)) != -1) {}} catch (Exception e) {} finally {if (in != null) {try {in.close();} catch (IOException e) {}}}while (true) {try {defineClass.invoke(B.class.getClassLoader(), new Object[]{"BBBB", bcs, 0, bcs.length});} catch (Throwable e) {}}}
}

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

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

-Xmx100M -Xms100M -verbose:class -XX:+PrintGCDetails -XX:MaxPermSize=50M -XX:PermSize=50M -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled

在JDK8下跑的JVM参数是:

-Xmx100M -Xms100M -verbose:class -XX:+PrintGCDetails -XX:MaxMetaspaceSize=50M -XX:MetaspaceSize=50M -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled

二者区别就在于一个是PermSize,一个是MetaspaceSize

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

JDK7下的结果:

[Full GC[CMS: 0K->346K(68288K), 0.0267620 secs] 12607K->346K(99008K), [CMS Perm : 51199K->3122K(51200K)], 0.0269490 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]

JDK8下的结果:

[Full GC (Metadata GC Threshold) [CMS: 5308K->5308K(68288K), 0.0397720 secs] 5844K->5308K(99008K), [Metaspace: 49585K->49585K(1081344K)], 0.0398189 secs] [Times: user=0.04 sys=0.00, real=0.04 secs]
[Full GC (Last ditch collection) [CMS: 5308K->5308K(68288K), 0.0343949 secs] 5308K->5308K(99008K), [Metaspace: 49585K->49585K(1081344K)], 0.0344473 secs] [Times: user=0.03 sys=0

重复类定义

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

private synchronized Class inject(String className, byte[] image){if (!this.loadable) {return null;}Class c = (Class)this.classes.get(className);if (c == null){try{c = (Class)defineClass.invoke(this.parent, new Object[] { className.replace('/', '.'), image, Integer.valueOf(0), Integer.valueOf(image.length) });resolveClass.invoke(this.parent, new Object[] { c });}catch (IllegalAccessException e){logger.log(Level.FINE, "Unable to inject " + className, e);return null;}catch (InvocationTargetException e){logger.log(Level.FINE, "Unable to inject " + className, e);return null;}catch (SecurityException e){logger.log(Level.FINE, "Unable to inject " + className, e);return null;}catch (LinkageError e){logger.log(Level.FINE, "Unable to inject " + className, e);return null;}this.classes.put(className, c);}return c;}

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

private Class inject(String className, byte[] image){...c = (Class)findLoadedClass.invoke(this.parent, new Object[] { className.replace('/', '.') });...if (c == null){c = (Class)defineClass.invoke(this.parent, new Object[] { className.replace('/', '.'), image, Integer.valueOf(0), Integer.valueOf(image.length) });resolveClass.invoke(this.parent, new Object[] { c })...}}

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

重复类定义带来的影响

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

void SystemDictionary::check_constraints(int d_index, unsigned int d_hash,instanceKlassHandle k,Handle class_loader, bool defining,TRAPS) {const char *linkage_error = NULL;{Symbol*  name  = k->name();ClassLoaderData *loader_data = class_loader_data(class_loader);MutexLocker mu(SystemDictionary_lock, THREAD);Klass* check = find_class(d_index, d_hash, name, loader_data);if (check != (Klass*)NULL) {// if different InstanceKlass - duplicate class definition,// else - ok, class loaded by a different thread in parallel,// we should only have found it if it was done loading and ok to use// system dictionary only holds instance classes, placeholders// also holds array classesassert(check->oop_is_instance(), "noninstance in systemdictionary");if ((defining == true) || (k() != check)) {linkage_error = "loader (instance of  %s): attempted  duplicate class ""definition for name: \"%s\"";} else {return;}}...}

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

Perm和Metaspace在类卸载上的差异

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

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

void GenMarkSweep::mark_sweep_phase4() {// All pointers are now adjusted, move objects accordingly// It is imperative that we traverse perm_gen first in phase4. All// classes must be allocated earlier than their instances, and traversing// perm_gen first makes sure that all klassOops have moved to their new// location before any instance does a dispatch through it's klass!// The ValidateMarkSweep live oops tracking expects us to traverse spaces// in the same order in phase2, phase3 and phase4. We don't quite do that// here (perm_gen first rather than last), so we tell the validate code// to use a higher index (saved from phase2) when verifying perm_gen.GenCollectedHeap* gch = GenCollectedHeap::heap();Generation* pg = gch->perm_gen();GCTraceTime tm("phase 4", PrintGC && Verbose, true, _gc_timer);trace("4");VALIDATE_MARK_SWEEP_ONLY(reset_live_oop_tracking(true));pg->compact();VALIDATE_MARK_SWEEP_ONLY(reset_live_oop_tracking(false));GenCompactClosure blk;gch->generation_iterate(&blk, true);VALIDATE_MARK_SWEEP_ONLY(compaction_complete());pg->post_compact(); // Shared spaces verification.
}

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

void GenMarkSweep::mark_sweep_phase4() {// All pointers are now adjusted, move objects accordingly// It is imperative that we traverse perm_gen first in phase4. All// classes must be allocated earlier than their instances, and traversing// perm_gen first makes sure that all Klass*s have moved to their new// location before any instance does a dispatch through it's klass!// The ValidateMarkSweep live oops tracking expects us to traverse spaces// in the same order in phase2, phase3 and phase4. We don't quite do that// here (perm_gen first rather than last), so we tell the validate code// to use a higher index (saved from phase2) when verifying perm_gen.GenCollectedHeap* gch = GenCollectedHeap::heap();GCTraceTime tm("phase 4", PrintGC && (Verbose || LogCMSParallelFullGC),true, _gc_timer, _gc_tracer->gc_id());trace("4");GenCompactClosure blk;gch->generation_iterate(&blk, true);
}

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

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

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

  int i = 0;while (i++ < 1000) {try {defineClass.invoke(B.class.getClassLoader(), new Object[]{"BBBB", bcs, 0, bcs.length});} catch (Throwable e) {}}System.gc();

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

转载自: 微信公众号你假笨
原文链接:https://zhuanlan.zhihu.com/p/25634935

假笨说-谨防JDK8重复类定义造成的内存泄漏相关推荐

  1. python︱函数、for、if、_name_、迭代器、防范报错、类定义、装饰器、argparse模块、yield

    新手入门python,开始写一些简单函数,慢慢来,加油~ 文章目录 一.函数 +三个内建函数filter,map和reduce + if 1.def/lambda 2.if 如果if + for列表的 ...

  2. python 类中定义类_Python中的动态类定义

    python 类中定义类 Here's a neat Python trick you might just find useful one day. Let's look at how you ca ...

  3. 【C++ 语言】面向对象 ( 类定义 | 限制头文件引用次数 | 构造方法 | 析构方法 )

    文章目录 类的定义 ( 头文件引用次数控制 ) 类的实现 ( 构造函数 | 析构函数 ) CMake 编译配置 类测试 ( 构造析构调用测试 ) 类的定义 ( 头文件引用次数控制 ) 类定义 , 类一 ...

  4. 假笨说-从一起GC血案谈到反射原理

    概述 公司之前有个大内存系统(70G以上)一直使用CMS GC,不过因为该系统对时间很敏感,偶尔会因为gclocker导致remark特别长(虽然加了-XX:+CMSScavReengeBeforeR ...

  5. 【无标题】类模板详解\n类模板的定义及实例化\ntemplate\u003Cclass 模板参数>\nclass 类名 {\n // 类定义\n};\n\ntemplate\u003Ctypen

    类模板详解\n类模板的定义及实例化\ntemplate\u003Cclass 模板参数>\nclass 类名 {\n    // 类定义\n};\n\ntemplate\u003Ctypenam ...

  6. Matlab中的类定义 classdef

    Matlab也可以写面向对象的代码,首先表现在可以定义类,以及可以继承,使用类(class)有很多好处,其中一个重要的好处便是解决变量名冲突和让函数.对象的结构清晰.class的static func ...

  7. C++ 笔记(16)— 类和对象(类定义、类实例对象定义、访问类成员、类成员函数、类 public/private/protected 成员、类对象引用和指针)

    1. 类的定义 类定义是以关键字 class 开头,后跟类的名称.并在它后面依次包含类名,一组放在 {} 内的成员属性和成员函数,以及结尾的分号. 类声明将类本身及其属性告诉编译器.类声明本身并不能改 ...

  8. java受保护的数据与_Javascript类定义语法,私有成员、受保护成员、静态成员等介绍...

    摘要:这篇JavaScript栏目下的"Javascript类定义语法,私有成员.受保护成员.静态成员等介绍",介绍的技术点是"javascript类.JavaScrip ...

  9. 数据结构课本学习 --单链表类定义

    单链表的类定义: 复合类: class List;class LinkNode{ //结点类定义 friend class List; //List类所有成员可以使用LinkNode的私有成员,反过来 ...

最新文章

  1. bzoj4830 hnoi2017 抛硬币
  2. 项目通用环境使用说明
  3. 从函数劫持角度看开发调试工具AlloyLever
  4. CRM 里面table download to excel的实现
  5. 信号与系统 chapter9 关于信号与系统中微分方程的求解
  6. 什么是罗技LogitechFlow技术
  7. 分贝测试软件哪个好 家庭影院,家庭影院5.1和7.1哪种好?5.1和7.1家庭影院区别...
  8. CentOS7安装无线网卡驱动和更新yum源
  9. 创建一个带目录的Word模板
  10. 统计推断——假设检验——方差分析之多重比较(LSD法、Sidak法、Bonferroni法、Dunnett法、Tukey法、SNK 法、Duncan法)
  11. 新媒传信Java_新媒小课堂——多媒体、流媒体、富媒体
  12. 【摘抄】领导力21法则-约翰·C·马克斯韦尔
  13. 微服务网关Gateway基本知识(一)
  14. android图片压缩的两个开源库
  15. 云计算基础架构 (三)Mongodb
  16. pdf转换成txt转换器1.2详细教程
  17. 前端-JS基础之常用内置对象
  18. Tasker 手机短信自动转发
  19. [Delphi]将一个窗体Form1嵌入另一个窗体Form2
  20. 基于ffmpeg+sdl+opencv的windows控制台,字符串播放badapple

热门文章

  1. Bootstrap全局css样式_图片,响应式工具
  2. 编程开发使用的辅助软件大全
  3. 210213阶段三回顾
  4. Allegro 关闭与显示网络飞线
  5. 新手学习Linux——rsync+shell脚本完成自动化备份
  6. AsyncTask实现断点续传
  7. Lync在Internet上无法登录问题之一
  8. c++中std::set自定义去重和排序函数
  9. 牛客多校2 - Greater and Greater(bitset优化暴力)
  10. CodeForces - 1307C Cow and Message(思维)