Groovy与Java集成常见的坑
groovy特性
Groovy是一門基于JVM的動態(tài)語言,同時也是一門面向?qū)ο蟮恼Z言,語法上和Java非常相似。它結(jié)合了Python、Ruby和Smalltalk的許多強(qiáng)大的特性,Groovy 代碼能夠與 Java 代碼很好地結(jié)合,也能用于擴(kuò)展現(xiàn)有代碼。
Java作為一種通用、靜態(tài)類型的編譯型語言有很多優(yōu)勢,但同樣存在一些負(fù)擔(dān):
- 重新編譯太費工;
- 靜態(tài)類型不夠靈活,重構(gòu)起來時間可能比較長;
- 部署的動靜太大;
- java的語法天然不適用生產(chǎn)dsl;
相對于Java,它在編寫代碼的靈活性上有非常明顯的提升,對于一個長期使用Java的開發(fā)者來說,使用Groovy時能夠明顯地感受到負(fù)身上的“枷鎖”輕了。Groovy是動態(tài)編譯語言,廣泛用作腳本語言和快速原型語言,主要優(yōu)勢之一就是它的生產(chǎn)力。Groovy 代碼通常要比 Java 代碼更容易編寫,而且編寫起來也更快,這使得它有足夠的資格成為開發(fā)工作包中的一個附件。
Java不是解決動態(tài)層問題的理想語言,這些動態(tài)層問題包括原型設(shè)計、腳本處理等。可以把Groovy看作給Java靜態(tài)世界補(bǔ)充動態(tài)能力的語言,同時Groovy已經(jīng)實現(xiàn)了java不具備的語言特性:
- 函數(shù)字面值;
- 對集合的一等支持;
- 對正則表達(dá)式的一等支持;
- 對xml的一等支持;
groovy與java集成的方式
重溫下Groovy調(diào)用Java方式,包括使用GroovyClassLoader、GroovyShell和GroovyScriptEngine。
GroovyClassLoader
用 Groovy 的 GroovyClassLoader ,動態(tài)地加載一個腳本并執(zhí)行它的行為。GroovyClassLoader是一個定制的類裝載器,負(fù)責(zé)解釋加載Java類中用到的Groovy類。
GroovyClassLoader loader = new GroovyClassLoader(); Class groovyClass = loader.parseClass(new File(groovyFileName)); GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance(); groovyObject.invokeMethod("run", "helloworld");GroovyShell
GroovyShell允許在Java類中(甚至Groovy類)求任意Groovy表達(dá)式的值。您可使用Binding對象輸入?yún)?shù)給表達(dá)式,并最終通過GroovyShell返回Groovy表達(dá)式的計算結(jié)果。
GroovyShell shell = new GroovyShell(); Script groovyScript = shell.parse(new File(groovyFileName)); Object[] args = {}; groovyScript.invokeMethod("run", args);GroovyScriptEngine
GroovyShell多用于推求對立的腳本或表達(dá)式,如果換成相互關(guān)聯(lián)的多個腳本,使用GroovyScriptEngine會更好些。GroovyScriptEngine從您指定的位置(文件系統(tǒng),URL,數(shù)據(jù)庫,等等)加載Groovy腳本,并且隨著腳本變化而重新加載它們。如同GroovyShell一樣,GroovyScriptEngine也允許您傳入?yún)?shù)值,并能返回腳本的值。
Groovy代碼文件與class文件的對應(yīng)關(guān)系
而作為基于JVM的語言,Groovy可以非常容易的和Java進(jìn)行互操作,但也需要編譯成class文件后才能運行,所以了解Groovy代碼文件和class文件的對應(yīng)關(guān)系,有助于更好地理解Groovy的運行方式和結(jié)構(gòu)。
對于沒有任何類定義
如果Groovy腳本文件里只有執(zhí)行代碼,沒有定義任何類(class),則編譯器會生成一個Script的子類,類名和腳本文件的文件名一樣,而腳本的代碼會被包含在一個名為run的方法中,同時還會生成一個main方法,作為整個腳本的入口。
對于僅有一個類
如果Groovy腳本文件里僅含有一個類,而這個類的名字又和腳本文件的名字一致,這種情況下就和Java是一樣的,即生成與所定義的類一致的class文件。
對于多個類
如果Groovy腳本文件含有多個類,groovy編譯器會很樂意地為每個類生成一個對應(yīng)的class文件。如果想直接執(zhí)行這個腳本,則腳本里的第一個類必須有一個static的main方法。
groovy與java集成中經(jīng)常出現(xiàn)的問題
使用GroovyShell的parse方法導(dǎo)致perm區(qū)爆滿的問題
如果應(yīng)用中內(nèi)嵌Groovy引擎,會動態(tài)執(zhí)行傳入的表達(dá)式并返回執(zhí)行結(jié)果,而Groovy每執(zhí)行一次腳本,都會生成一個腳本對應(yīng)的class對象,并new一個InnerLoader去加載這個對象,而InnerLoader和腳本對象都無法在gc的時候被回收運行一段時間后將perm占滿,一直觸發(fā)fullgc。
- 為什么Groovy每執(zhí)行一次腳本,都會生成一個腳本對應(yīng)的class對象?
一個ClassLoader對于同一個名字的類只能加載一次,都由GroovyClassLoader加載,那么當(dāng)一個腳本里定義了C這個類之后,另外一個腳本再定義一個C類的話,GroovyClassLoader就無法加載了。為什么這里會每次執(zhí)行都會加載?
這是因為對于同一個groovy腳本,groovy執(zhí)行引擎都會不同的命名,且命名與時間戳有關(guān)系。當(dāng)傳入text時,class對象的命名規(guī)則為:"script" + System.currentTimeMillis() + Math.abs(text.hashCode()) + ".groovy"。這就導(dǎo)致就算groovy腳本未發(fā)生任何變化,每次執(zhí)行parse方法都會新生成一個腳本對應(yīng)的class對象,且由GroovyClassLoader進(jìn)行加載,不斷增大perm區(qū)。
- 為什么InnerLoader加載的對應(yīng)無法通過gc清理掉?
大家都知道,JVM中的Class只有滿足以下三個條件,才能被GC回收,也就是該Class被卸載:1. 該類所有的實例都已經(jīng)被GC,也就是JVM中不存在該Class的任何實例;2. 加載該類的ClassLoader已經(jīng)被GC;3. 該類的java.lang.Class對象沒有在任何地方被引用,如不能在任何地方通過反射訪問該類的方法。
在GroovyClassLoader代碼中有一個class對象的緩存,進(jìn)一步跟下去,發(fā)現(xiàn)每次編譯腳本時都會在Map中緩存這個對象,即:setClassCacheEntry(clazz)。每次groovy編譯腳本后,都會緩存該腳本的Class對象,下次編譯該腳本時,會優(yōu)先從緩存中讀取,這樣節(jié)省掉編譯的時間。這個緩存的Map由GroovyClassLoader持有,key是腳本的類名,這就導(dǎo)致每個腳本對應(yīng)的class對象都存在引用,無法被gc清理掉。
- 如何解決?
請參考:Groovy引發(fā)的PermGen區(qū)爆滿問題定位與解決。
如需更深入的理解GroovyClassLoader體系,請參考下面這篇文章Groovy深入探索——Groovy的ClassLoader體系
使用GroovyClassLoader加載機(jī)制導(dǎo)致頻繁gc問題
通常使用如下代碼在Java 中執(zhí)行 Groovy 腳本:
GroovyClassLoader groovyLoader = new GroovyClassLoader(); Class<Script> groovyClass = (Class<Script>) groovyLoader.parseClass(groovyScript); Script groovyScript = groovyClass.newInstance(); 每次執(zhí)行g(shù)roovyLoader.parseClass(groovyScript),Groovy 為了保證每次執(zhí)行的都是新的腳本內(nèi)容,會每次生成一個新名字的Class文件,這個點已經(jīng)在前文中說明過。當(dāng)對同一段腳本每次都執(zhí)行這個方法時,會導(dǎo)致的現(xiàn)象就是裝載的Class會越來越多,從而導(dǎo)致PermGen被用滿。
同時這里也存在性能瓶頸問題,如果去分析這段代碼會發(fā)現(xiàn)90%的耗時占用在Class
為了避免這一問題通常做法是緩存Script對象,從而避免以上2個問題。在這過程中通常又會引入新的問題:
- 高并發(fā)情況下,binding對象混亂導(dǎo)致計算出錯
在高并發(fā)的情況下,在執(zhí)行賦值binding對象后,真正執(zhí)行run操作時,拿到的binding對象可能是其它線程賦值的對象,所以出現(xiàn)數(shù)據(jù)計算混亂的情況
- 長時間運行仍然出現(xiàn)oom,無法解決Class
這點在上文中已經(jīng)提到,由于groovyClassLoader會緩存每次編譯groovy腳本的Class對象,下次編譯該腳本時,會優(yōu)先從緩存中讀取,這樣節(jié)省掉編譯的時間。導(dǎo)致被加載的Class對象因為存在引用而無法被卸載,雖然通過緩存避免了短時間內(nèi)大量生成新的class對象,但如果長時間運營仍然會存在問題。
比較好的做法是:
- 每個 script 都 new 一個 GroovyClassLoader 來裝載;
- 對于 parseClass 后生成的 Class 對象進(jìn)行cache,key 為 groovyScript 腳本的md5值。
CodeCache用滿,導(dǎo)致JIT禁用問題
對于大量使用Groovy的應(yīng)用,尤其是 Groovy 腳本還會經(jīng)常更新的應(yīng)用,由于這些Groovy腳本在執(zhí)行了很多次后都會被JVM編譯為 native 進(jìn)行優(yōu)化,會占據(jù)一些 CodeCache 空間,而如果這樣的腳本很多的話,可能會導(dǎo)致 CodeCache 被用滿,而 CodeCache 一旦被用滿,JVM 的 Compiler 就會被禁用,那性能下降的就不是一點點了
參考文獻(xiàn)
Groovy引發(fā)的PermGen區(qū)爆滿問題定位與解決
groovy腳本導(dǎo)致的FullGC問題
Groovy性能問題
Groovy的classloader加載機(jī)制喚起的頻繁GC
Groovy深入探索——Groovy的ClassLoader體系
總結(jié)
以上是生活随笔為你收集整理的Groovy与Java集成常见的坑的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Groovy与Java的不同点
- 下一篇: 【Java】MD5字符串的加密解密