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 就會被禁用,那性能下降的就不是一點點了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM