我在Nashorn的表现有些呆滞,我无法真正解释原因。我将详细说明我的设置是什么以及如何调试它。
硬件: 相当不错的服务器硬件('13时代-12核心Xeon,2.1GHz)。 64GB DDR3 RAM。
软件: Oracle JDK8(最新的64位)(40GB RAM已预先分配给JVM)。
我的实现是: 多个Nashorn ScriptEngine实例,每个实例都有一个预编译的“ utility.js”,其中提供了一些用户定义的脚本可以利用的辅助功能。
我有一个ScriptEngine对象池,它们已经准备好与已经针对它们编译的Utility.js一起使用,还有一个线程分配器,该分配器会将线程加速到设定的限制。每个线程将获取一个预分配的ScriptEngine,并使用新的上下文将用户JS评估给它,并执行它/将结果存储在某个地方,然后再将ScriptEngine返回到池中。一切都很好,如果我的用户脚本非常简单(单个功能),那就快了。
但是,大多数用户脚本相当大,其形式为:
function myFunc() {
myFunc1();
myFunc2();
... (you get the picture, they define and call a lot of functions!)
myFunc100();
}
function myFunc1() {
// do something simple here
}
并行运行时,一次要说25个线程,每个线程都有自己的ScriptEngine(以及上面提到的所有预编译的东西)将花费很长时间执行,而CPU使用率却很少(8-10) %total),并且jmc / jvisualvm中没有重大阻塞。线程将显示它们已经阻塞了相当数量(按数量计),但是切片是如此之细,以至于我在单击线程时都无法看到它们。
在大多数情况下,当我单击线程时,它们都显示它们在MethodHandleNatives.setCallSiteTargetNormal中。
我尝试了一些事情: 1.单引擎,不同的环境。即使所有线程都已预编译,我也可以在它们之间看到阻塞。线程将等待(如应有的那样),然后再根据我的判断调用各个字节码片段。这不是可行的解决方案。
这是我创建ScriptEngine并使用“ utility.js”对其进行预填充的方式(我将它们填充到池中的代码已省略以使其简短):
/**
* Creates a PreCompiledScriptEngine which will contain a ScriptEngine + Pre-compiled utility.js
*/
private PreCompiledScriptEngine createScriptEngine() {
String source = new Scanner(this.getClass().getClassLoader().getResourceAsStream(UTILITY_SCRIPT)).useDelimiter("\\Z").next();
try {
totalEngines.getAndAdd(1);
ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine();
return new PreCompiledScriptEngine(engine, ((Compilable) engine).compile(source));
}
catch (ScriptException e) {
Logger.error(e);
}
return null;
}
/**
* Small helper class to group a ScriptEngine and a CompiledScript (of utility.js) together
*/
public class PreCompiledScriptEngine {
private ScriptEngine scriptEngine;
private CompiledScript compiledScript;
PreCompiledScriptEngine(ScriptEngine scriptEngine, CompiledScript compiledScript) {
this.scriptEngine = scriptEngine;
this.compiledScript = compiledScript;
}
public ScriptEngine getScriptEngine() {
return scriptEngine;
}
/**
* This method will return the utility.js compiled runtime against our engine.
*
* @return CompiledScript version of utility.js
*/
public CompiledScript getCompiledScript() {
return compiledScript;
}
}
这是我执行用户特定JavaScript的方法:
public Object executeUserScript(String script, String scriptFunction, Object[] parameters) {
try {
// Create a brand new context
PreCompiledScriptEngine preCompiledScriptEngine = obtainFromMyScriptEnginePool();
ScriptEngine engine = preCompiledScriptEngine.getScriptEngine();
ScriptContext context = new SimpleScriptContext();
context.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
// Evaluate the pre-compiled utility.js in our new context
preCompiledScriptEngine.getCompiledScript().eval(context);
// Evaluate the specific user script in this context too
engine.eval(script, context);
//get the JS function the user wants to call
JSObject jsObject = (JSObject) context.getAttribute(scriptFunction, ScriptContext.ENGINE_SCOPE);
// Call the JS function with the parameters
return jsObject.call(null, parameters);
}
catch (ScriptException e) {
Logger.error("generated", e);
throw new RuntimeException(e.getMessage());
}
}
我期望的是,如果我的线程池用尽了计算机上的可用资源并显示出较低的性能,那么CPU使用率将为100%,但是相反,我看到的是较低的CPU和较低的性能:(无法完全确定我在哪里出错了,或者为什么它这么慢而又没有明显的资源消耗。
我要从JVisualVM抓取堆栈跟踪时刚注意到的一件事是,我的所有线程似乎都呈现这种情况:我允许用户定义的Java脚本调用Utility.js函数,该函数本质上是“执行另一个脚本” ”,所有堆栈跟踪似乎都是从此嵌套调用到另一个脚本的。在我的设置中,它将使用相同的线程,并使用新上下文再次使用该线程中的相同引擎。我想这和以前一样,不需要进一步编译吗?
我已经看过的相关文章: What is the difference between anonymous and inline functions in JavaScript? 和 Nashorn inefficiency
编辑: 从更深入的角度来看,主要是eval()是从编译脚本内部发生的,但并非总是如此,有关特定情况的某些事情必须使它无法直接重新调用而不调用setTarget(),这最终会花费更多时间。
有趣的是,线程在向本地JVM方法进行这些调用时不会显示它们在阻塞,因此很难看到我所看过的每个工具在哪里花费时间