溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

發(fā)布時間:2020-07-06 17:18:49 來源:網(wǎng)絡(luò) 閱讀:221 作者:wx5d9ed7c8443c3 欄目:編程語言

前言

在實際工作中的一些特定應(yīng)用場景下,JAVA類反射是經(jīng)常用到、必不可少的技術(shù),在項目研發(fā)過程中,我們也遇到了不得不運用JAVA類反射技術(shù)的業(yè)務(wù)需求,并且不可避免地面臨這個技術(shù)固有的性能瓶頸問題。

通過近兩年的研究、嘗試和驗證,我們總結(jié)出一套利用緩存機(jī)制、大幅度提高JAVA類反射代碼運行效率的方法,和沒有優(yōu)化的代碼相比,性能提高了20~30倍。本文將與大家分享在探索和解決這個問題的過程中的一些有價值的心得體會與實踐經(jīng)驗。

一、簡述:JAVA類反射技術(shù)

首先,用最簡短的篇幅介紹JAVA類反射技術(shù)。

如果用一句話來概述,JAVA類反射技術(shù)就是

繞開編譯器,在運行期直接從虛擬機(jī)獲取對象實例/訪問對象成員變量/調(diào)用對象的成員函數(shù)。

抽象的概念不多講,用代碼說話……舉個例子,有這樣一個類:

public class ReflectObj {
    private String field01;
    public String getField01() {
        return this.field01;
    }
    public void setField01(String field01) {
        this.field01 = field01;
    }
}

如果按照下列代碼來使用這個類,就是傳統(tǒng)的“創(chuàng)建對象-調(diào)用”模式

   ReflectObj obj = new ReflectObj();
   obj.setField01("value01");
   System.out.println(obj.getField01());

如果按照如下代碼來使用它,就是“類反射”模式:

    // 直接獲取對象實例
    ReflectObj obj = ReflectObj.class.newInstance();
    // 直接訪問Field
    Field field = ReflectObj.class.getField("field01");
    field.setAccessible(true);
    field.set(obj, "value01");
    // 調(diào)用對象的public函數(shù)
    Method method = ReflectObj.class.getMethod("getField01");
    System.out.println((String) method.invoke(obj));

類反射屬于古老而基礎(chǔ)的JAVA技術(shù),本文不再贅述。
從上面的代碼可以看出:

  • 相比較于傳統(tǒng)的“創(chuàng)建對象-調(diào)用”模式,“類反射”模式的代碼更抽象、一般情況下也更加繁瑣;
  • 類反射繞開了編譯器的合法性檢測——比如訪問了一個不存在的字段、調(diào)用了一個不存在或不允許訪問的函數(shù),因為編譯器設(shè)立的防火墻失效了,編譯能夠通過,但是運行的時候會報錯;
  • 實際上,如果按照標(biāo)準(zhǔn)模式編寫類反射代碼,效率明顯低于傳統(tǒng)模式。在后面的章節(jié)會提到這一點。

二、緣起:為什么使用類反射

前文簡略介紹了JAVA類反射技術(shù),在與傳統(tǒng)的“創(chuàng)建對象-調(diào)用”模式對比時,提到了類反射的幾個主要弱點。但是在實際工作中,我們發(fā)現(xiàn)類反射無處不在,特別是在一些底層的基礎(chǔ)框架中,類反射是應(yīng)用最為普遍的核心技術(shù)之一。最常見的例子:Spring容器。

這是為什么呢?我們不妨從實際工作中的具體案例出發(fā),分析類反射技術(shù)的不可替代性。

大家?guī)缀趺刻於己豌y行打交道,通過銀行進(jìn)行存款、轉(zhuǎn)帳、取現(xiàn)等金融業(yè)務(wù),這些動賬操作都是通過銀行核心系統(tǒng)(包括交易核心/賬務(wù)核心/對外支付/超級網(wǎng)銀等模塊)完成的,因為歷史原因造成的技術(shù)路徑依賴,銀行核心系統(tǒng)的報文幾乎都是xml格式,而且以這種格式最為普遍:

<?xml version='1.0' encoding='UTF-8'?>
<service>
    <sys-header>
        <data name="SYS_HEAD">
            <struct>
                <data name="MODULE_ID">
                    <field type="string" length="2">RB</field>
                </data>
                <data name="USER_ID">
                    <field type="string" length="6">OP0001</field>
                </data>
                <data name="TRAN_TIMESTAMP">
                    <field type="string" length="9">003026975</field>
                </data>
                <!-- 其它字段略過 -->
            </struct>
        </data>
    </sys-header>
    <!-- 其它段落略過 -->
    <body>
        <data name="REF_NO">
            <field type="string" length="23">OPS18112400302633661837</field>
        </data>
    </body>
</service>

和常用的xml格式進(jìn)行對比:

