前言:
真的好久沒在博客園上更新博客了, 如今趁這段空閒的時間, 對以前接觸的一些工程知識作下總結. 先來說下借用Groovy如何來實現代碼的熱載, 以及其中涉及到的原理和須要注意的點.
總的來講, Groovy做爲一本動態編譯語言, 其對標應該是c/c++體系中的lua, 在一些業務邏輯變更頻繁的場景, 其意義很是的重大.java
簡單入門:
本文的主題是Groovy實現代碼熱載, 其餘大背景是java實現主幹代碼, groovy實現易變更的邏輯代碼. 先來看下java是如何調用的groovy腳本的.c++
import groovy.lang.Binding; import groovy.lang.GroovyShell; public class GroovyTest { public static void main(String[] args) { // *) groovy 代碼 String script = "println 'hello'; 'name = ' + name;"; // *) 傳入參數 Binding binding = new Binding(); binding.setVariable("name", "lilei"); // *) 執行腳本代碼 GroovyShell shell = new GroovyShell(binding); Object res = shell.evaluate(script); System.out.println(res); } }
這段代碼的輸出爲:shell
hello name = lilei
Binding類主要用於傳遞參數集, 而GroovyShell則主要用於編譯執行Groovy代碼. 是否是比想象中的要簡答, ^_^.
固然java調用groovy還有其餘的方式, 下文會涉及到.緩存
原理分析:
下面這段其實大有文章.多線程
GroovyShell shell = new GroovyShell(binding); Object res = shell.evaluate(script);
對於函數evaluate, 咱們追蹤進去, 會有很多的從新認識.app
public Object evaluate(GroovyCodeSource codeSource) throws CompilationFailedException { Script script = this.parse(codeSource); return script.run(); } public Script parse(GroovyCodeSource codeSource) throws CompilationFailedException { return InvokerHelper.createScript(this.parseClass(codeSource), this.context); }
其大體的思路, 爲Groovy腳本代碼包裝生成class, 而後產生該類實例對象, 在具體執行其包裝的邏輯代碼.
可是這邊須要注意的狀況:函數
public Class parseClass(String text) throws CompilationFailedException { return this.parseClass(text, "script" + System.currentTimeMillis() + Math.abs(text.hashCode()) + ".groovy"); }
對於groovy腳本, 它默認會生成名字爲script + System.currentTimeMillis() + Math.abs(text.hashCode())的class類, 也就是說傳入腳本, 它都會生成一個新類, 就算同一段groovy腳本代碼, 每調用一次, 都會生成一個新類.
ui
陷阱評估:
原理咱們基本上理解了, 可是讓咱們來構造一段代碼, 看看是否有哪些陷阱.this
import groovy.lang.Binding; import groovy.lang.GroovyShell; import groovy.lang.Script; import java.util.Map; import java.util.TreeMap; public class GroovyTest2 { private static GroovyShell shell = new GroovyShell(); public static Object handle(String script, Map<String, Object> params) { Binding binding = new Binding(); for ( Map.Entry<String, Object> ent : params.entrySet() ) { binding.setVariable(ent.getKey(), ent.getValue()); } Script sci = shell.parse(script); sci.setBinding(binding); return sci.run(); } public static void main(String[] args) { String script = "println 'hello'; 'name = ' + name;"; Map<String, Object> params = new TreeMap<String, Object>(); params.put("name", "lilei"); while(true) { handle(script, params); } } }
這段代碼執行到最後的結果爲, 頻繁觸發full gc, 究其緣由爲PermGen區爆滿. 這是爲什麼呢?
如上所分析的, 雖然是同一份腳本代碼, 可是都爲其每次調用, 間接生成了一個class類. 對於full gc, 除了清理老年代, 也會順便清理永久代(PermGen), 但爲什麼不清理這些一次性的class呢? 答案是gc條件不成立.
引用下class被gc, 需知足的三個條件:
1). 該類全部的實例都已經被GC
2). 加載該類的ClassLoader已經被GC
3). 該類的java.lang.Class對象沒有在任何地方被引用
加載類的ClassLoader實例被GroovyShell所持有, 做爲靜態變量(gc root), 條件2不成立, GroovyClassLoader有個map成員, 會緩存編譯的class, 所以條件3都不成立.
有人會問, 爲什麼不把GroovyShell對象, 做爲一個臨時變量呢?lua
public static Object handle(String script, Map<String, Object> params) { Binding binding = new Binding(); for ( Map.Entry<String, Object> ent : params.entrySet() ) { binding.setVariable(ent.getKey(), ent.getValue()); } GroovyShell shell = new GroovyShell(); Script sci = shell.parse(script); sci.setBinding(binding); return sci.run(); }
實際上, 仍是治標不治本, 只是說class能被gc掉, 可是清理的速度可能趕不上產生的速度, 依舊頻繁觸發full gc.
推薦作法:
解決上述問題很簡單, 就是引入緩存, 固然緩存的對象不上Script實例(在多線程環境下, 會遇到數據混亂的問題, 對象有狀態), 而是Script.class自己. 對應的key爲腳本代碼的指紋.
大體的代碼以下所示:
private static ConcurrentHashMap<String, Class<Script>> zlassMaps = new ConcurrentHashMap<String, Class<Script>>(); public static Object invoke(String scriptText, Map<String, Object> params) { String key = fingerKey(scriptText); Class<Script> script = zlassMaps.get(key); if ( script == null ) { synchronized (key.intern()) { // Double Check script = zlassMaps.get(key); if ( script == null ) { GroovyClassLoader classLoader = new GroovyClassLoader(); script = classLoader.parseClass(scriptText); zlassMaps.put(key, script); } } } Binding binding = new Binding(); for ( Map.Entry<String, Object> ent : params.entrySet() ) { binding.setVariable(ent.getKey(), ent.getValue()); } Script scriptObj = InvokerHelper.createScript(script, binding); return scriptObj.run(); } // *) 爲腳本代碼生成md5指紋 public static String fingerKey(String scriptText) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(scriptText.getBytes("utf-8")); final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); StringBuilder ret = new StringBuilder(bytes.length * 2); for (int i=0; i<bytes.length; i++) { ret.append(HEX_DIGITS[(bytes[i] >> 4) & 0x0f]); ret.append(HEX_DIGITS[bytes[i] & 0x0f]); } return ret.toString(); } catch (Exception e) { throw new RuntimeException(e); } }
這邊會爲每一個新類單首創建一個GroovyClassLoader對象, 也是巧妙地迴避以前的陷阱.
總結: 這邊沒有深刻研究java中類的加載機制, 只是涉及class被gc的先決條件, 同時提供了一種思路, 如何藉助groovy實現代碼熱加載, 同時又規避其中的陷阱.