百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

一起 FastJSON 和 Spring-Mongo联合作妖的类卸载事故排查

suiw9 2024-11-17 15:49 21 浏览 0 评论

问题背景

有同学反馈,在自己的业务中调用 groovy 脚本动态生成一些 class 的时候,出现了类无法卸载的现象,下图来自你假笨大神 PerfMa 公司 的 XElephant 「 memory.console.heapdump.cn/ 」

如果想离线分析也可以用 JProfile(付费)、YourKit 等工具。

可以看到有 4808 个 classloader,这些 classloader 加载的类总数是 9612,加载的类其中一个是我们 groovy 中定义的 com.yuping.app214c2d6e_8f0e_209a_7cbf_81130c799181.bookDataModel 类。

这些类都无法被 GC 卸载。对应的启动参数如下:

java -Xmx2688M -Xms2688M -Xmn960M 
-XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M   
-XX:+UseConcMarkSweepGC 
-XX:+UseCMSInitiatingOccupancyOnly 
-XX:CMSInitiatingOccupancyFraction=70 
-XX:+CMSClassUnloadingEnabled 
-XX:+ParallelRefProcEnabled 
-XX:+CMSScavengeBeforeRemark 
-XX:ErrorFile=/tmp/hs_err_pid%p.log   
-Xloggc:/tmp/gc.log 
-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-verbose:class 
-XX:+PrintClassHistogramBeforeFullGC 
-XX:+PrintClassHistogramAfterFullGC 
-XX:+PrintCommandLineFlags 
-XX:+PrintHeapAtGC 
-XX:-DisableExplicitGC 
-jar  target/groovy-demo-project-1.0-SNAPSHOT.jar
复制代码

经查看,这个参数是允许 CMS 类卸载的。

业务逻辑

大致的逻辑如下,就是从 db 中动态加载一段 groovy 脚本

@Service
public class MyService {
    @Resource
    private MongoTemplate mongoTemplate;

    void insert() {

        GroovyClassLoader groovyClassLoader = null;
        try {
            groovyClassLoader = new GroovyClassLoader();

            Class<? extends BaseClazz> dataModelClazz = groovyClassLoader.parseClass("groovy content");
            // 真实业务是这个 data 是从外部传进来的,有数据的数据结构,这里简化处理
            JSONObject data = new JSONObject(); 
            data.put("id", UUID.randomUUID().toString());
            data.put("enterpriseCode", "foo");

            BaseClazz model = JSON.toJavaObject(data, dataModelClazz);
            BaseClazz newModel = mongoTemplate.insert(model, "test_ya");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            groovyClassLoader.clearCache();
        }
    }

}

groovy 脚本的内容大概如下,是一个简单的子类定义:

package com.yuping.app214c2d6e_8f0e_209a_7cbf_81130c799181

import com.imdach.demo.BaseClazz

class bookDataModel extends BaseClazz {
    String author
    String charter
    // ... 省略很多字段和方法
}

拿到这个问题的时候,第一个我想的是类卸载的条件到底是什么。

  • 首先第一个要求是「这个类的所有实例(instance)不可达、被 GC」,不然实例还在,类没了,就好比人没有了灵魂,是不行的。
  • 第二个要求是该类的 ClassLoader 不可达、被 GC,这也好理解,ClassLoader 需要持有 Class 的引用,不然无法判断一个类是否已经加载,无法实现类加载基本的功能。
  • 第三个要求,没有被其它 GC Root 引用,这个好理解,这个对所有的场景都适用,可达对象不应该被回收。
  • 第四个要求:触发 GC(FullGC),类卸载的场景是比较少见的,以 CMS 为例,类卸载在 FullGC 时触发。

现在来看上面的条件,第一个条件类实例不可达,这个比较显而易见,这里的类实例都是局部变量,函数调用完就不可达了。

第二个条件 ClassLoader 不可达,这个在这个场景下是 OK 的,每次加载 groovy 脚本都是新建的 ClassLoader,调用完就可以被 GC 了。

第三个条件 没有被其它 GC Root 引用,这个目前无法确定,晚点 dump 内存来看。

第四个条件,触发 GC(FullGC),这个也可以排除,已经手动触发过,且在 dump 堆内存时候本来会触发一次 FullGC。

所以接下来就是看这个 class 有没有被 GC Root 引用。

对象被谁引用

我们找到其中一个类,比如第一个,它的地址是 0x79357f308

接下来,切换到「对象视图」界面,通过对象地址找到这个对象,找到这个对象更详细的信息。

首先看到了 fastjson 库,这货咋掺和进来了,我不就是调用了你这个工具人做了一下序列化吗?

可以看到 com.yuping.app214c2d6e_8f0e_209a_7cbf_81130c799181.bookDataModel 被 com.alibaba.fastjson.util.IdentityHashMap$Entry 引用,看名字也可以猜到,bookDataModel 类被放到了 fastjson 的一个 hashmap 里了。

