摘要:介紹是使用字節(jié)碼生成來加強反射的性能。實現(xiàn)原理方法字節(jié)碼生成大致邏輯為通過反射獲取必要的函數(shù)名函數(shù)類型等信息。由于里面包含字節(jié)碼生成操作,所以相對來說這個函數(shù)是比較耗時的。
java編程中,使用反射來增強靈活性(如各類框架)、某些抽象(如各類框架)及減少樣板代碼(如Java Bean)。
因此,反射在實際的java項目中被大量使用。
由于項目里存在反射的性能瓶頸,使用的是ReflectASM高性能反射庫來優(yōu)化。
因此,在空閑時間研究了下的這個庫,并做了簡單的Beachmark。
ReflectASM是使用字節(jié)碼生成來加強反射的性能。
反射包含多種反射,這個庫很簡單,它提供的特性則是:
根據(jù)匹配的字符串操作成員變量。
根據(jù)匹配的字符串調(diào)用成員函數(shù)。
根據(jù)匹配的字符串調(diào)用構(gòu)造函數(shù)。
這三種也恰恰是實際使用中最多的,且在特殊場景下也容易產(chǎn)生性能問題。
例子舉個例子,使用MethodAccess來反射調(diào)用類的函數(shù):
Person person = new Person(); MethodAccess m = MethodAccess.get(Person.class); Object value = m.invoke(person, "getName");
更多的例子參考官方文檔,這個庫本身就不大,就幾個類。
實現(xiàn)原理 MethodAccess.get方法static public MethodAccess get (Class type) { ArrayListmethods = new ArrayList (); boolean isInterface = type.isInterface(); if (!isInterface) { Class nextClass = type; while (nextClass != Object.class) { addDeclaredMethodsToList(nextClass, methods); nextClass = nextClass.getSuperclass(); } } else { recursiveAddInterfaceMethodsToList(type, methods); } int n = methods.size(); String[] methodNames = new String[n]; Class[][] parameterTypes = new Class[n][]; Class[] returnTypes = new Class[n]; for (int i = 0; i < n; i++) { Method method = methods.get(i); methodNames[i] = method.getName(); parameterTypes[i] = method.getParameterTypes(); returnTypes[i] = method.getReturnType(); } String className = type.getName(); String accessClassName = className + "MethodAccess"; if (accessClassName.startsWith("java.")) accessClassName = "reflectasm." + accessClassName; Class accessClass; AccessClassLoader loader = AccessClassLoader.get(type); synchronized (loader) { try { accessClass = loader.loadClass(accessClassName); } catch (ClassNotFoundException ignored) { String accessClassNameInternal = accessClassName.replace(".", "/"); String classNameInternal = className.replace(".", "/"); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); MethodVisitor mv; /* ... 字節(jié)碼生成 */ byte[] data = cw.toByteArray(); accessClass = loader.defineClass(accessClassName, data); } } try { MethodAccess access = (MethodAccess)accessClass.newInstance(); access.methodNames = methodNames; access.parameterTypes = parameterTypes; access.returnTypes = returnTypes; return access; } catch (Throwable t) { throw new RuntimeException("Error constructing method access class: " + accessClassName, t); } }
大致邏輯為:
通過java反射獲取必要的函數(shù)名、函數(shù)類型等信息。
動態(tài)生成一個用于調(diào)用被反射對象的類,其為MethodAccess的子類。
反射生成動態(tài)生成的類,返回。
由于里面包含字節(jié)碼生成操作,所以相對來說這個函數(shù)是比較耗時的。
我們來分析一下,如果第二次調(diào)用對相同的類調(diào)用MethodAccess.get()方法,會不會好一些?
注意到:
synchronized (loader) { try { accessClass = loader.loadClass(accessClassName); } catch { /* ... */ } }
因此,如果這個動態(tài)生成的MethodAccess類已經(jīng)生成過,第二次調(diào)用MethodAccess.get是不會操作字節(jié)碼生成的。
但是,前面的一大堆準(zhǔn)備反射信息的操作依然會被執(zhí)行。所以,如果在代碼中封裝這樣的一個函數(shù)試圖使用ReflectASM庫:
Object reflectionInvoke(Object bean, String methodName) { MethodAccess m = MethodAccess.get(bean.getClass()); return m.invoke(bean, methodName); }
那么每次反射調(diào)用前都得執(zhí)行這么一大坨準(zhǔn)備反射信息的代碼,實際上還不如用原生反射呢。這個后面會有Beachmark。
為什么不在找不到動態(tài)生成的MethodAccess類時(即第一次調(diào)用)時,再準(zhǔn)備反射信息?這個得問作者。
動態(tài)生成的類 通過idea調(diào)試器獲取動態(tài)生成類的字節(jié)碼那么那個動態(tài)生成的類的內(nèi)部到底是什么?
由于這個類是動態(tài)生成的,所以獲取它的定義比較麻煩。
一開始我試圖尋找java的ClassLoader的API獲取它的字節(jié)碼,但是似乎沒有這種API。
后來,我想了一個辦法,直接在MethodAccess.get里面的這行代碼打斷點:
byte[] data = cw.toByteArray();
通過idea的調(diào)試器把data的內(nèi)容復(fù)制出來。但是這又遇到一個問題,data是二進(jìn)制內(nèi)容,根本復(fù)制不出來。
一個一年要400美刀的IDE,為啥不能做的貼心一點啊?
既然是二進(jìn)制內(nèi)容,那么只能設(shè)法將其編碼成文本再復(fù)制了。通過idea調(diào)試器自定義view的功能,將其編碼成base64后復(fù)制了出來。
然后,搞個python小腳本將其base64解碼回.class文件:
#!/usr/bin/env python3 import base64 with open("tmp.txt", "rb") as fi, open("tmp.class", "wb") as fo: base64.decode(fi, fo)
反編譯.class文件,得到:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package io.github.frapples.javademoandcookbook.commonutils.entity; import com.esotericsoftware.reflectasm.MethodAccess; public class PointMethodAccess extends MethodAccess { public PointMethodAccess() { } public Object invoke(Object var1, int var2, Object... var3) { Point var4 = (Point)var1; switch(var2) { case 0: return var4.getX(); case 1: var4.setX((Integer)var3[0]); return null; case 2: return var4.getY(); case 3: var4.setY((Integer)var3[0]); return null; case 4: return var4.toString(); case 5: return Point.of((Integer)var3[0], (Integer)var3[1], (String)var3[2]); default: throw new IllegalArgumentException("Method not found: " + var2); } } }
可以看到,生成的invoke方法中,直接根據(jù)索引使用switch直接調(diào)用。
所以,只要使用得當(dāng),性能媲美原生調(diào)用是沒有什么問題的。
來看invoke方法內(nèi)具體做了哪些操作:
abstract public Object invoke (Object object, int methodIndex, Object... args); /** Invokes the method with the specified name and the specified param types. */ public Object invoke (Object object, String methodName, Class[] paramTypes, Object... args) { return invoke(object, getIndex(methodName, paramTypes), args); } /** Invokes the first method with the specified name and the specified number of arguments. */ public Object invoke (Object object, String methodName, Object... args) { return invoke(object, getIndex(methodName, args == null ? 0 : args.length), args); } /** Returns the index of the first method with the specified name. */ public int getIndex (String methodName) { for (int i = 0, n = methodNames.length; i < n; i++) if (methodNames[i].equals(methodName)) return i; throw new IllegalArgumentException("Unable to find non-private method: " + methodName); }
如果通過函數(shù)名稱調(diào)用函數(shù)(即調(diào)用invoke(Object, String, Class[], Object...),
則MethodAccess是先遍歷所有函數(shù)名稱拿到索引,然后根據(jù)索引調(diào)用對應(yīng)方法(即調(diào)用虛函數(shù)invoke(Object, int, Object...),
實際上是通過多態(tài)調(diào)用字節(jié)碼動態(tài)生成的子類的對應(yīng)函數(shù)。
如果被反射調(diào)用的類的函數(shù)很多,則這個遍歷操作帶來的性能損失不能忽略。
所以,性能要求高的場合,應(yīng)該預(yù)先通過getIndex方法提前獲得索引,然后后面即可以直接使用invoke(Object, int, Object...)來調(diào)用。
談這種細(xì)粒度操作級別的性能問題,最有說服力的就是實際測試數(shù)據(jù)了。
下面,Talk is cheap, show you my beachmark.
首先是相關(guān)環(huán)境:
操作系統(tǒng)版本: elementary OS 0.4.1 Loki 64-bit
CPU: 雙核 Intel? Core? i5-7200U CPU @ 2.50GHz
JMH基準(zhǔn)測試框架版本: 1.21
JVM版本: JDK 1.8.0_181, OpenJDK 64-Bit Server VM, 25.181-b13
Benchmark Mode Cnt Score Error Units // 通過MethodHandle調(diào)用。預(yù)先得到某函數(shù)的MethodHandle ReflectASMBenchmark.javaMethodHandleWithInitGet thrpt 5 122.988 ± 4.240 ops/us // 通過java反射調(diào)用。緩存得到的Method對象 ReflectASMBenchmark.javaReflectWithCacheGet thrpt 5 11.877 ± 2.203 ops/us // 通過java反射調(diào)用。預(yù)先得到某函數(shù)的Method對象 ReflectASMBenchmark.javaReflectWithInitGet thrpt 5 66.702 ± 11.154 ops/us // 通過java反射調(diào)用。每次調(diào)用都先取得Method對象 ReflectASMBenchmark.javaReflectWithOriginGet thrpt 5 3.654 ± 0.795 ops/us // 直接調(diào)用 ReflectASMBenchmark.normalCall thrpt 5 1059.926 ± 99.724 ops/us // ReflectASM通過索引調(diào)用。預(yù)先取得MethodAccess對象,預(yù)先取得某函數(shù)的索引 ReflectASMBenchmark.reflectAsmIndexWithCacheGet thrpt 5 639.051 ± 47.750 ops/us // ReflectASM通過函數(shù)名調(diào)用,緩存得到的MethodAccess對象 ReflectASMBenchmark.reflectAsmWithCacheGet thrpt 5 21.868 ± 1.879 ops/us // ReflectASM通過函數(shù)名調(diào)用,預(yù)先得到的MethodAccess ReflectASMBenchmark.reflectAsmWithInitGet thrpt 5 53.370 ± 0.821 ops/us // ReflectASM通過函數(shù)名調(diào)用,每次調(diào)用都取得MethodAccess ReflectASMBenchmark.reflectAsmWithOriginGet thrpt 5 0.593 ± 0.005 ops/us
可以看到,每次調(diào)用都來一次MethodAccess.get,性能是最慢的,時間消耗是java原生調(diào)用的6倍,不如用java原生調(diào)用。
最快的則是預(yù)先取得MethodAccess和函數(shù)的索引并用索引來調(diào)用。其時間消耗僅僅是直接調(diào)用的2倍不到。
基準(zhǔn)測試代碼見:
https://github.com/frapples/j...
jmh框架十分專業(yè),在基準(zhǔn)測試前會做復(fù)雜的預(yù)熱過程以減少環(huán)境、優(yōu)化等影響,基準(zhǔn)測試也盡可能通過合理的迭代次數(shù)等方式來減小誤差。
所以,在默認(rèn)的迭代次數(shù)、預(yù)熱次數(shù)下,跑一次基準(zhǔn)測試的時間不短,CPU呼呼的轉(zhuǎn)。。。
在使用ReflectASM對某類進(jìn)行反射調(diào)用時,需要預(yù)先生成或獲取字節(jié)碼動態(tài)生成的MethodAccess子類對象。
這一操作是非常耗時的,所以正確的使用方法應(yīng)該是:
在某個利用反射的耗時函數(shù)啟動前,先預(yù)先生成這個MethodAccess對象。
如果是自己里面ReflectASM封裝工具類,則應(yīng)該設(shè)計緩存,緩存生成的MethodAccess對象。
如果不這樣做,這個ReflectASM用的沒有任何意義,性能還不如java的原生反射。
如果想進(jìn)一步提升性能,那么還應(yīng)該避免使用函數(shù)的字符串名稱來調(diào)用,而是在耗時的函數(shù)啟動前,預(yù)先獲取函數(shù)名稱對應(yīng)的整數(shù)索引。
在后面的耗時的函數(shù),使用這個整數(shù)索引進(jìn)行調(diào)用。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/72125.html
摘要:哪吒社區(qū)技能樹打卡打卡貼函數(shù)式接口簡介領(lǐng)域優(yōu)質(zhì)創(chuàng)作者哪吒公眾號作者架構(gòu)師奮斗者掃描主頁左側(cè)二維碼,加入群聊,一起學(xué)習(xí)一起進(jìn)步歡迎點贊收藏留言前情提要無意間聽到領(lǐng)導(dǎo)們的談話,現(xiàn)在公司的現(xiàn)狀是碼農(nóng)太多,但能獨立帶隊的人太少,簡而言之,不缺干 ? 哪吒社區(qū)Java技能樹打卡?【打卡貼 day2...
摘要:導(dǎo)讀閱讀本文需要有足夠的時間,筆者會由淺到深帶你一步一步了解一個資深架構(gòu)師所要掌握的各類知識點,你也可以按照文章中所列的知識體系對比自身,對自己進(jìn)行查漏補缺,覺得本文對你有幫助的話,可以點贊關(guān)注一下。目錄一基礎(chǔ)篇二進(jìn)階篇三高級篇四架構(gòu)篇五擴 導(dǎo)讀:閱讀本文需要有足夠的時間,筆者會由淺到深帶你一步一步了解一個資深架構(gòu)師所要掌握的各類知識點,你也可以按照文章中所列的知識體系對比自身,對自己...
摘要:面試通關(guān)要點匯總集部分解答說明如果你有幸能看到的話,本文整體框架來自阿里梁桂釗的博文,總結(jié)的非常不錯。這樣做的目的是對內(nèi)部數(shù)據(jù)進(jìn)行了不同級別的保護(hù),防止錯誤的使用了對象的私有部分。被繼承的類稱為基類和父類或超類。 showImg(https://segmentfault.com/img/remote/1460000013442471?w=1280&h=819); Java面試通關(guān)要點匯...
摘要:知識點總結(jié)反射反射機制性能問題知識點總結(jié)反射性能相關(guān)注意點啟用和禁用訪問安全檢查的開關(guān)值為則指示反射的對象在使用時應(yīng)該取消語言訪問檢查。并不是為就能訪問為就不能訪問。禁止安全檢查,可以提高反射的運行速度。 Java知識點總結(jié)(反射-反射機制性能問題) @(Java知識點總結(jié))[Java, 反射] 性能相關(guān)注意點: setAccessible 啟用和禁用訪問安全檢查的開關(guān),值為 tru...
閱讀 3160·2021-11-19 09:40
閱讀 3647·2021-11-16 11:52
閱讀 2980·2021-11-11 16:55
閱讀 3171·2019-08-30 15:55
閱讀 1177·2019-08-30 13:08
閱讀 1656·2019-08-29 17:03
閱讀 3012·2019-08-29 16:19
閱讀 2579·2019-08-29 13:43