JUnit内存过小导致JPinyinEngine报出NoClassDefFoundError

486 阅读4分钟
  • 起 在使用JUnit跑单元测试的时候,其中使用了hutool工具类生成拼音,cn.hutool.extra.pinyin.PinyinUtil#getFirstLetter,但是跑的过程中发现启动巨慢,并且抛出了一个异常

image.png 并且在启动过程中可能会抛出以下异常 image.png

  • 承 据以上信息,我们可以推测PinyinHelper应该没有引进来,并且单元测试所使用的内存也不够大,因此查看pom文件是否引入 image.png 神奇的是居然有引入这个jar包,并且明目张胆地被JPinyinEngine引用着 image.png emmm。。应该是编译有问题, reload一下重新编译。

跑起来。。此时一位少年感到不可思议

我们继续看

<groupId>com.github.stuxuhai</groupId>
<artifactId>jpinyin</artifactId>

是近期才引入进来的,但是在生产环境并不会产生报错。 进入到cn.hutool.extra.pinyin.PinyinUtil#getFirstLetter查看源码 首先获取拼音引擎 image.png image.png 这里使用Java SPI去获取,在hutool-all中已经写好了PinyinEngine的SPI实现。 image.png image.png 其实到这里可以先说一下结论,在没有引用jpinyin前使用的是Pinyin4jEngine,引用jpinyin后使用的是JPinyinEngine,那么hutool是如何决定使用哪个引擎呢? 在iterator.next()中,有这么一段,c.newInstance()出错会抛出异常,从而使SPI循环寻找下一个实现。 image.png 因此引入jpinyin后使用的是JPinyinEngine,我们将Breakpoint打到JPinyinEngine中,发现报错变成了内存溢出。 image.png 😱一个小小的工具类怎么可能让2G的内存溢出,沿着堆栈的指引,来到DoubleArrayTrie.resize image.png 如何调用的resize()呢? image.png image.png 可以计算一下,resize大概会开辟约20M的空间,从而导致内存溢出,虽然这个20M不太合理,如果用2G的内存给他跑呢。 于是将JUnit设置为2G。 image.png 此时一位少年感觉稳了。 然而又打脸了。 那么是不是真的给了2G的内存给单元测试在跑呢? 看看JVM启动参数 image.png 此时一个少年惊了,-Xmx2048m后竟然跟了一个256m。

  • 转 看个pom文件
<pluginManagement>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <skipTests>true</skipTests>
                <includes>
                    <include>**/*Test.java</include>
                </includes>
                <argLine>-Xmx256m</argLine>
            </configuration>
        </plugin>
    </plugins>
</pluginManagement>

此时一个少年感觉稳了,结果确实稳如老狗,JUnit的启动速度也有了质的飞跃。

  • 合 以上,这个问题就解决了,但是困扰我们还有几个问题? 为什么实例化JPinyinEngine可以成功,但是调用其中方法的时候却报错了。熟悉类加载过程的同学知道,类加载的初始化阶段会执行<clinit>()方法,<clinit>()是由编译器按照源码顺序收集类中静态变量的赋值语句和静态代码块,而初始化的触发时机,根据JVM规范,有且只有 5 种情况必须立即对类进行“初始化”:
  1. 遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。
  2. 对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)
  4. 虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

在JPinyinEngine实例化过程中,虽然代码中有PinyinHelper,但是并不会引起它的初始化,实际使用时,调用了com.github.stuxuhai.jpinyin.PinyinHelper#convertToPinyinString静态方法,符合第一种情况,因此执行了静态代码块中的内容。

另外在启动过程中出现的GC Overhead Limit Exceeded Error错误,这个错误是由于JVM花费太长时间执行GC且只能回收很少的堆内存时抛出的。根据Oracle官方文档,默认情况下,如果Java进程花费98%以上的时间执行GC,并且每次只有不到2%的堆被恢复,则JVM抛出此错误。换句话说,这意味着我们的应用程序几乎耗尽了所有可用内存,垃圾收集器花了太长时间试图清理它,并多次失败。也可以侧面反映出内存已经相当吃紧。

最后,不要轻易引入其他依赖。