Java闭包&Flink ClosureCleaner

1,202 阅读1分钟

Java内部类为什么能访问外部类信息

访问外部类的静态属性

内部类获取外部类静态属性,通过字节码来看,是外部类提供了一个静态方法。例如:

public class Demo {
    private static int version;
    private class Inner {
        void run() {
            System.out.println(version);
        }
    }
}

这个知识点不用关内部类是静态的还是非静态的。他为什么能拿到外面类的静态属性呢javap -v Demo看下字节码:

{
  public Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  static int access$000();
    descriptor: ()I
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field version:I
         3: ireturn
      LineNumberTable:
        line 1: 0
}
SourceFile: "Demo.java"

这里字节码没有放常量池信息。 能看到代码编译后又一个静态方法access$xxx,内部类(嵌套类)使用。这就是为什么能获取到外面的静态变量。

访问外部类的成员变量

上面例子的代码简单修改后是这样。

public class Demo {
    private int version;
    private class Inner {
        void run() {
            System.out.println(version);
        }
    }
}

同样看下字节码

{
  public Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  static int access$000(Demo);
    descriptor: (LDemo;)I
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #1                  // Field version:I
         4: ireturn
      LineNumberTable:
        line 1: 0
}

还是可以看到在外部类的字节码中,也有一个静态方法access$xxx不过这个方法需要一个入参,类型是Demo。再看下内部类的字节码

{
  final Demo this$0;
    descriptor: LDemo;
    flags: ACC_FINAL, ACC_SYNTHETIC

  void run();
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: getfield      #1                  // Field this$0:LDemo;
         7: invokestatic  #4                  // Method Demo.access$000:(LDemo;)I
        10: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        13: return
      LineNumberTable:
        line 6: 0
        line 7: 13
}

看到内部类又一个成员变量this$0 这个变量的类型是Demo ,在看run 方法里的逻辑,有一步就是调用外部类的静态方法,然后把这个this$0 传入了。 实际上在构建内部类对象的时候,外部类会把自己的实例对象传入内部类的构造方法中。这也就是闭包

Flink ClosureCleaner

首先看创建kafka数据源的一个代码:

public class SourceCreator {
    public FlinkKafkaConsumer011<JsonNode> createKafkaConsumer() {
        Properties config = new Properties();
        // config.xxx
        return new FlinkKafkaConsumer011<JsonNode>("demo-topic", new AbstractDeserializationSchema<JsonNode>() {
            @Override
            public JsonNode deserialize(byte[] bytes) throws IOException {
                return Demo.objectMapper.readTree(bytes);
            }
        }, config);
    }
}

然后我的程序应该是从这个SourceCreator 实例中获取数据源。

public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    FlinkKafkaConsumer011<JsonNode> kafkaConsumer = new SourceCreator().createKafkaConsumer();
    env.addSource(kafkaConsumer);
//        xxx
//        env.execute();
}

懂得人已经知道问题在哪了。在flink(或者spark)这种分布式计算的框架中,我们写的代码其实基本上都是一个个零件(函数式编程),这些东西最后都是需要由 JobManager序列化后,分发到各个计算节点上的。所以能否序列化很关键。在env.addSource(kafkaConsumer);这里,flink 中调用了一个clean 方法。

/**
 * Returns a "closure-cleaned" version of the given function. Cleans only if closure cleaning
 * is not disabled in the {@link org.apache.flink.api.common.ExecutionConfig}
 */
@Internal
public <F> F clean(F f) {
    if (getConfig().isClosureCleanerEnabled()) {
        ClosureCleaner.clean(f, getConfig().getClosureCleanerLevel(), true);
    }
    ClosureCleaner.ensureSerializable(f);
    return f;
}

这个clean其实主要是进行闭包清理,一般我们开发可能是直接在flink 留的api里传入一个匿名内部类的实例,这样这个实例里面可能又一个this$0成员指向外部类的实例。闭包清理主要就是将这个成员的值改为null,关键源码如下:

// 通过反射拿到成员变量,然后改为null。进行闭包清理。清除无用的东西
this0.setAccessible(true);
this0.set(func, null);

闭包清理后,还要进行检查能否序列化。