合成字段?一个线上问题带你回顾类加载子系统-全程高能 线上问题排查 jacoco 类加载 NoClassDefFound

合成字段?一个线上问题带你回顾类加载子系统。全程高能

场景复现

代码小考查

  • 在不考虑代码规范及写法的前提下,判断以下代码是否可以正常运行结束?
    • 其实代码很简单,很容易看出来的。可以在弹幕上写出来。
    • 代码主要有几部分:静态代码块,main方法,Student类,Property注解。

image-20240522182413246

  • 答案是能够能够正常执行,代码没有任何问题,仅是写法上不够优雅,以及可能有风险,但是跑还是可以跑得,不信我给你跑一个。

image-20240522183838505

  • 结果居然报错了,这不是怕怕打脸么。刚刚说可以正常运行结束的都给自己两耳光!

  • 但这不是违背常理了吗,高斯林(Java之父)来了也得说可以正常运行并结束。

  • 别急,各位客户请听我慢慢道来。

场景描述

  • 这是我当时遇到的一个线上问题,但是代码并不是我写的哈,而是我当时带的实习生写的。
  • 代码也很简单,由于涉及到公司项目,我就把场景还原了。
  • 大致逻辑就是一个类有一个静态代码块,在静态代码块中读取所有字段的Property注解中的value值,作为map的key,而当前字段作为map的value添加到map中。也就是构建了一个<String, Field>Map。
  • 当时代码在本地跑的非常顺畅,简直就是丝滑的不能再丝滑。
  • 然后一上线就报错了。

排查思路

  • 由于当时场景比较特殊,拿不到报错的那些异常信息,这个原因我会在后面介绍(很硬的哟,干货满满)。只知道大概是当前这个类的问题。

  • 其实这里的关键代码也就是static静态代码块了。逻辑就是拿Student类的所有字段,然后获取Property注解的value。虽然这里的写法是一段高危写法(字段如果没有Property注解就直接获取value就会空指针),但是抵不住我的Student两个字段都有Property注解啊,也都有value属性。那这就不可能报错啊。

    image-20240522194854659

  • 关键是本地还是正常的,代码看破天也没问题,还拿不到正确的异常信息(后面会解释),但是线上还是有问题。

  • 只能祭出终极大法了,try+catch+日志。合理怀疑stati静态代码块的问题,给整个static代码块都try起来了,捕获异常并打印日志。

    image-20240522195102745

  • 重新打包发布,终于看到异常信息了,但是也就一个空指针,没有任何额外的信息。但看瞎了也没可能空指针啊。

  • 我突然想起眼见不一定为实,如果空指针,那就把最有可能空指针的地方找出来。毫无疑问是field.getDeclaredAnnotation(Property.class).value()。如果字段没有Property注解就会空指针。那就把Student类的所有字段都打印出来,看看他到底有哪些字段。

    image-20240522194854659

  • 线上环境居然多了一个字段,$jacocoData这个字段是什么鬼?为什么线上环境会比测试环境多出一个字段?这个字段到底是干啥的呢?又为什么会空指针呢?

    image-20240522195659338

揭秘时刻

  1. 为什么会多出一个$jacocoData字段?而本地环境没有。

    • 首先,jacoco是一个统计代码覆盖率的工具。

    • 其实就是因为线上环境使用了jacoco,将jacoco作为javaagent运行。增强了我们的代码,用于统计我们的代码覆盖率。

    • 我这里为了模拟这种场景,使用了覆盖运行,它也会使用jacoco,也就能够模拟线上环境使用jacoco的javaagent了。

      image-20240522214839378

  2. 为什么会空指针?

    • 因为jacoco要统计代码覆盖率,就需要增强我们的代码,他就往类中加了一个字段,$jacocoData,用于辅助统计代码覆盖率。
    • 由于这个字段是jacoco合成了,那当然也就没有Property注解了,所以我们去获取他的value也就会空指针。

如何解决

  • 这个问题解决其实很简单,代码本身写的就不优雅,并且属于高危代码。

    1. 首先是可以直接对field.getDeclaredAnnotation(Property.class)直接判空,不为空在范围value属性。
    2. 或者是使用field.isAnnotationPresent(Property.class)判断字段是否存在Property注解。
    3. 直接判断字段是否为合成字段field.isSynthetic(),如果是合成字段则过滤掉。
    4. 我的建议是不论字段是否是合成字段,获取注解的相关属性是,都应该先确定字段存在当前注解。当然value也有可能为空哈,我这里是有默认值,就直接获取了(错误示范)。

    image-20240522201959502