<?xml version="1.0" encoding="UTF-8"?>
<recipe>
        <recipename>Ice Cream Sundae</recipename>
        <ingredlist>
            <listitem>
                <quantity>3</quantity>
                <itemdescription>chocolate syrup or chocolate fudge</itemdescription>
            </listitem>
            <listitem>
                <quantity>1</quantity>
                <itemdescription>nuts</itemdescription>
            </listitem>
            <listitem>
                <quantity>1</quantity>
                <itemdescription>cherry</itemdescription>
            </listitem>
        </ingredlist>
        <preptime>5 minutes</preptime>
</recipe>

銀行核心系統(tǒng)的xml報文不是用標(biāo)簽的名字區(qū)分元素,而是用屬性(name屬性)區(qū)分,在解析的時候,不管是用DOM、SAX,還是Digester或其它方案,都要用條件判斷語句、分支處理,偽代碼如下:

// ……
接口類實例 obj = new 接口類();
List<Node> nodeList = 獲取xml標(biāo)簽列表
for (Node node: nodeList) {
  if (node.getProperty("name") == "張三") obj.set張三 (node.getValue());
    else if (node.getProperty("name") == "李四") obj.set李四 (node.getValue());
    // ……
  }
// ……

顯而易見,這樣的代碼非常粗劣、不優(yōu)雅,每解析一個接口的報文,都要寫一個專門的類或者函數(shù),堆砌大量的條件分支語句,難寫、難維護(hù)。如果報文結(jié)構(gòu)簡單還好,如果有一百個甚至更多的字段,怎么辦?毫不夸張,在實際工作中,我遇到過一個銀行核心接口有140多個字段的情況,而且這還不是最多的!

三、試水:優(yōu)雅地解析XML

當(dāng)我們碰到這種結(jié)構(gòu)的xml、而且字段還特別多的時候,解決問題的鑰匙就是類反射技術(shù),基本思路是:

  • 從xml中解析出字段的name和value,以鍵值對的形式存儲起來;?
  • 用類反射的方法,用鍵值對的name找到字段或字段對應(yīng)的setter(這是有規(guī)律可循的);?
  • 然后把value直接set到字段,或者調(diào)用setter把值set到字段。

接口類應(yīng)該是這樣的結(jié)構(gòu):

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

nodes是存儲字段的name-value鍵值對的列表,MessageNode就是鍵值對,結(jié)構(gòu)如下:

public class MessageNode {
    private String name;
    private String value;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
    public MessageNode() {
        super();
    }
}
  • createNode是在解析xml的時候,把鍵值對添加到列表的函數(shù);?

  • initialize是用類反射方法,根據(jù)鍵值對初始化每個字段的函數(shù)。

這樣,解析xml的代碼可以變得非常優(yōu)雅、簡潔。如果用Digester解析之前列舉的那種格式的銀行報文,可以這樣寫:

?   Digester digester = new Digester();
    digester.setValidating(false);
    digester.addObjectCreate("service/sys-header", SysHeader.class);
    digester.addCallMethod("service/sys-header/data/struct/data", "createNode", 2);
    digester.addCallParam("service/sys-header/data/struct/data", 0, "name");
    digester.addCallParam("service/sys-header/data/struct/data/field", 1);
    parseObj = (SysHeader) digester.parse(new StringReader(msg));
    parseObj.initialize();

initialize函數(shù)的代碼,可以寫在一個基類里面,子類繼承基類即可。具體代碼如下:

public void initialize() {
     for (MessageNode node: nodes) {
        try {
            /**
             * 直接獲取字段、然后設(shè)置字段值
             */
            //String fieldName = StringUtils.camelCaseConvert(node.getName());
            // 只獲取調(diào)用者自己的field(private/protected/public修飾詞皆可)
            //Field field = this.getClass().getDeclaredField(fieldName);
            // 獲取調(diào)用者自己的field(private/protected/public修飾詞皆可)和從父類繼承的field(必須是public修飾詞)
            //Field field = this.getClass().getField(fieldName);
            // 把field設(shè)為可寫
            //field.setAccessible(true);
            // 直接設(shè)置field的值
            //field.set(this, node.getValue());
            /**
             * 通過setter設(shè)置字段值
             */
            Method method = this.getSetter(node.getName());
            // 調(diào)用setter
            method.invoke(this, node.getValue());
        } catch (Exception e) {
            log.debug("It's failed to initialize field: {}, reason: {}", node.getName(), e);
        };
    }
}

上面被注釋的段落是直接訪問Field的方式,下面的段落是調(diào)用setter的方式,兩種方法在效率上沒有差別??紤]到JAVA語法規(guī)范(書寫bean的規(guī)范),調(diào)用setter是更通用的辦法,因為接口類可能是被繼承、派生的,子類無法訪問父類用private關(guān)鍵字修飾的Field。getSetter函數(shù)很簡單,就是用Field的名字反推setter的名字,然后用類反射的辦法獲取setter。代碼如下:

 private Method getSetter(String fieldName) throws NoSuchMethodException, SecurityException {
    String methodName = String.format("set%s", StringUtils.upperFirstChar(fieldName));
    // 獲取field的setter,只要是用public修飾的setter、不管是自己的還是從父類繼承的,都能取到
    return this.getClass().getMethod(methodName, String.class);
 }

