1. 程式人生 > >Groovy與Java整合常見的坑

Groovy與Java整合常見的坑

摘要: groovy特性 Groovy是一門基於JVM的動態語言,同時也是一門面向物件的語言,語法上和Java非常相似。它結合了Python、Ruby和Smalltalk的許多強大的特性,Groovy 程式碼能夠與 Java 程式碼很好地結合,也能用於擴充套件現有程式碼。 Java作為一種通用、靜態型別的編譯型語

groovy特性

Groovy是一門基於JVM的動態語言,同時也是一門面向物件的語言,語法上和Java非常相似。它結合了Python、Ruby和Smalltalk的許多強大的特性,Groovy 程式碼能夠與 Java 程式碼很好地結合,也能用於擴充套件現有程式碼。

Java作為一種通用、靜態型別的編譯型語言有很多優勢,但同樣存在一些負擔:

  • 重新編譯太費工;
  • 靜態型別不夠靈活,重構起來時間可能比較長;
  • 部署的動靜太大;
  • java的語法天然不適用生產dsl;

相對於Java,它在編寫程式碼的靈活性上有非常明顯的提升,對於一個長期使用Java的開發者來說,使用Groovy時能夠明顯地感受到負身上的“枷鎖”輕了。Groovy是動態編譯語言,廣泛用作指令碼語言和快速原型語言,主要優勢之一就是它的生產力。Groovy 程式碼通常要比 Java 程式碼更容易編寫,而且編寫起來也更快,這使得它有足夠的資格成為開發工作包中的一個附件。

Java不是解決動態層問題的理想語言,這些動態層問題包括原型設計、指令碼處理等。可以把Groovy看作給Java靜態世界補充動態能力的語言,同時Groovy已經實現了java不具備的語言特性:

  • 函式字面值;
  • 對集合的一等支援;
  • 對正則表示式的一等支援;
  • 對xml的一等支援;

groovy與java整合的方式

重溫下Groovy呼叫Java方式,包括使用GroovyClassLoader、GroovyShell和GroovyScriptEngine。

GroovyClassLoader

用 Groovy 的 GroovyClassLoader ,動態地載入一個指令碼並執行它的行為。GroovyClassLoader是一個定製的類裝載器,負責解釋載入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表示式的值。您可使用Binding物件輸入引數給表示式,並最終通過GroovyShell返回Groovy表示式的計算結果。

GroovyShell shell = new GroovyShell();
Script groovyScript = shell.parse(new File(groovyFileName));
Object[] args = {};
groovyScript.invokeMethod("run", args);

GroovyScriptEngine

GroovyShell多用於推求對立的指令碼或表示式,如果換成相互關聯的多個指令碼,使用GroovyScriptEngine會更好些。GroovyScriptEngine從您指定的位置(檔案系統,URL,資料庫,等等)載入Groovy指令碼,並且隨著指令碼變化而重新載入它們。如同GroovyShell一樣,GroovyScriptEngine也允許您傳入引數值,並能返回指令碼的值。

Groovy程式碼檔案與class檔案的對應關係

而作為基於JVM的語言,Groovy可以非常容易的和Java進行互操作,但也需要編譯成class檔案後才能執行,所以瞭解Groovy程式碼檔案和class檔案的對應關係,有助於更好地理解Groovy的執行方式和結構。

對於沒有任何類定義

如果Groovy指令碼檔案裡只有執行程式碼,沒有定義任何類(class),則編譯器會生成一個Script的子類,類名和指令碼檔案的檔名一樣,而指令碼的程式碼會被包含在一個名為run的方法中,同時還會生成一個main方法,作為整個指令碼的入口。

對於僅有一個類

如果Groovy指令碼檔案裡僅含有一個類,而這個類的名字又和指令碼檔案的名字一致,這種情況下就和Java是一樣的,即生成與所定義的類一致的class檔案。

對於多個類

如果Groovy指令碼檔案含有多個類,groovy編譯器會很樂意地為每個類生成一個對應的class檔案。如果想直接執行這個指令碼,則腳本里的第一個類必須有一個static的main方法。

groovy與java整合中經常出現的問題

使用GroovyShell的parse方法導致perm區爆滿的問題

如果應用中內嵌Groovy引擎,會動態執行傳入的表示式並返回執行結果,而Groovy每執行一次指令碼,都會生成一個指令碼對應的class物件,並new一個InnerLoader去載入這個物件,而InnerLoader和指令碼物件都無法在gc的時候被回收執行一段時間後將perm佔滿,一直觸發fullgc。

  • 為什麼Groovy每執行一次指令碼,都會生成一個指令碼對應的class物件?