面试

  1. 把这个问题搞明白了,加在面试里面,平时开发中遇到过什么问题,那不是爽歪歪啊,涉及知识面也广。
  2. 在之前的面试中回答过,但是还有一些东西没有弄清楚。
  3. 感兴趣的可以去看看 https://www.bilibili.com/video/BV1ro4y1K7uv 1:00–4:30

真的正确吗?

  • 如果不使用jacoco,那么就没有这个合成字段,那这个代码就能够完美运行了吗?大家思考一下,可以发送弹幕记录一下。

  • 不要在乱说了哈,不然高斯林的棺材板压不住了,不对,他还没有到那一步,还没有棺材板。

  • 答案是不,在某些情况下他依旧不能够正常运行。

    image-20240522215615981

    • 这是因为内部类,lambda表达式这些,也会生成合成字段,标识这个表达式属于哪个类。当前内部类属于哪个外部类。

      image-20240522220053684

为何拿不到异常信息【类加载子系统】

  1. 我们的业务系统是springboot的web项目,然后当前功能是通过接口去触发的,在第一次调用接口才会触发完整的异常信息,后面调用接口都是其他异常。

  2. 这是因为在第一次调用接口时,由于内存中还没有这个类,jvm就会尝试去加载这个类,根据异常堆栈可以看到,首先是空指针异常,访问了不存在的实例。由于空指针发生在static代码块中;在编译阶段,编译器会收集所有的静态字段和static静态代码块,构建<clinit>方法。

    image-20240522210234193

  3. clinit方法中发送空指针异常,clinit方法也就执行失败。然后jvm就抛出了ExceptionInInitializerError异常,表示静态初始化失败。

  4. 那我们的clinit执行在类加载的那个阶段呢,先说结论,初始化就是执行<clinit>的过程,注意,初始化方法不是构造器哈。

    1. 类加载整体分为三个阶段:加载,链接,初始化。
    2. 加载:将class文件从磁盘加载到jvm内存(使用双亲委派模型)。
    3. 链接:链接又分为三个阶段。
      1. 验证:文件格式校验(开头字节是否为0X CAFEBABE),版本校验…
      2. 准备:静态变量赋默认值
      3. 解析:将常量池中的符号引号转换为直接引用的过程
    4. 初始化:执行<clinit>方法。给静态变量赋程序中的真实值,执行静态代码块。

image-20240522210039480

  1. 现在也就是加载,链接阶段都成功了,但是在初始化阶段失败了。那么jvm就会认定当前类加载失败。并且不会重新加载当前类。为什么?

    1. 确定性:Java虚拟机保证在程序的生命周期内,一个类在初始化时只会执行一次<clinit>()方法。如果重新加载,就会违反这一保证。
    2. 性能:重新加载类是一个昂贵的操作,如果一个类初始化失败,重新加载它并不会解决根本问题,反而可能浪费资源。 因此,一旦类加载过程中的<clinit>()方法执行失败,Java虚拟机就会将该类标记为错误状态,并且不会再次尝试加载这个类。开发者需要根据错误信息来修正类定义中的问题,然后重新编译和部署,以确保类能够正确加载和执行。
  2. 所以,我们在去访问这个类时,jvm就不会再去加载这个类,但是方法区中又没有当前类,要怎么办呢,那不如抛出一个异常吧,也就是NoClassDefFoundError(未找到类定义错误)异常。不同于ClassNotFoundException(类未找到异常)。NoClassDefFoundError它是能够找到这个类,但是没有相应的类定义。

    image-20240522214054398

  3. 同理,在web程序中,第一次请求接口时,去尝试加载类,加载失败之后,后续不会重新加载。而是在再次访问类是抛出NoClassDefFoundError.

总结

  1. 使用Jacoco会生成合成字段,获取字段注解时应该注意判空。
  2. 内部类,lambda也有可能生成合成字段。
  3. 判断是否为合成字段: field.isSynthetic()
  4. 类加载整体分为三个阶段:加载,链接,初始化。执行静态代码块在初始化阶段,也就是执行<clinit>方法。
  5. <clinit>方法执行失败会抛出ExceptionInInitializerError异常初始化类错误,并且不会重新加载类,后续使用类时,会抛出NoClassDefFoundError(无类定义错误)。

结语

  • 记得3连多多支持一下,也可以点个关注。
  • 后续也会更新更多干货
  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值