如果設(shè)計得好,甚至可以用一個解析函數(shù)處理所有的接口,這涉及到Digerser的運用技巧和接口類的設(shè)計技巧,本文不作深入講解。2017年,我們在一個和銀行有關(guān)的金融增值服務(wù)項目中使用了這個解決方案,取得了非常不錯的效果,之后在公司內(nèi)部推廣開來成為了通用技術(shù)架構(gòu)。經(jīng)過一年多的實踐,證明這套架構(gòu)性能穩(wěn)定、可靠,極大地簡化了代碼編寫和維護(hù)工作,顯著提高了生產(chǎn)效率。

四、問題:類反射性能差

但是,隨著業(yè)務(wù)量的增加,2018年末在進(jìn)行壓力測試的時候,發(fā)現(xiàn)解析xml的代碼占用CPU資源居高不下。進(jìn)一步分析、定位,發(fā)現(xiàn)問題出在類反射代碼上,在某些極端的業(yè)務(wù)場景下,甚至?xí)加?0%的CPU資源!這就提出了性能優(yōu)化的迫切要求。

類反射的性能優(yōu)化不是什么新課題,因此有一些成熟的第三方解決方案可以參考,比如運用比較廣泛的ReflectASM,據(jù)稱可以比未經(jīng)優(yōu)化的類反射代碼提高1/3左右的性能。

在研究了ReflectASM的源代碼以后,我們決定不使用現(xiàn)成的第三方解決方案,而是從底層入手、自行解決類反射代碼的優(yōu)化問題。主要基于兩點考慮

  • ReflectASM的基本技術(shù)原理,是在運行期動態(tài)分析類的結(jié)構(gòu),把字段、函數(shù)建立索引,然后通過索引完成類反射,技術(shù)上并不高深,性能也談不上完美;

  • 類反射是我們系統(tǒng)使用的關(guān)鍵技術(shù),使用場景、調(diào)用頻率都非常高,從自主掌握和控制基礎(chǔ)、核心技術(shù),實現(xiàn)系統(tǒng)的性能最優(yōu)化角度考慮,應(yīng)該盡量從底層技術(shù)出發(fā),獨立、可控地完成優(yōu)化工作。

五、思路和實踐:緩存優(yōu)化

前面提到ReflectASM給類的字段、函數(shù)建立索引,借此提高類反射效率。進(jìn)一步分析,這實際上是變相地緩存了字段和函數(shù)。那么,在我們面臨的業(yè)務(wù)場景下,能不能用緩存的方式優(yōu)化類反射代碼的效率呢?我們的業(yè)務(wù)場景需要以類反射的方式頻繁調(diào)用接口類的setter,這些setter都是用public關(guān)鍵字修飾的函數(shù),先是getMethod()、然后invoke()。基于以上特點,我們用如下邏輯和流程進(jìn)行了技術(shù)分析:

  • 用調(diào)試分析gongju統(tǒng)計出每一句類反射代碼的執(zhí)行耗時,結(jié)果發(fā)現(xiàn)性能瓶頸在getMethod();?
  • 分析JAVA虛擬機(jī)的內(nèi)存模型和管理機(jī)制,尋找解決問題的方向。JAVA虛擬機(jī)的內(nèi)存模型,可以從下面兩個維度來描述:

A.類空間/對象空間維度

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

B.堆/棧維度

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

  • 從JAVA虛擬機(jī)內(nèi)存模型可以看出,getMethod()需要從不連續(xù)的堆中檢索代碼段、定位函數(shù)入口,獲得了函數(shù)入口、invoke()之后就和傳統(tǒng)的函數(shù)調(diào)用差不多了,所以性能瓶頸在getMethod();?
  • 代碼段屬于類空間(也有資料將其描述為“函數(shù)空間”/“代碼空間”),類被加載后,除非虛擬機(jī)關(guān)閉,函數(shù)入口不會變化。那么,只要把setter函數(shù)的入口緩存起來,不就節(jié)約了getMethod()消耗的系統(tǒng)資源,進(jìn)而提高了類反射代碼的執(zhí)行效率嗎?

把接口類修改為這樣的結(jié)構(gòu)(標(biāo)紅的部分是新增或修改):

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

setterMap就是緩存字段setter的HashMap。為什么是兩層嵌套結(jié)構(gòu)呢?因為這個Map是寫在基類里面的靜態(tài)變量,每個從基類派生出的接口類都用它緩存setter,所以第一層要區(qū)分不同的接口類,第二層要區(qū)分不同的字段。如下圖所示:

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

當(dāng)ClassLoader加載基類時,創(chuàng)建setterMap(內(nèi)容為空):

 static {
    setterMap = new HashMap<String, Map<String, Method>>();
 }