为啥会被放到 hashmap 里,看看它做了什么骚操作。全局搜索一下 IdentityHashMap 被什么引用,看到被 SerializeConfig、ParserConfig 引用,ParserConfig 里面有一个 static 的 IdentityHashMap 字段 global,后面的调用都是用 static 变量,这个 static 的类变量不会被 GC。

public class ParserConfig {
    public static ParserConfig getGlobalInstance() {
        return global;
    }
     public static ParserConfig                              global                = new ParserConfig();

    private final IdentityHashMap<Type, ObjectDeserializer> deserializers         = new IdentityHashMap<Type, ObjectDeserializer>();

FastJson 做解析的过程中,会把 com.yuping.app214c2d6e_8f0e_209a_7cbf_81130c799181.bookDataModel 类放到 IdentityHashMap 中,这下凉凉,global 这个 GC Root 持有 deserializers 这个 IdentityHashMap,IdentityHashMap 里面存放了 bookDataModel 类。

到了这一步,我们可以先把 FastJson 的问题先解决了,我找了一下,它有一个手动清空的函数

public class ParserConfig {

    public void clearDeserializers() {
        this.deserializers.clear();
        this.initDeserializers();
    }
}

这样我就可以把那个 hashmap 清空了,这样也就不持有那个类的引用了。

@Service
public class MyService {
    @Resource
    private MongoTemplate mongoTemplate;

    void insert() {

        GroovyClassLoader groovyClassLoader = null;
        try {
            //省略
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            groovyClassLoader.clearCache();
            // 增加下面这两行
            SerializeConfig.getGlobalInstance().clearSerializers();
            ParserConfig.getGlobalInstance().clearDeserializers();
        }
    }
}

本以为问题就解决了,放心的让开发同学去改一下,然后就等着说「问题解决了」,结果说,类还是没有卸载,啪啪啪打脸。

二战类卸载

再次让开发的小姐姐帮忙 dump 了内存,接下来继续上面的流程,发现确实类还在被其它对象引用,只不过这次已经没有 FastJson 了,这次多了很多 Spring 相关的信息。

可以看到 bookDataModel 类被 org.springframework.data.util.ClassTypeInformation 对象的 type 字段引用,ClassTypeInformation 类的定义如下。

public class ClassTypeInformation<S> extends TypeDiscoverer<S> {
	private final Class<S> type;
}

这里的 type 字段存的就是我们 groovy 生成的 bookDataModel 类 class。

展开其中一个 org.springframework.data.util.ClassTypeInformation, 往上层查看 GC 链。

可以看到 ClassTypeInformation 对象被 MongoMappingContext 对象的 persistentEntities 字段所引用。

public abstract class AbstractMappingContext {
	private final Map<TypeInformation<?>, Optional<E>> persistentEntities = new HashMap<>();
}

public class MongoMappingContext extends AbstractMappingContext {
}

因为 MongoMappingContext 是长期存在的 Spring 单例 Bean,所以 persistentEntities 不会被 GC,它引用 ClassTypeInformation,ClassTypeInformation 引用 bookDataModel 类,导致 bookDataModel 类无法被回收。

到这里,我们就比较清楚了原因。至于这么解决,这个我就不太懂了,需要熟悉 spring-mongodb 的同学看下怎么绕过 spring 里的这套缓存机制,重新定制一个 AbstractMongoConfiguration,让 Spring 不缓存即可(我不会)。

我这里有一个很不成熟的解法,直接用裸的 mongodb-java-driver,经测试是 OK 的,但是不推荐。

@Service
public class MyService {
    @Resource
    private MongoTemplate mongoTemplate;

    void insert() {

        GroovyClassLoader groovyClassLoader = null;
        try {
            groovyClassLoader = new GroovyClassLoader();
            File f = new File("test.groovy");
            Class<? extends BaseClazz> dataModelClazz = groovyClassLoader.parseClass(FileUtils.readFileToString(f));
            JSONObject data = new JSONObject();
            data.put("id", UUID.randomUUID().toString());
            data.put("enterpriseCode", "foo");
            BaseClazz model = JSON.toJavaObject(data, dataModelClazz);

            CodecRegistry pojoCodecRegistry = fromProviders(PojoCodecProvider.builder().automatic(true).build());
            CodecRegistry codecRegistry = fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), pojoCodecRegistry);
            MongoClientSettings clientSettings = MongoClientSettings.builder()
                    .applyConnectionString(new ConnectionString("mongodb://localhost:27017"))
                    .codecRegistry(codecRegistry)
                    .build();

