歡迎來到啾啾的博客🐱。
記錄學習點滴。分享工作思考和實用技巧,偶爾也分享一些雜談💬。
有很多很多不足的地方,歡迎評論交流,感謝您的閱讀和評論😄。
目錄
- 引言
- 避免在性能敏感的熱點代碼中使用反射
- 緩存反射對象
- 使用setAccessible(true)
- 使用MethodHandle
- 演示
- 生成字節碼來避免反射
引言
總所周知,反射操作性能開銷相對較大。
然而,一些技巧可以顯著優化反射操作的性能。
資料引用:Java Reflection, 1000x Faster
避免在性能敏感的熱點代碼中使用反射
這是最重要的一條原則。因為反射涉及動態類型解析,會阻礙JVM的即時編譯(JIT)優化。在需要頻繁調用的代碼中,應盡量避免使用反射。
緩存反射對象
反射操作中,Class.forName()、Class.getMethod() 和 Class.getField() 這類查找操作非常耗時。如果需要多次對同一個類或方法進行反射操作,應該將查找到的 Class、Method、Field 和 Constructor 對象緩存起來,避免重復查找。
// 不推薦的方式
public void badReflection(Object obj) throws Exception {Method method = obj.getClass().getMethod("doSomething");method.invoke(obj);
}// 推薦的方式:使用Map緩存Method對象
private final Map<String, Method> methodCache = new ConcurrentHashMap<>();public void goodReflection(Object obj) throws Exception {String className = obj.getClass().getName();Method method = methodCache.computeIfAbsent(className, k -> {try {return obj.getClass().getMethod("doSomething");} catch (NoSuchMethodException e) {throw new RuntimeException(e);}});method.invoke(obj);
}
使用setAccessible(true)
當需要調用非公共(private, protected, default)的成員時,必須先調用 setAccessible(true) 來跳過Java語言的訪問權限檢查。這可以顯著提升反射調用的速度。
使用MethodHandle
從Java 7開始引入的 MethodHandle 提供了一種比反射更現代、性能更好的動態方法調用機制。它與反射不同,是“類型化”的,并且可以更好地被JVM優化。
常用于處理處理在編譯時未知,在運行時才確定的調用。
和反射區別如下:
特性 | 反射 (java.lang.reflect.Method) | 方法句柄 (java.lang.invoke.MethodHandle) |
---|---|---|
本質 | 描述方法元數據的信息類 | 指向方法字節碼的直接引用,像函數指針 |
性能 | 較慢,每次調用都有安全檢查和參數解包/打包開銷 | 首次查找有開銷,后續調用接近原生調用速度,可被JIT優化 |
類型安全 | 弱類型。invoke(Object, Object…) 接收任意對象,編譯時不檢查,運行時可能拋出類型轉換異常 | 強類型。句柄有明確的 MethodType,調用時如果類型不匹配,編譯或運行時會立即失敗 |
安全性 | 每次調用都檢查訪問權限 | 僅在創建句柄時檢查一次訪問權限 |
靈活性 | API簡單直觀 | API更復雜,但提供強大的句柄組合與轉換能力 |
- MethodHandle:直接指向方法的引用,調用時可以像普通方法一樣,并且可以被JIT編譯器優化。
- LambdaMetafactory:結合 MethodHandle 使用,可以創建出實現了特定函數式接口的Lambda實例,其性能幾乎等同于直接的方法調用。這種方式在運行時完全避免了反射的開銷。
演示
- 比如在編寫框架時,需要根據配置文件來調用某個對象的特定方法。
配置文件(config.json)
{"className": "com.example.UserValidator","methodToCall": "validate"
}
我們的程序它在編譯時完全不知道UserValidator這個類,也不知道validate這個方法。
此時可以使用MethodHandle來動態
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;// --- 假設這是用戶編寫的類 ---
class UserValidator {public boolean validate() {System.out.println("UserValidator is validating...");return true;}
}// --- 這是您編寫的框架代碼 ---
public class Framework {public void executeFromConfig(Object instance, String methodName) {System.out.println("Framework is about to call method '" + methodName + "' on instance " + instance.getClass().getSimpleName());try {// 1. 獲取一個查找上下文MethodHandles.Lookup lookup = MethodHandles.lookup();// 2. 動態確定方法的簽名(這里假設是無參,返回boolean)MethodType methodType = MethodType.methodType(boolean.class);// 3. 動態查找方法,得到一個MethodHandle// 這里的 instance.getClass() 和 methodName 都是在運行時才確定的!MethodHandle handle = lookup.findVirtual(instance.getClass(), methodName, methodType);// 4. 調用 handle.invoke() 或 invokeExact()// 這里告訴JVM:請在`instance`這個具體的對象上,執行`handle`所代表的方法。boolean result = (boolean) handle.invoke(instance);System.out.println("Execution result: " + result);} catch (Throwable t) {// 注意:invoke() 和 findVirtual() 都會拋出 Throwablet.printStackTrace();}}public static void main(String[] args) {Framework framework = new Framework();UserValidator userValidatorInstance = new UserValidator();// 框架根據運行時信息(比如配置文件),來調用對象的方法framework.executeFromConfig(userValidatorInstance, "validate");}
}
- 假設我們有兩個不同的類,但我們想用一個統一的框架來處理它們的某個方法
class Greeter {public String sayHello(String name) {return "Hello, " + name;}
}class Calculator {public int add(int a, int b) {return a + b;}
}public class AdvancedFramework {public static void main(String[] args) throws Throwable {MethodHandles.Lookup lookup = MethodHandles.lookup();// --- 場景一:調用 Greeter 的 sayHello 方法 ---Greeter greeter = new Greeter();// 1. 定義方法類型:(String) -> StringMethodType mtHello = MethodType.methodType(String.class, String.class);// 2. 查找句柄MethodHandle mhHello = lookup.findVirtual(Greeter.class, "sayHello", mtHello);// 3. 調用,第一個參數是實例本身,后面是方法的參數String result1 = (String) mhHello.invoke(greeter, "World");System.out.println("Greeter result: " + result1); // 輸出: Greeter result: Hello, World// --- 場景二:調用 Calculator 的 add 方法 ---Calculator calculator = new Calculator();// 1. 定義方法類型:(int, int) -> intMethodType mtAdd = MethodType.methodType(int.class, int.class, int.class);// 2. 查找句柄MethodHandle mhAdd = lookup.findVirtual(Calculator.class, "add", mtAdd);// 3. 調用int result2 = (int) mhAdd.invoke(calculator, 10, 20);System.out.println("Calculator result: " + result2); // 輸出: Calculator result: 30}
}
- 適配器模式-綁定參數
假設你有一個方法對象,但想預先填好它的一個參數,生成一個新的、參數更少的方法對象。這在事件處理等場景中非常有用。
import java.lang.invoke.*;public class BindExample {public void printMessage(String level, String message) {System.out.println("[" + level + "]: " + message);}public static void main(String[] args) throws Throwable {BindExample instance = new BindExample();MethodHandles.Lookup lookup = MethodHandles.lookup();// 原始方法句柄: (BindExample, String, String) -> voidMethodType mt = MethodType.methodType(void.class, String.class, String.class);MethodHandle originalHandle = lookup.findVirtual(BindExample.class, "printMessage", mt);// 我們想創建一個 "INFO" 級別的日志記錄器// 使用 bindTo 綁定第一個參數(實例)和第二個參數(level)MethodHandle infoLogger = originalHandle.bindTo(instance).bindTo("INFO");// 新的句柄類型變成了: (String) -> void// 現在調用新的句柄,只需要提供 message 參數infoLogger.invoke("This is an informational message."); // 輸出: [INFO]: This is an informational message.// 同樣,我們可以創建一個 "ERROR" 級別的日志記錄器MethodHandle errorLogger = originalHandle.bindTo(instance).bindTo("ERROR");errorLogger.invoke("A critical error occurred!"); // 輸出: [ERROR]: A critical error occurred!}
}
生成字節碼來避免反射
對于性能要求極高的場景,例如在框架和庫的開發中,可以通過直接生成字節碼來避免反射。像 ASM、cglib、Byte Buddy 等庫允許在運行時動態創建和修改類。雖然首次生成字節碼的開銷較大,但一旦生成,其執行速度就和普通的Java代碼完全一樣,并且可以被JIT充分優化。
字節碼增強技術內容有點多,感興趣的可以看這篇入門。
深入淺出 Byte Buddy:掌握 Java 運行時代碼操作的利器