這樣寫可以保證setterMap只被初始化一次。Initialize()函數(shù)作如下改進(jìn):

 public void initialize() {
        // 先檢查子類的setter是否被緩存
        String className = this.getClass().getName();
        if (setterMap.get(className) == null) setterMap.put(className, new HashMap<String, Method>());
        Map<String, Method> setters = setterMap.get(className);
        // 遍歷報文節(jié)點
        for (MessageNode node: nodes) {
            try {
                // 檢查對應(yīng)的setter是否被緩存了
                Method method = setters.get(node.getName());
                if (method == null) {
                    // 沒有緩存,先獲取、再緩存
                    method = this.getSetter(node.getName());
                    setters.put(node.getName(), method);
                }
                // 用類反射方式調(diào)用setter
                method.invoke(this, node.getValue());
            } catch (Exception e) {
                log.debug("It's failed to initialize field: {}, reason: {}", node.getName(), e);
            };
        }
    }

基本思路就是把setter緩存起來,通過MessageNode的name(字段的名字)找setter的入口地址,然后調(diào)用。因為只在初始化第一個對象實例的時候調(diào)用getMethod(),極大地節(jié)約了系統(tǒng)資源、提高了效率,測試結(jié)果也證實了這一點。

基本思路就是把setter緩存起來,通過MessageNode的name(字段的名字)找setter的入口地址,然后調(diào)用。

因為只在初始化第一個對象實例的時候調(diào)用getMethod(),極大地節(jié)約了系統(tǒng)資源、提高了效率,測試結(jié)果也證實了這一點。

六、驗證:測試方法和標(biāo)準(zhǔn)

1)先寫一個測試類,結(jié)構(gòu)如下:

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

2)在構(gòu)造函數(shù)中,用UUID初始化存儲鍵值對的列表nodes:

this.createNode("test001",String.valueOf(UUID.randomUUID().toString().hashCode()));
this.createNode("test002",String.valueOf(UUID.randomUUID().toString().hashCode()));
// ……

之所以用UUID,是保證每個實例、每個字段的值都不一樣,避免JAVA編譯器自動優(yōu)化代碼而破壞測試結(jié)果的原始性。

3)Initialize_ori()函數(shù)是用傳統(tǒng)的硬編碼方式直接調(diào)用setter的方法初始化實例字段,代碼如下:

 for (MessageNode node: this.nodes) {
      if (node.getName().equalsIgnoreCase("test001")) this.setTest001(node.getValue());
         else if (node.getName().equalsIgnoreCase("test002")) this.setTest002(node.getValue());
   // ……
 }

優(yōu)化效果就以它作為對照標(biāo)準(zhǔn)1,對照標(biāo)準(zhǔn)2就是沒有優(yōu)化的類反射代碼。

4)checkUnifomity()函數(shù)用來驗證:代碼是否用name-value鍵值對正確地初始化了各字段。

 for (MessageNode node: nodes) {
     if (node.getName().equalsIgnoreCase("test001") && !node.getValue().equals(this.test001)) return false;
        else if (node.getName().equalsIgnoreCase("test002") && !node.getValue().equals(this.test002)) return false;
            // ……
 }
 return true;

每一種優(yōu)化方案,我們都會用它驗證實例的字段是否正確,只要出現(xiàn)一次錯誤,該方案就會被否定。

5)創(chuàng)建100萬個TestInvoke類的實例,然后循環(huán)調(diào)用每一個實例的initialize_ori()函數(shù)(傳統(tǒng)的硬編碼,非類反射方法),記錄執(zhí)行耗時(只記錄初始化耗時,創(chuàng)建實例的耗時不記錄);再創(chuàng)建100萬個實例,循環(huán)調(diào)用每一個實例的類反射初始化函數(shù)(未優(yōu)化),記錄執(zhí)行耗時;再創(chuàng)建100萬個實例,改成調(diào)用優(yōu)化后的類反射初始化函數(shù),記錄執(zhí)行耗時。

6)以上是一個測試循環(huán),得到三種方法的耗時數(shù)據(jù),重復(fù)做10次,得到三組耗時數(shù)據(jù),把記錄下的數(shù)據(jù)去掉最大、最小值,剩下的求平均值,就是該方法的平均耗時。某一種方法的平均耗時越短則認(rèn)為該方法的效率越高。

7)為了進(jìn)一步驗證三種方法在不同負(fù)載下的效率變化規(guī)律,改成創(chuàng)建10萬個實例,重復(fù)5/6兩步,得到另一組測試數(shù)據(jù)。
測試結(jié)果顯示:在確保測試環(huán)境穩(wěn)定、一致的前提下,8個字段的測試實例、初始化100萬個對象,傳統(tǒng)方法(硬編碼)耗時850~1000毫秒;沒有優(yōu)化的類反射方法耗時23000~25000毫秒;優(yōu)化后的類反射代碼耗時600~800毫秒。10萬個測試對象的情況,三種方法的耗時也大致是這樣的比例關(guān)系。這個數(shù)據(jù)取決于測試環(huán)境的資源狀況,不同的機(jī)器、不同時刻的測試,結(jié)果都有出入,但總的規(guī)律是穩(wěn)定的。
基于測試結(jié)果,可以得出這樣的結(jié)論:緩存優(yōu)化的類反射代碼比沒有優(yōu)化的代碼效率提高30倍左右,比傳統(tǒng)的硬編碼方法提高了10~20%。有必要強(qiáng)調(diào)的是,這個結(jié)論偏向保守。和ReflecASM相比,性能大幅度提高也是毋庸置疑的。