            try (MongoClient mongoClient = MongoClients.create(clientSettings)) {
                MongoDatabase mongoDatabase = mongoClient.getDatabase("seewo_easi_pass");
                MongoCollection collection = mongoDatabase.getCollection("test_ya", dataModelClazz);
                InsertOneResult result = collection.insertOne(model);
                System.out.println(result);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            groovyClassLoader.clearCache();
            SerializeConfig.getGlobalInstance().clearSerializers();
            ParserConfig.getGlobalInstance().clearDeserializers();
        }
    }
}

经过实验,GC 过后确实可以将类卸载,通过对内存 dump 查看,也找不到相关的类存在。

小结

后面我大概搜了一下,关于 FastJson IdentityHashMap 有关的内存问题网友们也遇到过不少,看来大家踩的坑还不少。至于 MongoDB 这个是真没有想到会遇到,可能作者也没有想到,还会有人动态生成类和对应的类实例,然后插入 mongodb 吧。

能复现的问题,其实都不是问题,解决只是一个时间问题。上面的解决思路,可能都是错的,看看思路就好。


作者:挖坑的张师傅
链接:https://juejin.cn/post/7075710935251451911

相关推荐

俄罗斯的 HTTPS 也要被废了?(俄罗斯网站关闭)

发布该推文的ScottHelme是一名黑客,SecurityHeaders和ReportUri的创始人、Pluralsight作者、BBC常驻黑客。他表示,CAs现在似乎正在停止为俄罗斯域名颁发...

如何强制所有流量使用 HTTPS一网上用户

如何强制所有流量使用HTTPS一网上用户使用.htaccess强制流量到https的最常见方法可能是使用.htaccess重定向请求。.htaccess是一个简单的文本文件,简称为“.h...

https和http的区别(https和http有何区别)

“HTTPS和HTTP都是数据传输的应用层协议,区别在于HTTPS比HTTP安全”。区别在哪里,我们接着往下看:...

快码住!带你十分钟搞懂HTTP与HTTPS协议及请求的区别

什么是协议?网络协议是计算机之间为了实现网络通信从而达成的一种“约定”或“规则”,正是因为这个“规则”的存在,不同厂商的生产设备、及不同操作系统组成的计算机之间,才可以实现通信。简单来说,计算机与网络...

简述HTTPS工作原理(简述https原理,以及与http的区别)

https是在http协议的基础上加了一层SSL(由网景公司开发),加密由ssl实现,它的目的是为用户提供对网站服务器的身份认证(需要CA),以至于保护交换数据的隐私和完整性,原理如图示。1、客户端发...

21、HTTPS 有几次握手和挥手?HTTPS 的原理什么是(高薪 常问)

HTTPS是3次握手和4次挥手,和HTTP是一样的。HTTPS的原理...

一次安全可靠的通信——HTTPS原理

为什么HTTPS协议就比HTTP安全呢?一次安全可靠的通信应该包含什么东西呢,这篇文章我会尝试讲清楚这些细节。Alice与Bob的通信...

为什么有的网站没有使用https(为什么有的网站点不开)

有的网站没有使用HTTPS的原因可能涉及多个方面,以下是.com、.top域名的一些见解:服务器性能限制:HTTPS使用公钥加密和私钥解密技术,这要求服务器具备足够的计算能力来处理加解密操作。如果服务...

HTTPS是什么?加密原理和证书。SSL/TLS握手过程

秘钥的产生过程非对称加密...

图解HTTPS「转」(图解http 完整版 彩色版 pdf)

我们都知道HTTPS能够加密信息,以免敏感信息被第三方获取。所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。...

HTTP 和 HTTPS 有何不同?一文带你全面了解

随着互联网时代的高速发展,Web服务器和客户端之间的安全通信需求也越来越高。HTTP和HTTPS是两种广泛使用的Web通信协议。本文将介绍HTTP和HTTPS的区别,并探讨为什么HTTPS已成为We...

HTTP与HTTPS的区别,详细介绍(http与https有什么区别)

HTTP与HTTPS介绍超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的...

一文让你轻松掌握 HTTPS(https详解)

一文让你轻松掌握HTTPS原文作者:UC国际研发泽原写在最前:欢迎你来到“UC国际技术”公众号,我们将为大家提供与客户端、服务端、算法、测试、数据、前端等相关的高质量技术文章,不限于原创与翻译。...

如何在Spring Boot应用程序上启用HTTPS?

HTTPS是HTTP的安全版本,旨在提供传输层安全性(TLS)[安全套接字层(SSL)的后继产品],这是地址栏中的挂锁图标,用于在Web服务器和浏览器之间建立加密连接。HTTPS加密每个数据包以安全方...

一文彻底搞明白Http以及Https(http0)

早期以信息发布为主的Web1.0时代,HTTP已可以满足绝大部分需要。证书费用、服务器的计算资源都比较昂贵,作为HTTP安全扩展的HTTPS,通常只应用在登录、交易等少数环境中。但随着越来越多的重要...

取消回复欢迎 发表评论: