一、查看匿名内部类的字节码
- 写一个匿名内部类的实现,然后编译下
- 看到该类路径下,会多一个XXX$1.class
- 使用javap -v xxx$.class
注意这里一定要保留"",否则javap找到的是原始类
二、源码分析
我们先贴一段dubbo的JavassistProxyFactory类的源码,引出今天的问题:
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// TODO Wrapper类不能正确处理带$的类名
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes,
Object[] arguments) throws Throwable {
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
仔细观察下这段代码,getInvoker是dubbo启动时的初始化过程。该方法只会在初始化执行一次,现在提出我们的疑问。wrapper实例在方法中属于局部变量,那么getInvoker执行结束后,方法栈销毁,wrapper实例应该要被jvm回收。那么dubbo在执行调用时,会执行到doInvoke方法,而这个AbstractProxyInvoker匿名内部类的构造方法并没有把wrapper传递到它的构造方法里面,doInvoker是怎么调用到wrapper的呢?
带着疑问,我们先来猜测下new AbstractProxyInvoker<T>(proxy, type, url)的构造方法,只会把proxy, type, url三个变量初始化给AbstractProxyInvoker实现类,那wrapper呢?肯定在某个地方也赋值给它了。为了验证猜想,我们只能抓取字节码查看了。
使用HSDB抓取到的字节码,分析如下图所示:
image.png
看到void <init>这个方法了,它就是构造方法。数一数它的参数:5个参数,是不是比代码中的3个多出来2个。这两个看下上面的Fields,分别对应到参数列表的第1个和第5个。
所以得出结论:匿名内部类默认生成了新的构造函数,新构造函数会把final变量设置到构造方法里面。
我们看下完整的匿名内部类的字节码:
Compiled from "JavassistProxyFactory.java"
class com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1 extends com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker<T> {
//字段1
final com.alibaba.dubbo.common.bytecode.Wrapper val$wrapper;
//字段2
final com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory this$0;
//新构造函数,或者匿名实现类的构造函数
com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1(com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory, java.lang.Object, java.lang.Class, com.alibaba.dubbo.common.URL, com.alibaba.dubbo.common.bytecode.Wrapper);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/alibaba/dubbo/rpc/proxy/javassist/JavassistProxyFactory;
5: aload_0
6: aload 5
8: putfield #2 // Field val$wrapper:Lcom/alibaba/dubbo/common/bytecode/Wrapper;
11: aload_0
12: aload_2
13: aload_3
14: aload 4
16: invokespecial #3 // Method com/alibaba/dubbo/rpc/proxy/AbstractProxyInvoker."<init>":(Ljava/lang/Object;Ljava/lang/Class;Lcom/alibaba/dubbo/common/URL;)V
19: return
LineNumberTable:
line 41: 0
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 this Lcom/alibaba/dubbo/rpc/proxy/javassist/JavassistProxyFactory$1;
0 20 1 this$0 Lcom/alibaba/dubbo/rpc/proxy/javassist/JavassistProxyFactory;
0 20 2 proxy Ljava/lang/Object;
0 20 3 type Ljava/lang/Class;
0 20 4 url Lcom/alibaba/dubbo/common/URL;
protected java.lang.Object doInvoke(T, java.lang.String, java.lang.Class<?>[], java.lang.Object[]) throws java.lang.Throwable;
Code:
0: aload_0
1: getfield #2 // Field val$wrapper:Lcom/alibaba/dubbo/common/bytecode/Wrapper;
4: aload_1
5: aload_2
6: aload_3
7: aload 4
9: invokevirtual #4 // Method com/alibaba/dubbo/common/bytecode/Wrapper.invokeMethod:(Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/Object;)Ljava/lang/Object;
12: areturn
LineNumberTable:
line 46: 0
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/alibaba/dubbo/rpc/proxy/javassist/JavassistProxyFactory$1;
0 13 1 proxy Ljava/lang/Object;
0 13 2 methodName Ljava/lang/String;
0 13 3 parameterTypes [Ljava/lang/Class;
0 13 4 arguments [Ljava/lang/Object;
}
三、本地验证
首先贴出来咱们的匿名接口,如下:
public interface MyInterface {
int hello();
int helloWorld();
}
再贴出来实现:
public class AnonyInnerClassTest {
public static void main(String[] args){
final ClassB b = new ClassB(2);
final ClassB c = new ClassB(2);
MyInterface target = new MyInterface(){
@Override
public int hello() {
b.setA(3);
return 0;
}
@Override
public int helloWorld() {
c.getA();
return 1;
}
};
target.hello();
}
}
观察下代码,我们只在方法里面用了b和c变量。但是就是这种使用,影响了构造函数的生成。我们看下他的字节码:
Compiled from "AnonyInnerClassTest.java"
class com.example.demo.object.AnonyInnerClassTest$1 implements com.example.demo.object.MyInterface {
com.example.demo.object.AnonyInnerClassTest$1(com.example.demo.object.ClassB, com.example.demo.object.ClassB);
Code:
0: aload_0
1: aload_1
2: putfield #13 // Field val$b:Lcom/example/demo/object/ClassB;
5: aload_0
6: aload_2
7: putfield #15 // Field val$c:Lcom/example/demo/object/ClassB;
10: aload_0
11: invokespecial #17 // Method java/lang/Object."<init>":()V
14: return
LineNumberTable:
line 1: 0
line 14: 10
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 this Lcom/example/demo/object/AnonyInnerClassTest$1;
public int hello();
Code:
0: aload_0
1: getfield #13 // Field val$b:Lcom/example/demo/object/ClassB;
4: iconst_3
5: invokevirtual #26 // Method com/example/demo/object/ClassB.setA:(I)V
8: iconst_0
9: ireturn
LineNumberTable:
line 17: 0
line 18: 8
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/example/demo/object/AnonyInnerClassTest$1;
public int helloWorld();
Code:
0: aload_0
1: getfield #15 // Field val$c:Lcom/example/demo/object/ClassB;
4: invokevirtual #33 // Method com/example/demo/object/ClassB.getA:()I
7: pop
8: iconst_1
9: ireturn
LineNumberTable:
line 23: 0
line 24: 8
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/example/demo/object/AnonyInnerClassTest$1;
}
可以看到它的字节码中,构造函数AnonyInnerClassTest$1(com.example.demo.object.ClassB, com.example.demo.object.ClassB),里面有两个参数,分别对应b和c。
验证了我们的猜想,在编译阶段匿名内部类被编译器解开语法糖,编译器为匿名类的实现类生成类名,构造函数信息。构造函数的生成,依赖其实现方法用到的所有final申明的对象。
至于为啥要final,网上一大堆,自己可以去看看。这里我说下我的理解吧。
通过上面构造函数的分析,如果内部类使用了局部变量,那么局部变量就变成了匿名内部类实现类的成员变量。
- 从变量的生命周期来看。
本来局部变量的生命周期就是这个方法内部,方法执行结束就销毁了。但是变成了成员变量后,它的生命周期就大大提高了,和实例的生命周期一致了。如果在这么长的生命周期内,且该对象不小心变成多线程共享的,那么每个方法都可以随意修改它的值,会导致这个变量的调用非常混乱,多线程不安全。当然引用对象修饰为final也没用,里面的东西还是能改的,这是另外一回事。
2.如果不用final修饰,那么在局部变量生命周期内其他时间,这个变量被修改了。那么匿名内部类的实现类持有的相同的对象,到底要不要跟着修改呢?答案是匿名内部类在申明的那一刻起,它持有的对象是通过copy方式达到一致。后面原变量的修改,不会影响到匿名内部类。
- 为什么要拷贝呢?
现在我们知道了,是由于一个拷贝的动作,使得内外两个变量无法实时同步,其中一方修改,另外一方都无法同步修改,因此要加上final限制变量不能修改。那么为什么要拷贝呢,不拷贝不就没那么多事了吗?这时候就得考虑一下Java虚拟机的运行时数据区域了,原始的局部变量是位于方法内部的,因此它是在虚拟机栈上,也就意味着这个变量无法进行共享,匿名内部类也就无法直接访问,因此只能通过值传递的方式,传递到匿名内部类中。
所以必须使用final来避免这种混乱。
到这里,终于解开了dubbo那段源码wrapper局部变量在getInvoker执行完成后,为啥它没有被回收。
至于匿名内部类为什么要这么做?我猜测如果不这么做,那么开发者就要定义一个新类,然后自己写构造函数,自己把所有实现方法里面要用到的对象都传递进这个构造函数。编译器觉得这种模式完全可以由它来自动提供,所以就提供了这么个语法糖,解放开发者。
网友评论