日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

java 反射 性能_java高性能反射及性能对比

發(fā)布時(shí)間:2024/4/13 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java 反射 性能_java高性能反射及性能对比 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

java編程中,使用反射來增強(qiáng)靈活性(如各類框架)、某些抽象(如各類框架)及減少樣板代碼(如Java Bean)。

因此,反射在實(shí)際的java項(xiàng)目中被大量使用。

由于項(xiàng)目里存在反射的性能瓶頸,使用的是ReflectASM高性能反射庫(kù)來優(yōu)化。

因此,在空閑時(shí)間研究了下的這個(gè)庫(kù),并做了簡(jiǎn)單的Beachmark。

介紹

ReflectASM是使用字節(jié)碼生成來加強(qiáng)反射的性能。

反射包含多種反射,這個(gè)庫(kù)很簡(jiǎn)單,它提供的特性則是:

根據(jù)匹配的字符串操作成員變量。

根據(jù)匹配的字符串調(diào)用成員函數(shù)。

根據(jù)匹配的字符串調(diào)用構(gòu)造函數(shù)。

這三種也恰恰是實(shí)際使用中最多的,且在特殊場(chǎng)景下也容易產(chǎn)生性能問題。

例子

舉個(gè)例子,使用MethodAccess來反射調(diào)用類的函數(shù):

Person person = new Person();

MethodAccess m = MethodAccess.get(Person.class);

Object value = m.invoke(person, "getName");

更多的例子參考官方文檔,這個(gè)庫(kù)本身就不大,就幾個(gè)類。

實(shí)現(xiàn)原理

MethodAccess.get方法

static public MethodAccess get (Class type) {

ArrayList methods = 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ù)類型等信息。

動(dòng)態(tài)生成一個(gè)用于調(diào)用被反射對(duì)象的類,其為MethodAccess的子類。

反射生成動(dòng)態(tài)生成的類,返回。

由于里面包含字節(jié)碼生成操作,所以相對(duì)來說這個(gè)函數(shù)是比較耗時(shí)的。

我們來分析一下,如果第二次調(diào)用對(duì)相同的類調(diào)用MethodAccess.get()方法,會(huì)不會(huì)好一些?

注意到:

synchronized (loader) {

try {

accessClass = loader.loadClass(accessClassName);

} catch {

/* ... */

}

}

因此,如果這個(gè)動(dòng)態(tài)生成的MethodAccess類已經(jīng)生成過,第二次調(diào)用MethodAccess.get是不會(huì)操作字節(jié)碼生成的。

但是,前面的一大堆準(zhǔn)備反射信息的操作依然會(huì)被執(zhí)行。所以,如果在代碼中封裝這樣的一個(gè)函數(shù)試圖使用ReflectASM庫(kù):

Object reflectionInvoke(Object bean, String methodName) {

MethodAccess m = MethodAccess.get(bean.getClass());

return m.invoke(bean, methodName);

}

那么每次反射調(diào)用前都得執(zhí)行這么一大坨準(zhǔn)備反射信息的代碼,實(shí)際上還不如用原生反射呢。這個(gè)后面會(huì)有Beachmark。

為什么不在找不到動(dòng)態(tài)生成的MethodAccess類時(shí)(即第一次調(diào)用)時(shí),再準(zhǔn)備反射信息?這個(gè)得問作者。

動(dòng)態(tài)生成的類

通過idea調(diào)試器獲取動(dòng)態(tài)生成類的字節(jié)碼

那么那個(gè)動(dòng)態(tài)生成的類的內(nèi)部到底是什么?

由于這個(gè)類是動(dòng)態(tài)生成的,所以獲取它的定義比較麻煩。

一開始我試圖尋找java的ClassLoader的API獲取它的字節(jié)碼,但是似乎沒有這種API。

后來,我想了一個(gè)辦法,直接在MethodAccess.get里面的這行代碼打斷點(diǎn):

byte[] data = cw.toByteArray();

通過idea的調(diào)試器把data的內(nèi)容復(fù)制出來。但是這又遇到一個(gè)問題,data是二進(jìn)制內(nèi)容,根本復(fù)制不出來。