七、第一次迭代:忽略字段

緩存優(yōu)化的效果非常好,但是,這個方案真的完美無缺了么?
經(jīng)過分析,我們發(fā)現(xiàn):如果數(shù)據(jù)更復(fù)雜一些,這個方案的缺陷就暴露了。比如鍵值對列表里的值在接口類里面并沒有定義對應(yīng)的字段,或者是沒有對應(yīng)的、可以訪問的setter,性能就會明顯下降。
這種情況在實際業(yè)務(wù)中是很常見的,比如對接銀行核心接口,往往并不需要解析報文的全部字段,很多字段是可以忽略的,所以接口類里面不用定義這些字段,但解析代碼依然會把這些鍵值對全部解析出來,這時就會給優(yōu)化代碼造成麻煩了。
分析過程如下:

1)舉例而言,如果鍵值對里有兩個值在接口類(Interface01)并未定義,假定名字是fieldX、filedY,第一次執(zhí)行initialize()函數(shù):

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

初始狀態(tài)下,setterMap檢索不到Interface01類的setter緩存,initialize()函數(shù)會在第一次執(zhí)行的時候,根據(jù)鍵值對的名字(field01/field02/……/fieldN/fieldX/fieldY)調(diào)用getMethod()函數(shù)、初始化sertter引用的緩存。因為fieldX和fieldY字段不存在,找不到它們對應(yīng)的setter,緩存里也沒有它們的引用。

2)第二次執(zhí)行initialize()函數(shù)(也就是初始化第二個對象實例),field01/field02/……/fieldN鍵值對都能在緩存中找到setter的引用,調(diào)用速度很快;但緩存里找不到fieldX/fieldY的setter的引用,于是再次調(diào)用getMethod()函數(shù),而因為它們的setter根本不存在(連這兩個字段都不存在),做的是無用功,setterMap的狀態(tài)沒有變化。

3)第三次、第四次……第N次,都是如此,白白消耗系統(tǒng)資源,運行效率必然下降。

測試結(jié)果印證了這個推斷:在TestInvoke的構(gòu)造函數(shù)增加了兩個不存在對應(yīng)字段和setter的鍵值對(姑且稱之為“無效鍵值對”),進(jìn)行100萬個實例的初始化測試,經(jīng)過優(yōu)化的類反射代碼,耗時從原來的600~800毫秒,增加到7000~8000毫秒,性能下降10倍左右。如果增加更多的鍵值對(不存在對應(yīng)字段),性能下降更嚴(yán)重。所以必須進(jìn)一步完善優(yōu)化代碼。為了加以區(qū)分,我們把之前的優(yōu)化代碼稱為V1版;進(jìn)一步完善的代碼稱為V2版。

怎么完善?從上面的分析不難找到思路:增加忽略字段(ignore field)緩存。

基類BaseModel作如下修改(標(biāo)紅部分是新增或者修改),增加了ignoreMap:

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

ignoreMap的數(shù)據(jù)結(jié)構(gòu)類似于setterMap,但第二層不是HashMap,而是Set,緩存每個子類需要忽略的鍵值對的名字,使用Set更節(jié)約系統(tǒng)資源,如下圖所示:

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

同樣的,當(dāng)ClassLoader加載基類的時候,創(chuàng)建ignoreMap(內(nèi)容為空):

 static {
        setterMap = new HashMap<String, Map<String, Method>>();
        ignoreMap = new HashMap<String, Set<String>>();
    }

Initialize()函數(shù)作如下改進(jìn):

