阿里巴巴內部流出技術文章:Groovy與Java集成常見的坑

jopen 8年前發布 | 27K 次閱讀 Groovy Java Java開發

groovy特性

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

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

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

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

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

    • 函數字面值;
    • 對集合的一等支持;
    • 對正則表達式的一等支持;
    • 對xml的一等支持;
    • </ul>

      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對象?
      • </ul>

        一個ClassLoader對于同一個名字的類只能加載一次,都由GroovyClassLoader加載,那么當一個腳本里定義了C這個類之后,另外一個腳本再定義一個C類的話,GroovyClassLoader就無法加載了。為什么這里會每次執行都會加載?

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

        • 為什么InnerLoader加載的對應無法通過gc清理掉?
        • </ul>

          大家都知道,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清理掉。

          • 如何解決?
          • </ul>

            請參考: 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對象混亂導致計算出錯
            • </ul>

              在高并發的情況下,在執行賦值binding對象后,真正執行run操作時,拿到的binding對象可能是其它線程賦值的對象,所以出現數據計算混亂的情況

              • 長時間運行仍然出現oom,無法解決Class
              • </ul>

                這點在上文中已經提到,由于groovyClassLoader會緩存每次編譯groovy腳本的Class對象,下次編譯該腳本時,會優先從緩存中讀取,這樣節省掉編譯的時間。導致被加載的Class對象因為存在引用而無法被卸載,雖然通過緩存避免了短時間內大量生成新的class對象,但如果長時間運營仍然會存在問題。

                比較好的做法是: