本文主要记述在某次迭代开发中, 处理因Nashorn的不当使用导致OOM的问题 的全部过程。

在一次迭代开发中,测试环境的爬虫任务内存突然告警了,起初没在意,因为测试环境本身服务器负载就跑的比较高。结果没过多久,该服务直接OOM死掉了,于是决定对去查一下具体原因。

首先查看运行日志,这里当时没截图。

然后查看了GC日志,发现空间已被占满,同时产生了heap 文件。

于是决定对heap文件进行分析。

将heap文件从服务器导出,使用jprofile 分析。

JVM 监控分析

查看JVM监控大盘,部分情况如图所示:

GC的暂停时间
GC的暂停时间
Metaspace空间
Metaspace空间
类加载数量
类加载数量

可以看到报警时到OOM发生期间,GC暂停时间翻了10倍, 类加载的数量Metaspace空间 在此期间几乎都翻了一倍。大部分情况下,程序运行中不会出现过多的类加载数量的变动,该服务以前也不曾有过相关的情况,因此大致可以确定跟类加载相关。

为了进一步定位问题所在,我们还需要更多详细的信息,于是将 OOM 时产生的 heap文件导出,对其进行分析。

Heap 文件分析

heap文件中,我们可以看到对象的分布引用情况 和 线程(Thread Dump)的相关情况。

Thread Dump

Thread Dump 中 很轻松找到了 OOM 的具体位置,在 parseFromJs 这个方法,一般情况下,触发OOM的地方就是问题代码的地方,我们稍后对该方法进行分析。

Thread Dump
Thread Dump

当然也有可能是别的地方出现问题,触发OOM的代码只是背锅了而已,这种情况就需要我们对系统整体进行分析。

当前对象集合 (Current Object Set )

对象分布中,发现大量的 byte[]char[] 数组,如图:
对象集合

对象集合

选择查看占用内存最多的 char[] 的引用,可以看到大量的类似字符串的数据,如图所示:

char[]对象集合
char[]对象集合

问了一下其他的同事,知道了这些数据的来源,是爬虫中获取的js的数据,但是不知道为什么会出现在这里。

我们继续分析,选择其中一个对象查看对象的引用关系 show in graph

show in graph
show in graph

其引用关系如图

引用关系
引用关系

char[] 的引用指向了 Script 对象的 eval 方法,结合同事的描述,爬虫中对 js 进行了解析,根据同事描述,找到了解析的相关方法,稍后再做分析。

该方法大致如图(略有改动与dump日志位置不同):

    public static Object parseFromJs(String js, String key) {
        ScriptEngineManager manager = new ScriptEngineManager();
        try {
            ScriptEngine engine = manager.getEngineByName("js");
            engine.eval(js);
            return engine.get(key));
        } catch (Exception e) {
            log.warn("parse from js error ! ex:{}", e.getMessage());
            return null;
        }
    }

改方法的功能为:
传入一段js代码,和一个 key , 执行该代码,返回 js 代码中定义的 key 的值。其应用场景是目标网站数据返回是以js为返回值的,比如:

var arrXXXXX = ['xxx'];
var arrXXX= [[171111,'xxxxx']];
var lastUpdateTime = '2021-09-16 10:06:09';

需要获取 arrXXXXX 的值,然后去处理数据。

分析总结

看到这里有经验的小伙伴应该已经能看出问题了。js中解析数据其实有很多方法,该同事使用了此方法是觉得很方便没有深究其实现逻辑。

Nashorn,发音 “nass-horn”,是德国二战时一个坦克的命名,最初是在JDK 8中引入的,用于取代Rhino脚本引擎。当其发布时,Nashorn是ECMAScript-262 5.1的完整实现,增强了Java和JavaScript的兼容性。后面还增加了新的ECMAScript 6(ES6)特性。借助Nashorn,开发人员可以从JavaScript调用Java代码,也可以从Java代码调用JavaScript函数。Nashorn可以作为Java应用程序的嵌入式解释器,提供使用Nashorn命令行工具jjs从命令行运行JavaScript的能力。当在Java中对JavaScript代码求值时,Nashorn实现了javax.script API。在JDK11中已被标记为 @Deprecated(since="11", forRemoval=true) , 即将被移除。Oracle表示,弃用Nashorn不会影响javax.script API。

上述代码中运行js,实际上使用的是java中默认的 NashornScriptEngine 来实现的,它没有用Java实现的JavaScript解释器,而只有把JavaScript编译为Java字节码再交由JVM执行这一种流程。

简单来说,Nashorn的编译入口可以从 Context.compile() 开始看:[ JavaScript源码 ] -> ( 语法分析器 Parser ) -> [ 抽象语法树(AST) ir ] -> ( 编译优化 Compiler ) -> [ 优化后的AST + Java Class文件(包含Java字节码) ] -> JVM加载和执行生成的字节码 -> [ 运行结果 ]

此过程是十分耗时的,每次执行eval 去运行js ,如果脚本不同,都需要编译成字节码、然后加载执行。同时会将编译过的字节码缓存起来,以便后续使用,因此加载的类会长时间存活,占用很大的内存空间。

这里很明显是开发人员没弄懂其中的原理,随意使用了该方法。

改进

这里解析 js 里存的数据,直接使用正则表达式对字符串进行处理即可,没有必要使用js 引擎,如果需要使用js处理,可以编写一个通用的js脚步来处理相关信息,每次调用同样的脚本,就不会产生过多无用的类,给系统造成额外的负荷。

参考文献

相关文章

Q.E.D.