《深入理解Java虚拟机》- JVM是如何实现反射的
Posted chenscript
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《深入理解Java虚拟机》- JVM是如何实现反射的相关的知识,希望对你有一定的参考价值。
Java反射学问很深,这里就浅谈吧。如果涉及到方法内联,逃逸分析的话,我们就说说是什么就好了。有兴趣的可以去另外看看,我后面可能也会写一下。(因为我也不会呀~)
一、Java反射是什么?
反射的核心是JVM在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
反射是由类开始的,从class对象中,我们可以获得有关该类的全部成员的完整列表;可以找出该类的所有类型、类自身信息。
二、反射的一些应用
1、java集成开发环境,每当我们敲入点号时,IDE便会根据点号前的内容,动态展示可以访问的字段和方法。
2、java调试器,它能够在调试过程中枚举某一对象所有字段的值。
3、web开发中,我们经常接触到各种配置的通用框架。为保证框架的可扩展性,他往往借助java的反射机制。例如Spring框架的依赖反转(IOC)便是依赖于反射机制。
三、Java反射的实现
1. Java反射使用的api(列举部分,具体在rt.jar包的java.lang.reflect.*)中
列举Class.java中的一些方法。这些都很常用,比如在你尝试编写一个mvc框架的时候,就可以参照这个类里面的方法,再结合一些Servlet的api就实现一个简单的框架。
2.代码实现
2.1代码实现的目的:说明反射调用是有两种方式,一种是本地实现,另一种是委派实现。
这里围绕Method.invoke方法展开。查看invoke()源码:
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException if (!override) if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); MethodAccessor ma = methodAccessor; // read volatile if (ma == null) ma = acquireMethodAccessor(); return ma.invoke(obj, args);
说明:invoke()是有MethodAccessor接口实现的,这个接口有俩实现:
一个是使用委派模式的“委派实现”,一个是通过本地方法调用来实现反射调用的“本地实现”。
这两种实现不是独立的,而是相互协作的。下面,用代码让大家看一下具体操作。
Java代码:
public class InvokeDemo public static void target(int i) new Exception("#"+i).printStackTrace(); public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException Class<?> invokeDemo1 = Class.forName("com.example.demo.invoke_demo.InvokeDemo"); Method method1 = invokeDemo1.getMethod("target", int.class); method1.invoke(null,0);
运行之后,便可以在异常栈中查找方法调用的路线:
java.lang.Exception: #0 at com.example.demo.invoke_demo.InvokeDemo.target(InvokeDemo.java:9) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.example.demo.invoke_demo.InvokeDemo.main(InvokeDemo.java:15)
这里,我们会看到,invoke方法是先调用委派实现,然后再将请求传到本地方法实现的,最后在传到目标方法使用。
为什么要这样做呢?为什么不直接调用本地方法呢?
其实,Java的反射调用机制还设立了另一种动态生成字节码的实现(“动态实现”),直接使用invoke指令来调用目标方法。之所以采用委派实现,便是为了能够在“本地实现”和动态实现之间来回切换。(但是,动态实现貌似并没有开源)
动态实现与本地实现的区别在于,反射代码段重复运行15次以上就会使用动态实现,15次以下就使用本地实现。下面是重复这个代码的控制台输出的第#14、#15、#16段异常:
Class<?> invokeDemo1 = Class.forName("com.example.demo.invoke_demo.InvokeDemo"); Method method1 = invokeDemo1.getMethod("target", int.class); method1.invoke(null,0);
控制台:
java.lang.Exception: #15 at com.example.demo.invoke_demo.InvokeDemo.target(InvokeDemo.java:9) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.example.demo.invoke_demo.InvokeDemo.main(InvokeDemo.java:20) java.lang.Exception: #16 at com.example.demo.invoke_demo.InvokeDemo.target(InvokeDemo.java:9) at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.example.demo.invoke_demo.InvokeDemo.main(InvokeDemo.java:20) java.lang.Exception: #17 at com.example.demo.invoke_demo.InvokeDemo.target(InvokeDemo.java:9) at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.example.demo.invoke_demo.InvokeDemo.main(InvokeDemo.java:20)
从#15到#16异常链路看,反射的调用就开始从本地实现向动态实现的转变。这 是JVM对反射调用进行辨别优化性能的一个手段。
另外注意一点,粉红色部分的字体,标记为“unkown source" ,那就是不开源的吧,所以看不到那是啥。。
四、Java反射的性能开销
public class InvokeDemo private static long n = 0; public static void target(int i) n++; /* 8662ms public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException Class<?> invokeDemo1 = Class.forName("com.example.demo.invoke_demo.InvokeDemo"); Method method1 = invokeDemo1.getMethod("target", int.class); long start = System.currentTimeMillis(); for (int i = 0; i < 1000000000; i++) if(i==1000000000-1) long total = System.currentTimeMillis()-start; System.out.println(total); method1.invoke(null,1); */ // 161ms public static void main(String[] args) long start = System.currentTimeMillis(); for (int i = 0; i < 1000000000; i++) if(i==1000000000-1) long total = System.currentTimeMillis()-start; System.out.println(total); target(1);
上面展示了使用反射调用和不使用反射调用的性能,结果表示,使用反射的耗时为8662ms,而不使用反射的耗时为161ms。这里就可以看到差异。
那么从字节码层面查看,又是什么样的一种风景呢?
1.不使用反射:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=6, args_size=1 0: invokestatic #3 // Method java/lang/System.currentTimeMillis:()J 3: lstore_1 4: iconst_0 5: istore_3 6: iload_3 7: ldc #4 // int 1000000000 9: if_icmpge 43 12: iload_3 13: ldc #5 // int 999999999 15: if_icmpne 33 18: invokestatic #3 // Method java/lang/System.currentTimeMillis:()J 21: lload_1 22: lsub 23: lstore 4 25: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 28: lload 4 30: invokevirtual #7 // Method java/io/PrintStream.println:(J)V 33: iconst_1 34: invokestatic #8 // Method target:(I)V 37: iinc 3, 1 40: goto 6 43: return LineNumberTable: line 8: 0 line 9: 4 line 10: 12 line 11: 18 line 12: 25 line 14: 33 line 9: 37 line 16: 43 StackMapTable: number_of_entries = 3 frame_type = 253 /* append */ offset_delta = 6 locals = [ long, int ] frame_type = 26 /* same */ frame_type = 250 /* chop */ offset_delta = 9
2.使用反射:
public static void main(java.lang.String[]) throws java.lang.ClassNotFoundException, java.lang.NoSuchMethodException, java.lang.reflect.InvocationTargetException, java.lang.IllegalAccessException; descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=6, locals=8, args_size=1 0: ldc #3 // String InvokeDemo2 2: invokestatic #4 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class; 5: astore_1 6: aload_1 7: ldc #5 // String target 9: iconst_1 10: anewarray #6 // class java/lang/Class 13: dup 14: iconst_0 15: getstatic #7 // Field java/lang/Integer.TYPE:Ljava/lang/Class; 18: aastore 19: invokevirtual #8 // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method; 22: astore_2 23: invokestatic #9 // Method java/lang/System.currentTimeMillis:()J 26: lstore_3 27: iconst_0 28: istore 5 30: iload 5 32: ldc #10 // int 1000000000 34: if_icmpge 82 37: iload 5 39: ldc #11 // int 999999999 41: if_icmpne 59 44: invokestatic #9 // Method java/lang/System.currentTimeMillis:()J 47: lload_3 48: lsub 49: lstore 6 51: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream; 54: lload 6 56: invokevirtual #13 // Method java/io/PrintStream.println:(J)V 59: aload_2 60: aconst_null 61: iconst_1 62: anewarray #14 // class java/lang/Object 65: dup 66: iconst_0 67: iconst_1 68: invokestatic #15 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 71: aastore 72: invokevirtual #16 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; 75: pop 76: iinc 5, 1 79: goto 30 82: return
anewarray: 表示创建一个引用类型的(如类、接口、数组)数组,并将其引用值压如栈顶 (1: anewarray #2)
大致的分析:
1.绿色部分:反射调用分配了更多的栈,说明需要进行比普通调用还要多的栈空间分配,也就是pop出,push进。。
2.从方法体上看: 在反射部分代码中的蓝色背景部分,也就是62行字节码,使用了创建数组这一操作,并且还有68行的将int类型的1进行装箱操作,这些步骤对于普通调用来说,都是多出来的,自然也就比普通调用的方式耗时得多了。
但是,普通调用和反射调用一个方法的用途不一样,我们不能为了反射调用而调用,最好能够在普通调用无法满足的情况下进行该操作。
五、优化反射调用
(明天再写吧。。。demo都没写出来,不好意思写了。。)
以上是关于《深入理解Java虚拟机》- JVM是如何实现反射的的主要内容,如果未能解决你的问题,请参考以下文章