public void initialize() {
    // 先檢查子類的setter是否被緩存
    String className = this.getClass().getName();
    if (setterMap.get(className) == null) {
        setterMap.put(className, new HashMap<String, Method>());
    }
    if (ignoreMap.get(className) == null) {
        ignoreMap.put(className, new HashSet<String>());
    }
    Map<String, Method> setters = setterMap.get(className);
    Set<String> ignores = ignoreMap.get(className);
    // 遍歷報文節(jié)點
    for (MessageNode node : nodes) {
        String sName = node.getName();
        try {
            // 檢查該字段是否被忽略
            if (ignores.contains(sName)) {
                continue;
            }
            // 檢查對應(yīng)的setter是否被緩存了
            Method method = setters.get(sName);
            if (method == null) {
                // 沒有緩存,先獲取、再緩存
                method = this.getSetter(sName);
                setters.put(sName, method);
            }
            // 用類反射方式調(diào)用setter
            method.invoke(this, node.getValue());
        } catch (NoSuchMethodException | SecurityException e) {
            log.debug("It's failed to initialize field: {}, reason: {}", sName, e);
            // 找不到對應(yīng)的setter,放到忽略字段集合,以后不再嘗試
            ignores.add(sName);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            log.error("It's failed to initialize field: {}, reason: {}", sName, e);
            try {
                // 不能調(diào)用setter,可能是虛擬機(jī)回收了該子類的全部實例、入口地址變化,更新地址、再試一次
                Method method = this.getSetter(sName);
                setters.put(sName, method);
                method.invoke(this, node.getValue());
            } catch (Exception e1) {
                log.debug("It's failed to initialize field: {}, reason: {}", sName, e1);
            }
        } catch (Exception e) {
            log.error("It's failed to initialize field: {}, reason: {}", sName, e);
        }
    }
}

雖然代碼復(fù)雜了一些,但思路很簡單:用鍵值對的名字尋找對應(yīng)的setter時,如果找不到,就把它放進(jìn)ignoreMap,下次不再找了。另外還增加了對setter引用失效的處理。雖然理論上說“只要虛擬機(jī)不重啟,setter的入口引用永遠(yuǎn)不會變”,在測試中也從來沒有遇到過這種情況,但為了覆蓋各種異常情況,還是增加了這段代碼。

繼續(xù)沿用前面的例子,分析改進(jìn)后的代碼的工作流程:

1)第一次執(zhí)行initialize()函數(shù),實例的狀態(tài)是這樣變化的:

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

因為fieldX和fieldY字段不存在,找不到它們對應(yīng)的setter,它們被放到ignoreMap中。

2)再次調(diào)用initialize()函數(shù)的時候,因為檢查到ignoreMap中存在fieldX和fieldY,這兩個鍵值對被跳過,不再徒勞無功地調(diào)用getMethod();其它邏輯和V1版相同,沒有變化。

還是用上面提到的TestInvoke類作驗證(8個字段+2個無效鍵值對),V2版本雖然代碼更復(fù)雜了,但100萬條紀(jì)錄的初始化耗時為600~800毫秒,V1版代碼這個時候的耗時猛增到7000~8000毫秒。哪怕增加更多的無效鍵值對,V2版代碼耗時增加也不明顯,而這種情況下V1版代碼的效率還會進(jìn)一步下降。

至此,對JAVA類反射代碼的優(yōu)化已經(jīng)比較完善,覆蓋了各種異常情況,如前所述,我們把這個版本稱為V2版。

八、第二次迭代:逆向思維

這樣就代表優(yōu)化工作已經(jīng)做到最好了嗎?不是這樣的。

仔細(xì)觀察V1、V2版的優(yōu)化代碼,都是循環(huán)遍歷鍵值對,用鍵值對的name(和字段的名字相同)推算setter的函數(shù)名,然后去尋找setter的入口引用。第一次是調(diào)用類反射的getMethod()函數(shù),以后是從緩存里面檢索,如果存在無效鍵值對,那就必然出現(xiàn)空轉(zhuǎn)循環(huán),哪怕是V2版代碼,ignoreMap也不能避免這種空轉(zhuǎn)循環(huán)。雖然單次空轉(zhuǎn)循環(huán)耗時非常短,但在無效鍵值對比較多、負(fù)載很大的情況下,依然有無效的資源開銷。

如果采用逆向思維,用setter去反推、檢索鍵值對,又會如何?

先分析業(yè)務(wù)場景以及由業(yè)務(wù)場景所決定的數(shù)據(jù)結(jié)構(gòu)特點:

  • 接口類的字段數(shù)量可能大于setter函數(shù)的數(shù)量,因為可能需要一些內(nèi)部使用的功能性字段,并不是從xml報文里解析出來的;?
  • xml報文里解析出的鍵值對和字段是交集關(guān)系,多數(shù)情況下,鍵值對的數(shù)量包含了接口類的字段,并且大概率存在一些不需要的鍵值對;?
  • 相比較字段,setter函數(shù)和需要解析的鍵值對最接近于一一對應(yīng)關(guān)系,出現(xiàn)空轉(zhuǎn)循環(huán)的概率最?。?
  • 因為接口類編寫要遵守JAVA編程規(guī)范,從setter函數(shù)的名字反推字段的名字,進(jìn)而檢索鍵值對,是可行、可靠的。

綜上所述,逆向思維用setter函數(shù)反推、檢索鍵值對,初始化接口類,就是第二次迭代的具體方向。

需要把接口類修改成這樣的結(jié)構(gòu)(標(biāo)紅的部分是新增或者修改):

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

1)為了便于逆向檢索鍵值對,nodes字段改成HashMap,key是鍵值對的名字、value是鍵值對的值。