一個(gè)一年要400美刀的IDE,為啥不能做的貼心一點(diǎn)啊?

既然是二進(jìn)制內(nèi)容,那么只能設(shè)法將其編碼成文本再?gòu)?fù)制了。通過idea調(diào)試器自定義view的功能,將其編碼成base64后復(fù)制了出來。

然后,搞個(gè)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)用是沒有什么問題的。

MethodAccess.invoke方法

來看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)用對(duì)應(yīng)方法(即調(diào)用虛函數(shù)invoke(Object, int, Object...),

實(shí)際上是通過多態(tài)調(diào)用字節(jié)碼動(dòng)態(tài)生成的子類的對(duì)應(yīng)函數(shù)。

如果被反射調(diào)用的類的函數(shù)很多,則這個(gè)遍歷操作帶來的性能損失不能忽略。

所以,性能要求高的場(chǎng)合,應(yīng)該預(yù)先通過getIndex方法提前獲得索引,然后后面即可以直接使用invoke(Object, int, Object...)來調(diào)用。

Beachmark

談這種細(xì)粒度操作級(jí)別的性能問題,最有說服力的就是實(shí)際測(cè)試數(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)測(cè)試框架版本: 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對(duì)象

ReflectASMBenchmark.javaReflectWithCacheGet thrpt 5 11.877 ± 2.203 ops/us

// 通過java反射調(diào)用。預(yù)先得到某函數(shù)的Method對(duì)象

ReflectASMBenchmark.javaReflectWithInitGet thrpt 5 66.702 ± 11.154 ops/us

// 通過java反射調(diào)用。每次調(diào)用都先取得Method對(duì)象

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對(duì)象,預(yù)先取得某函數(shù)的索引

ReflectASMBenchmark.reflectAsmIndexWithCacheGet thrpt 5 639.051 ± 47.750 ops/us

// ReflectASM通過函數(shù)名調(diào)用,緩存得到的MethodAccess對(duì)象

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,性能是最慢的,時(shí)間消耗是java原生調(diào)用的6倍,不如用java原生調(diào)用。

最快的則是預(yù)先取得MethodAccess和函數(shù)的索引并用索引來調(diào)用。其時(shí)間消耗僅僅是直接調(diào)用的2倍不到。

jmh框架十分專業(yè),在基準(zhǔn)測(cè)試前會(huì)做復(fù)雜的預(yù)熱過程以減少環(huán)境、優(yōu)化等影響,基準(zhǔn)測(cè)試也盡可能通過合理的迭代次數(shù)等方式來減小誤差。

所以,在默認(rèn)的迭代次數(shù)、預(yù)熱次數(shù)下,跑一次基準(zhǔn)測(cè)試的時(shí)間不短,CPU呼呼的轉(zhuǎn)。。。

最后總結(jié)

在使用ReflectASM對(duì)某類進(jìn)行反射調(diào)用時(shí),需要預(yù)先生成或獲取字節(jié)碼動(dòng)態(tài)生成的MethodAccess子類對(duì)象。

這一操作是非常耗時(shí)的,所以正確的使用方法應(yīng)該是:

在某個(gè)利用反射的耗時(shí)函數(shù)啟動(dòng)前,先預(yù)先生成這個(gè)MethodAccess對(duì)象。

如果是自己里面ReflectASM封裝工具類,則應(yīng)該設(shè)計(jì)緩存,緩存生成的MethodAccess對(duì)象。

如果不這樣做,這個(gè)ReflectASM用的沒有任何意義,性能還不如java的原生反射。

如果想進(jìn)一步提升性能,那么還應(yīng)該避免使用函數(shù)的字符串名稱來調(diào)用,而是在耗時(shí)的函數(shù)啟動(dòng)前,預(yù)先獲取函數(shù)名稱對(duì)應(yīng)的整數(shù)索引。

在后面的耗時(shí)的函數(shù),使用這個(gè)整數(shù)索引進(jìn)行調(diào)用。

總結(jié)

以上是生活随笔為你收集整理的java 反射 性能_java高性能反射及性能对比的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。