一個ClassLoader對於同一個名字的類只能載入一次,都由GroovyClassLoader載入,那麼當一個腳本里定義了C這個類之後,另外一個指令碼再定義一個C類的話,GroovyClassLoader就無法載入了。為什麼這裡會每次執行都會載入?

這是因為對於同一個groovy指令碼,groovy執行引擎都會不同的命名,且命名與時間戳有關係。當傳入text時,class物件的命名規則為:"script" + System.currentTimeMillis() + Math.abs(text.hashCode()) + ".groovy"。這就導致就算groovy指令碼未發生任何變化,每次執行parse方法都會新生成一個指令碼對應的class物件,且由GroovyClassLoader進行載入,不斷增大perm區。

  • 為什麼InnerLoader載入的對應無法通過gc清理掉?

大家都知道,JVM中的Class只有滿足以下三個條件,才能被GC回收,也就是該Class被解除安裝:1. 該類所有的例項都已經被GC,也就是JVM中不存在該Class的任何例項;2. 載入該類的ClassLoader已經被GC;3. 該類的java.lang.Class物件沒有在任何地方被引用,如不能在任何地方通過反射訪問該類的方法。

在GroovyClassLoader程式碼中有一個class物件的快取,進一步跟下去,發現每次編譯指令碼時都會在Map中快取這個物件,即:setClassCacheEntry(clazz)。每次groovy編譯指令碼後,都會快取該指令碼的Class物件,下次編譯該指令碼時,會優先從快取中讀取,這樣節省掉編譯的時間。這個快取的Map由GroovyClassLoader持有,key是指令碼的類名,這就導致每個指令碼對應的class物件都存在引用,無法被gc清理掉。

  • 如何解決?

請參考:Groovy引發的PermGen區爆滿問題定位與解決

如需更深入的理解GroovyClassLoader體系,請參考下面這篇文章Groovy深入探索——Groovy的ClassLoader體系

使用GroovyClassLoader載入機制導致頻繁gc問題

通常使用如下程式碼在Java 中執行 Groovy 指令碼:

GroovyClassLoader groovyLoader = new GroovyClassLoader();
Class<Script> groovyClass = (Class<Script>) groovyLoader.parseClass(groovyScript);
Script groovyScript = groovyClass.newInstance();

每次執行groovyLoader.parseClass(groovyScript),Groovy 為了保證每次執行的都是新的指令碼內容,會每次生成一個新名字的Class檔案,這個點已經在前文中說明過。當對同一段指令碼每次都執行這個方法時,會導致的現象就是裝載的Class會越來越多,從而導致PermGen被用滿。
同時這裡也存在效能瓶頸問題,如果去分析這段程式碼會發現90%的耗時佔用在Class

為了避免這一問題通常做法是快取Script物件,從而避免以上2個問題。在這過程中通常又會引入新的問題:

  • 高併發情況下,binding物件混亂導致計算出錯

在高併發的情況下,在執行賦值binding物件後,真正執行run操作時,拿到的binding物件可能是其它執行緒賦值的物件,所以出現數據計算混亂的情況

  • 長時間執行仍然出現oom,無法解決Class

這點在上文中已經提到,由於groovyClassLoader會快取每次編譯groovy指令碼的Class物件,下次編譯該指令碼時,會優先從快取中讀取,這樣節省掉編譯的時間。導致被載入的Class物件因為存在引用而無法被解除安裝,雖然通過快取避免了短時間內大量生成新的class物件,但如果長時間運營仍然會存在問題。

比較好的做法是:

  • 每個 script 都 new 一個 GroovyClassLoader 來裝載;
  • 對於 parseClass 後生成的 Class 物件進行cache,key 為 groovyScript 指令碼的md5值。

CodeCache用滿,導致JIT禁用問題

對於大量使用Groovy的應用,尤其是 Groovy 指令碼還會經常更新的應用,由於這些Groovy指令碼在執行了很多次後都會被JVM編譯為 native 進行優化,會佔據一些 CodeCache 空間,而如果這樣的指令碼很多的話,可能會導致 CodeCache 被用滿,而 CodeCache 一旦被用滿,JVM 的 Compiler 就會被禁用,那效能下降的就不是一點點了