2)為了提高循環(huán)遍歷的速度,setterMap的第二層改成鏈表,鏈表的成員是內(nèi)部類FieldSetter,結(jié)構(gòu)如下:

private class FieldSetter {

    private String name;
    private Method method;

    public String getName() {
        return name;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public FieldSetter(String name, Method method) {
        super();
        this.name = name;
        this.method = method;
    }
}

setterMap的第二層繼續(xù)使用HashMap也能實現(xiàn)功能,但循環(huán)遍歷的效率,HashMap不如鏈表,所以我們改用鏈表。

3)同樣的,setterMap在基類被加載的時候創(chuàng)建(內(nèi)容為空):

 static {
     setterMap = new HashMap<String, List<FieldSetter>>();
 }

4)第一次初始化某個接口類的實例時,調(diào)用initSetters()函數(shù),初始化setterMap:

protected List<FieldSetter> initSetters() {
    String className = this.getClass().getName();
    List<FieldSetter> setters = new ArrayList<FieldSetter>();
    // 遍歷類的可調(diào)用函數(shù)
    for (Method method : this.getClass().getMethods()) {
        String methodName = method.getName();
        // 如果從名字推斷是setter函數(shù),添加到setter函數(shù)列表
        if (methodName.startsWith("set")) {
            // 反推field的名字
            String fieldName = StringUtils.lowerFirstChar(methodName.substring(3));
            setters.add(new FieldSetter(fieldName, method));
        }
    }
    // 緩存類的setter函數(shù)列表
    setterMap
.put(className, setters);
    // 返回可調(diào)用的setter函數(shù)列表
    return setters;
}

5)Initialize()函數(shù)修改為如下邏輯:

public void initialize() {
    // 從緩存獲取接口類的setter列表
    List<FieldSetter> setters = setterMap.get(this.getClass().getName());
    // 如果還沒有緩存、初始化接口類的setter列表
    if (setters == null) {
        setters = this.initSetters();
    }
    // 遍歷接口類的setter
    for (FieldSetter setter : setters) {
        // 用setter的名字(也就是字段的名字)檢索鍵值對
        String fieldName = setter.getName();
        String fieldValue = nodes.get(fieldName);
        // 沒有檢索到鍵值對、或者鍵值對沒有賦值,跳過
        if (StringUtils.isEmpty(fieldValue)) {
            continue;
        }
        try {
            Method method = setter.getMethod();
            // 用類反射方式調(diào)用setter
            method.invoke(this, fieldValue);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            log.error("It's failed to initialize field: {}, reason: {}", fieldName, e);
            // 不能調(diào)用setter,可能是虛擬機(jī)回收了該子類的全部實例、入口地址變化,更新地址、再試一次
            try {
                Method method = this.getSetter(fieldName);
                setter.setMethod(method);
                method.invoke(this, fieldValue);
            } catch (Exception e1) {
                log.debug("It's failed to initialize field: {}, reason: {}", fieldName, e1);
            }
        } catch (Exception e) {
            log.error("It's failed to initialize field: {}, reason: {}", fieldName, e);
        }
    }
}

不妨把這版代碼稱為V3……繼續(xù)沿用前面TestInvoke的例子,分析改進(jìn)后代碼的工作流程:

1)第一次執(zhí)行initialize()函數(shù),實例的狀態(tài)是這樣變化的:

如何利用緩存機(jī)制實現(xiàn)JAVA類反射性能提升30倍

通過setterMap反向檢索鍵值對的值,fieldX、fieldY因為不存在對應(yīng)的setter,不會被檢索,避免了空轉(zhuǎn)。

2)之后每一次初始化對象實例,都不需要再初始化setterMap,也不會消耗任何資源去檢索fieldX、fieldY,最大限度地節(jié)省資源開銷。

3)因為取消了ignoreMap,取消了V2版判斷字段是否應(yīng)該被忽略的邏輯,代碼更簡潔,也能節(jié)約一部分資源。

結(jié)果數(shù)據(jù)顯示:用TestInvoke測試類、8個setter+2個無效鍵值對的情況下,進(jìn)行100萬/10萬個實例兩個量級的對比測試,V3版比V2版性能最多提高10%左右,100萬實例初始化耗時550~720毫秒。如果增加無效鍵值對的數(shù)量,性能提高更為明顯;沒有無效鍵值對的最理想情況下,V1、V2、V3版本的代碼效率沒有明顯差別。

至此,用緩存機(jī)制優(yōu)化類反射代碼的嘗試,已經(jīng)比較接近最優(yōu)解了,V3版本的代碼可以視為到目前為止最好的版本。

九、總結(jié)和思考:方法論

總結(jié)過去兩年圍繞著JAVA類反射性能優(yōu)化這個課題,我們所進(jìn)行的探索和研究,提高到方法論層面,可以提煉出一個分析問題、解決問題的思路和流程,供大家參考:

