Spring + Jackson有可能导致类数量爆炸

上周四线上的一个应用突然假死状态,通过gc log发现正在频繁的full gc,从gc log上可以看出是Metaspace导致频繁full gc(实际上这个时候Metaspace还没满,还有几十M的剩余),然后用jmap -histo <pid>发现有大量的GeneratedConstructorAccessorXXX和GeneratedMethodAccessorXXX类型的对象(总共有两万多个)。

熟悉java反射的同学应该知道,这两个类都是在反射过程中调用Method.invoke产生的,前者顾名思义就是调用构造函数产生的,后者就是调用普通的方法产生的,生成这个类的代码在:MethodAccessorGenerator

private static synchronized String generateName(boolean isConstructor,boolean forSerialization){if (isConstructor) {if (forSerialization) {int num = ++serializationConstructorSymnum;return "jdk/internal/reflect/GeneratedSerializationConstructorAccessor" + num;else {int num = ++constructorSymnum;return "jdk/internal/reflect/GeneratedConstructorAccessor" + num;}else {int num = ++methodSymnum;return "jdk/internal/reflect/GeneratedMethodAccessor" + num;}}

看到大量的这两个类,第一个念头是谁使用反射没有复用Method对象吧(意思是,我们如果使用反射的时候,如果这个反射会频繁的调用,那么不要每次都去拿Method,最好将method缓存着,不然成本会很高的)。有了这个念头后,我就想如果能把这个类代码dump下来,然后统计一下看看,是不是有些方法对应有很多的GeneratedMethodAccessor,那就基本上知道是什么方法的反射导致的了,然后翻翻代码基本上能确定原因。

正好github上有人写了个工具,可以将class的字节码dump出来:dumpclass

java -jar dumpclass.jar -p <pid> sun.reflect.GeneratedMethodAccessor*

这样就可以将所有的这样的类的字节码全部dump出来了,有类字节码然后我们使用javap对字节码进行反编译,然后就可以进行统计了。遗憾的是,最后发现几乎没有两个不同的GeneratedMethodAccessor类属于同一个方法反射生成的。线索到这里就断了,那说明不是因为没有缓存Method导致的,而是正常的反射使用。然后我随机的看了几个GeneratedMethodAccessor类的字节码,所有的方法基本上都是getter,那这说明什么呢?一般如果是业务代码里使用反射,大部分是调用一个业务方法,而什么会去调用getter呢?比如序列化,比如json。这个时候我正好看到这个应用里有很多地方打印日志,而打印日志的时候是直接使用Jackson将对象序列化之后打印,这里使用的Jackson序列化工具是公共包(common-api)里用Jackson封装的一个工具类。

然后我就怀疑是不是这个应用记录日志太多了,然后Jackson序列化的时候会反射调用对象的getter,然后产生大量的这个GeneratedMethodAccessor类呢?于是我就自己写了个简单的测试,我发现公共包的这个工具类并不会去调用Method.invoke,这就奇怪了,不是Jackson导致的?

不过正好这个时候,我在GeneratedMethodAccessor字节码里除了看到了getter,也看到了setter方法。有了setter方法,那说明应该不是打印日志导致的,打印日志只会序列化不会反序列化,线索再次中断。

然后我就从GeneratedMethodAccessor的字节码里找到一个类,然后查找引用,最后找到了Spring的Controller这一层。这个应用提供了非常非常多的HTTP API,然后这些Controller基本上都是返回对象的方式返回响应,并且Controller的参数接受的也是对象,类似下面这样的:

@RequestMapping(value = "/api/web/businessInfo/list/v2", method = RequestMethod.POST)@JsonResponseBody//返回BusinessInfoGridVO对象,接受BusinessInfoListVO对象public Pager<BusinessInfoGridVO> BusinessInfoListV2(@RequestBody BusinessInfoListVO infoListVO) {

我们都知道,返回对象的方式返回响应,在Spring框架里,会使用HttpMessageConverter进行序列化,然后将序列化结果吐给客户端,而接受对象也是在Spring框架里反序列化成对象。然后我找到了这个这个应用的HttpMessageConverter配置(实际上公司的web应用,如果引入了common-web, common-web-support,是不需要配置HttpMessageConverter的):

@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {super.configureMessageConverters(converters);converters.add(new MappingJackson2HttpMessageConverter(JsonUtil.getObjectMapperInstance()));}

通过这里我们可以看到使用的是MappingJackson2HttpMessageConverter,而传入MappingJackson2HttpMessageConverter构造函数的是一个ObjectMapper:JsonUtil.getObjectMapperInstance()。这个JsonUtil是这个应用自己封装的一个json工具类(非来自common包),然后我就用这个工具类写个序列化测试,惊奇的发现产生了GeneratedMethodAccessor。看来是有什么配置导致公共包里的和这里的两个json工具类的差异。但是这个ObjectMapper的配置太多了,很难看出来是什么差异,而且我也对Jackson本身的机制不太熟悉,只好祭起了debug大法。

我先debug了一下公共包里的json工具类,发现这个类虽然不产生GeneratedMethodAccessor,但是会产生另外一个类:Xxx$Access4JacksonSerializer00000000,最前面的Xxx是你原来的类名。但是Jackson生成的机制和反射生成机制不同的是一个类只会产生一个这个类,而使用Java原生的反射是每个方法产生一个类。更恐怖的是,Java的反射给每个类都实例化一个单独Classloader(DelegatingClassLoader,而这也是导致Metaspace频繁full gc的其中一个原因,后文再说):

static Class<?> defineClass(String name, byte[] bytes, int off, int len,final ClassLoader parentClassLoader){ClassLoader newLoader = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {public ClassLoader run() {return new DelegatingClassLoader(parentClassLoader);}});return unsafe.defineClass(name, bytes, off, len, newLoader, null);}

那么我们可以想象:假设我们有一万个getter+setter(对于大量的POJO来讲也不算多,也就总共5000个field)。

那为什么公共包里的JsonUtil不调用Method.invoke呢,经过debug找到了生成Xxx$Access4JacksonSerializer00000000的代码位置:PropertyAccessorCollector。然后设置断点,记录stacktrace,在每个stacktrack包含的方法都设置断点,然后再debug这个应用自己封装的JsonUtil类,发现这个JsonUtil没进入到这个路径,然后就打开两台电脑同时debug,看看是在哪里开始走不同的路径了,最终发现在这里com.fasterxml.jackson.databind.ser.BeanSerializerFactory:

// Need to allow reordering of properties to serializeif (_factoryConfig.hasSerializerModifiers()) {for (BeanSerializerModifier mod : _factoryConfig.serializerModifiers()) {props = mod.orderProperties(config, beanDesc, props);}}

这就很明显了,_factoryConfig.hasSerializerModifiers()看起来是配置的东西导致的。曙光来了,后来发现是这个是下面这个配置导致hasSerializerModifiers为true,在公共包的json工具类里是有这个配置的,而这个JsonUtil是没有配置的。

objectMapper.registerModules(new AfterburnerModule())

现在问题基本上确定了,也就是Spring使用的Jackson序列化导致的,只需要改一下配置基本上就没有问题了(AfterburnerModule github)。

但是在查这个问题的时候,还有另外一个问题:其实频繁发生full gc的时候,Metaspace并没有满。那么没有满为啥就频繁full gc呢,其实Metaspace的内存分配是按照classloader分配的,也就是会给每个classloader分配一块内存,所以如果存在大量的classloader的话,会有很严重的碎片问题,碎片多了自然没有满就会full gc了。

后记

1. 经常看到各种项目里有JsonUtil, HttpUtil,DateUtil, StringUtil等等各种Util,强烈不建议这样做,一来是没有什么必要,二来也让后来的人产生困惑,三来如果有bug什么的公共包里可以集中修复,另外公共包里的类也经过了更充分的测试。

2. 很多应用里打印了大量的日志,主要是担心查问题的时候没有线索,但是直接将对象序列化的方式打印日志并不可取,首先是这样打印的日志太多太多了,完全没有必要,消耗资源不说,还有可能泄露一些敏感信息。那么即使要将对象完全打印出来,最好也是通过覆盖toString的方法来输出日志,使用lombok等工具覆盖toString方法也几乎没有什么成本。

3. HttpMessageConverter这个配置公共包里已经配置好了,不需要自己再定义

注:转载自公司内wiki

Code Review 最佳实践

文章地址

原文地址:https://mp.weixin.qq.com/s/zIiDk7rcIKgvqh0YAvQPaA

英文地址:https://github.com/google/eng-practices/

CR的标准

  • Reviewer 有责任保证CL(change list,指这次改动)的质量,作为Review的代码的Owner
  • Reviewer不应追求完美,而应追求持续改进
  • CR要具有指导意义
  • 代码风格应该与现有的一致。如果项目没有统一风格,那就接受作者的风格
  • 解决冲突难以达成共识时,需要面对面或者拉起更大的团队讨论,带上Leader

在 CR 中要看些什么

  • 代码经过完善的设计
  • 功能性对于使用者们是好的
  • 对于任何UI改动要合理且好看
  • 任何并行编程的实现是安全
  • 代码不应该复杂超过原本所必须的
  • 开发者不该实现一个现在不用而未来可能需要的功能
  • 代码有适当的单元测试
  • 测试经过完善的设计
  • 开发者对于每样东西有使用清晰、明了的命名
  • 注释要清楚且有用,并只用来解释why而非what
  • 代码有适当的写下文件(一般在g3doc)
  • 代码风格符合style guide
  • 确保你查看被要求review的每一行代码、确认上下文、确保你正在改善代码质量,并赞扬开发人员所做的好事与优点吧!

如何浏览这次改动(CL)

步骤1: 用宏观的角度来看待改动,查看CL描述以及它做什么步骤2: 检查CL主要的部分步骤3: 用合理的顺序看CL 其余的改动

Review 的速度

为什么 Review 速度要快?

在Google我们优化开发团队 共同生产产品的速度,而不是优化个人开发的速度。个人的开发速度很重要,但它不如整个团队的开发速度重要。

CR 如果很慢,则:

①、团队整体的速度下降。

②、开发人员开始抗议 cr。

③、代码质量会收到影响。review 慢时,开发者提交的压力大。

CR 要多快?

如果你并没有处于需要专注工作的时候,那么应该在 review 完后尽快修改。回复最长的极限是一个工作日。

速度 vs 中断

我们可以在投入到处理他人给的review评论之前,找个适当的时机点来进行cr。这有可能是当你的当前开发任务完成后、午餐、刚从会议脱身或从微型厨房回来等等。

快速回应

个人回应评论的速度,比起让整个 cr 过程快速结束更重要。

 若在整个过程中能快速获得来自 reviewer 的回应,大大减轻开发者对缓慢 cr 过程的挫败感。

 reviewer员要花足够的时间来进行review,确保他们给出的LGTM,意味着“此代码符合我们的标准”。

 理想的个人的回应速度还是越快越好。

新增一个分类

最近脑子里的东西感觉比较散乱,而且有愈演愈烈的趋势。

大概至少半年多了。

至少这么长的时间好好安静下来梳理一下自己的体系和需要准备、学习的东西。

脑力不济,在这个行业,短期脑海中想到的最直接的威胁,就是被优化。比不上年轻人了,还拿着比别人高的工资,公司肯定不愿意。

但长期来看,这会对自己方方面面形成威胁。毕竟想要生活下去,想要保持收入水平,其实依赖的是自己本身的能力。倘若脑子里这堆东西不行了,那么接下来自己做什么,都会是一团浆糊。工作这件事情,本身是一件比较单纯的打工的事情,如果自己去做其他事情,则需要考虑的东西多得多,脑子不够用,也纯粹给别人送钱么。

对自己影响最大的,其实除了平时工作强度过高,导致身心俱疲,没心思去梳理整理这些,并且对工作产生了很强的抵触情绪。

但是,同时自己对积极学习准备,尽快离开当前的工作环境这个事儿也提不起精神气来。

这个问题就比较严重了。

累到了一定程度。感觉需要好好的休息上几个月,把精神养回来,才能继续后边的事情。

方法论方面,是自己需要提高的,对于很多事情,不只是说自己能做出来就行,需要有更高层面的思考,形成体系,让自己面对不同的事情的时候,可以使用方法论来快速接受理解处理事情并达到一个比较不错的目标。

最近交易的事情也做的很不好,需要好好总结思考一下。

但是有一点,操作不好梳理不来,或者操作杂乱无章的时候,就停下,保证利润不受损失。

制作自己的交易规则,严格遵守,并不断优化,才是用交易为自己谋取收益的路子。