1)從實踐中來
多數(shù)情況下,探索和研究的課題并不是坐在書齋里憑空想出來的,而是在實際工作中遇到具體的技術(shù)難點,在現(xiàn)實需求的驅(qū)動下發(fā)現(xiàn)需要研究的問題。

以本文為例,如果不是在對接銀行核心系統(tǒng)的時候遇到了大量的、格式奇特的xml報文,不會促使我們嘗試用類反射技術(shù)去優(yōu)雅地解析報文,也就不會面對類反射代碼執(zhí)行效率低的問題,自然不會有后續(xù)的研究成果。

2)拿出手術(shù)刀,解剖一只麻雀
在實踐中遇到了困難,首先要分析和研究面對的問題,不能著急,要有解剖一只麻雀的精神,抽絲剝繭,把問題的根源找出來。

這個過程中,邏輯分析和實操驗證都是必不可少的。沒有高屋建瓴的分析,就容易迷失大方向;沒有實操驗證,大概率會陷入坐而論道、腦補(bǔ)的怪圈。還是那句話:實踐是最寶貴的財富,也是驗證一切構(gòu)想的終極考官,是我們認(rèn)識世界改造世界的力量源泉。但我們也不能陷入庸俗的經(jīng)驗主義,不管怎么說,這個世界的基石是有邏輯的。

回到本文的案例,我們一方面研究JAVA內(nèi)存模型,從理論上探尋類反射代碼效率低下的原因;另一方面也在實務(wù)層面,用實實在在的時間戳驗證了JAVA類反射代碼的耗時分布。理論和實踐的結(jié)合,才能讓我們找到解決問題的正確方向,二者不可偏廢。

3)頭腦風(fēng)暴,勇于創(chuàng)新
分析問題,找到關(guān)鍵點,接下來就是尋找解決方案。JAVA程序員有一個很大的優(yōu)勢,同時也是很大的劣勢:第三方解決方案非常豐富。JAVA生態(tài)比較完善,我們面臨的麻煩和問題幾乎都有成熟的第三方解決方案,“吃現(xiàn)成的”是優(yōu)勢也是劣勢,很多時候,我們的創(chuàng)造力也因此被扼殺。所以,當(dāng)面臨高價值需求的時候,應(yīng)該拿出大無畏的勇氣,啃硬骨頭,做底層和原創(chuàng)的工作。

就本文案例而言,ReflexASM就是看起來很不錯的方案,比傳統(tǒng)的類反射代碼性能提升了至少三分之一。但是,它真的就是最優(yōu)解么?我們的實踐否定了這一點。JAVA程序員要有吃苦耐勞、以底層技術(shù)為原點解決問題的精神,否則你就會被別人所綁架,失去尋求技術(shù)自由空間的機(jī)會。中國的軟件行業(yè)已經(jīng)發(fā)展到了這個階段,提出了這樣的需求,我們應(yīng)該順應(yīng)歷史潮流。

4)螺旋式發(fā)展,波浪式前進(jìn)
研究問題和解決問題,迭代是非常有效的工作方法。首先,要有精益求精的態(tài)度,不斷改進(jìn),逼近最優(yōu)方案,迭代必不可少。其次,對于比較復(fù)雜的問題,不要追求畢其功于一役,把一個大的目標(biāo)拆分成不同階段,分步實施、逐漸推進(jìn),這種情況下,迭代更是解決問題的必由之路。

我們解決JAVA類反射代碼的優(yōu)化問題,就是經(jīng)過兩次迭代、寫了三個版本,才得到最終的結(jié)果,逼近了最優(yōu)解。在迭代的過程中會逐漸發(fā)現(xiàn)一些之前忽略的問題,這就是寶貴的經(jīng)驗,這些經(jīng)驗在解決其他技術(shù)問題時也能發(fā)揮作用。比如HashMap的數(shù)據(jù)結(jié)構(gòu)非常合理、經(jīng)典,平時使用的時候效率是很高的,如果不是迭dai開發(fā)、逼近極限的過程,我們又怎么可能發(fā)現(xiàn)在循環(huán)遍歷狀態(tài)下、它的性能不如鏈表呢?

行文至此,文章也快要寫完了,細(xì)心的讀者一定會有一個疑問:自始至終,舉的例子、類的字段都是String類型,類反射代碼根本沒有考慮setter的參數(shù)類型不同的情況。確實是這樣的,因為我們解決的是銀行核心接口報文解析的問題,接口字段全部是String,沒有其它數(shù)據(jù)類型。

其實,對類反射技術(shù)的研究深入到這個程度,解決這個問題、并且維持代碼的高效率,易如反掌。比如,給FieldSetter類增加一個數(shù)據(jù)類型的字段,初始化setterMap的時候把接口類對應(yīng)的字段的數(shù)據(jù)類型解析出來,和setter函數(shù)的入口一起緩存,類反射調(diào)用setter時,把參數(shù)格式轉(zhuǎn)換一下,就可以了。限于篇幅、這個問題就不展開了,感興趣的讀者可以自己嘗試一下。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI