Flink类加载

1.配置

  以下三个是主要的加载方式配置,其他还有一些插件加载和加载异常处理的配置
  1、classloader.resolve-order
  child-first(默认)、parent-first。从业务代码加载类时的策略,即先检查业务包还是按通常的java加载机制
  2、classloader.parent-first-patterns.default
  指定哪些类首先通过父类加载器解析,默认主要是java、flink、log4j一系列的
  3、classloader.parent-first-patterns.additional
  对上一个配置的补充

2.相关类

  classloader.resolve-order配置在client、JobManager、TaskManager三个地方分别读取使用,三个地方分别创建自己的类加载形式

2.1.FlinkUserCodeClassLoaders

  这个类是负责创建类加载器的,根据传入的参数,选择child或者parent类加载器

switch (resolveOrder) {
    case CHILD_FIRST:
        return childFirst(
                urls,
                parent,
                alwaysParentFirstPatterns,
                classLoadingExceptionHandler,
                checkClassLoaderLeak);
    case PARENT_FIRST:
        return parentFirst(
                urls, parent, classLoadingExceptionHandler, checkClassLoaderLeak);
    default:
        throw new IllegalArgumentException(
                "Unknown class resolution order: " + resolveOrder);

2.2.URLClassLoader

  这个是java提供的类加载器,是ClassLoader的子类,其功能是从指定的URL搜索路径加载类和资源。也就是说,通过URLClassLoader可以加载指定jar中的class到内存中。
  使用方式如下:其中jar包放在c盘下,Hello是jar包中的一个类

public void urlClassLoaderTest() throws Exception {
    File file = new File("c:/");
    URL url = file.toURI().toURL();
    ClassLoader loader=new URLClassLoader(new URL[]{url});
    Class<?> clazz = loader.loadClass("Hello");
    clazz.newInstance();
}

2.3.FlinkUserCodeClassLoader

  这个是类加载器的接口,继承自java的URLClassLoader

public abstract class FlinkUserCodeClassLoader extends URLClassLoader {

  重写了loadClass接口,但是实际没有做自定义操作,直接调用了父类的接口

return super.loadClass(name, resolve);

2.4.ParentFirstClassLoader

  FlinkUserCodeClassLoader的parent-first形式的实现类,基本没有自定义的内容,直接super父类的,也就是说用的java的默认加载机制。注意registerAsParallelCapable方法,这个是ClassLoader的接口

static {
    ClassLoader.registerAsParallelCapable();
}

  registerAsParallelCapable是注册类加载器为并行类加载器,关于并行类加载器:1、加载类的时候,默认是串行的,因为使用类加载器自身作为锁;2、在需要加载的classname上进行加锁,解决类加载死锁问题,可以进行并行加载;3、先进行类的注册,则能实现类的并行加载,从提高程序的启动速度
  关于对classname进行锁定,父类FlinkUserCodeClassLoader中进行了实现

public final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    try {
        synchronized (getClassLoadingLock(name)) {
            return loadClassWithoutExceptionHandling(name, resolve);
        }
    } catch (Throwable classLoadingException) {
        classLoadingExceptionHandler.accept(classLoadingException);
        throw classLoadingException;
    }
}

  此外,URLClassLoader加载上也进行了锁定

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);

2.5.ChildFirstClassLoader

  ChildFirstClassLoader对类加载过程有部分的自定义实现,主要几个过程:1、检查类是否加载;2、parent-first特殊模式的使用父类加载器;3、使用URLClassLoader的findClass加载类
  resolve是是否做连接的配置,就是类加载的一个过程

protected Class<?> loadClassWithoutExceptionHandling(String name, boolean resolve)
        throws ClassNotFoundException {

    // First, check if the class has already been loaded
    Class<?> c = findLoadedClass(name);

    if (c == null) {
        // check whether the class should go parent-first
        for (String alwaysParentFirstPattern : alwaysParentFirstPatterns) {
            if (name.startsWith(alwaysParentFirstPattern)) {
                return super.loadClassWithoutExceptionHandling(name, resolve);
            }
        }

        try {
            // check the URLs
            c = findClass(name);
        } catch (ClassNotFoundException e) {
            // let URLClassLoader do it, which will eventually call the parent
            c = super.loadClassWithoutExceptionHandling(name, resolve);
        }
    } else if (resolve) {
        resolveClass(c);
    }

    return c;
}

  与parent-first的差别主要在于parent-first使用了ClassLoader的loadClass,使用了双亲委派,如下

if (c == null) {
    long t0 = System.nanoTime();
    try {
        if (parent != null) {
            c = parent.loadClass(name, false);
        } else {
            c = findBootstrapClassOrNull(name);
        }

  child-first使用了URLClassLoader的findClass,并没有双亲委派,是直接加载类的。loadClass如果双亲委派在上层加载器没有加载类的情况下,最终用的也是findClass
  优先加载业务代码的产生应该就是来自这里,首先类加载器是child-first这个单独的,然后加载的时候是直接通过URL进行的加载,所以URL是业务包路径的话,加载的类就是业务包的

3.URL的传入

  看URLClassLoader的使用示例,URL是初始化类加载器的时候传入的。反向追踪,在classLoad层面,传入是从FlinkUserCodeClassLoaders的create方法。
上层调用有两个:1、ClientUtils;2、BlobLibraryCacheManager

3.1.BlobLibraryCacheManager

  这个是基于blobService的实现,blobService是Flink的一个文件管理模块,业务包会基于这个上传分发等,具体另介绍
  URL的来源在BlobLibraryCacheManager的createUserCodeClassLoader

final URL[] libraryURLs =
        new URL[requiredJarFiles.size() + requiredClasspaths.size()];
int count = 0;
// add URLs to locally cached JAR files
for (PermanentBlobKey key : requiredJarFiles) {
    libraryURLs[count] = blobService.getFile(jobId, key).toURI().toURL();
    ++count;
}

// add classpaths
for (URL url : requiredClasspaths) {
    libraryURLs[count] = url;
    ++count;
}

return classLoaderFactory.createClassLoader(libraryURLs);

  URL的来源可以看到是requiredJarFiles和requiredClasspaths,这两个是来自JobGraph,在客户端解析的时候会完成设置

final ClassLoader userCodeClassLoader =
        classLoaderLease
                .getOrResolveClassLoader(
                        jobGraph.getUserJarBlobKeys(), jobGraph.getClasspaths())
                .asClassLoader();

3.2.ClientUtils

  ClientUtils是一个工具类,给客户端使用。ClientUtils的buildUserCodeClassLoader上层有三个调用:1、PackagedProgram;2、SessionContext.updateClassLoaderAndDependencies;3、SessionContext.create
  其中,PackagedProgram是Application模式调用到的,包括SA、Yarn、K8S的Application,用于解析业务代码;SessionContext.create是sql-client使用的,应该是用于catalog进行类解析;updateClassLoaderAndDependencies是LocalExecutor使用的,用于动态的加减jar包(LocalExecutor需要研究一下)

4.java类加载机制

4.1.双亲委派

  双亲委派就是由父加载器先加载类,看第二章的内容,ClassLoader的loadClass方法中实现了双亲委派的处理,所以自定义类加载器不推荐重写loadClass,而是重写findClass,直接调用findClass就可以避开双亲委派
  BootstrapClassLoader -> ExtentionClassLoader -> AppClassLoader -> 自定义类加载器
  这里的父子关系不是来自类继承,加载器类中有一个成员指向父加载器。一般在加载器的构造函数中可以指定父加载器。没指定默认为AppClassLoader ,指定null则为BootstrapClassLoader
jvm的类加载顺序:1、BootstrapClassLoader,负责加载jre/lib下的几个核心包;2、ExtClassLoader,负责加载jre/lib/ext下的包,注意java.ext.dirs环境变量可替换加载目录;3、AppClassLoader负责加载java的其他包和业务包(classpath下),注意java.class.path。ExtClassLoader和AppClassLoader均在Launcher类下,继承自URLClassLoader

4.1.1.BootstrapClassLoader

  用于加载核心类,一般是java.下的方法。通过-Xbootclasspath修改或追加核心类目录
  替换:java -Xbootclasspath: <新路径>
  前部追加:java -Xbootclasspath/a:<追加路径>
  后部追加:java -Xbootclasspath/p:<追加路径>

4.1.2.ExtClassLoader

  在getExtDirs中有如下,可以看到目录是可以被环境变量改变的

private static File[] getExtDirs() {
    String var0 = System.getProperty("java.ext.dirs");

4.1.3.AppClassLoader

  有跟ExtClassLoader类似的地方,加载路径可被替换

public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
    final String var1 = System.getProperty("java.class.path");
    final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
    return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
        public Launcher.AppClassLoader run() {
            URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
            return new Launcher.AppClassLoader(var1x, var0);
        }
    });
}

4.1.4.父子关系

  具体BootstrapClassLoader、ExtClassLoader、AppClassLoader的父子关系,目前资料说是向下的一个父子关系(不是基于类继承的,是通过parent成员变量)
  在AppClassLoader当中定义了如下方法

public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
    final String var1 = System.getProperty("java.class.path");
    final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
    return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
        public Launcher.AppClassLoader run() {
            URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
            return new Launcher.AppClassLoader(var1x, var0);
        }
    });
}

  new Launcher.AppClassLoader最终调用父类的定义,也就是说,传入的classloader即为其父类加载器

public URLClassLoader(URL[] urls, ClassLoader parent,
                      URLStreamHandlerFactory factory) {

  继续查找,在其定义文件Launcher的Launcher方法中,有如下内容,可以确认其父加载器为ExtClassLoader

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

  对于ExtClassLoader,其构造函数传入的父加载器为null,没有找到明确的父子依赖关系(BootstrapClassLoader),其父子关系推测应该来源于ClassLoader的loadClass方法中的定义,有如下内容

if (parent != null) {
    c = parent.loadClass(name, false);
} else {
    c = findBootstrapClassOrNull(name);
}

  ExtClassLoader的parent为空,调用到BootstrapClass的分支,最终实现应该就是BootstrapClassLoader,但是findBootstrapClassOrNull最终调用的是一个native方法,暂时不确定和BootstrapClassLoader的关联

private native Class<?> findBootstrapClass(String name);

4.1.5.注意点

  父加载器加载的类无法访问子加载器加载的类(应该是说,java双亲委派调用的父加载器BootstrapClassLoader、ExtentionClassLoader、AppClassLoader只能加载classpath下的东西,自定义类加载器额外添加的东西,它们加载不了)

4.2.并行加载

  上文提到过类加载器通过调用ClassLoader.registerAsParallelCapable()注册为并行加载器

4.2.1.ParallelLoaders

  ClassLoader的一个内部类,记录并行加载类的信息

4.2.1.1.WeakHashMap

  ParallelLoaders定义了一个Set记录可执行并行加载的类

private static final Set<Class<? extends ClassLoader>> loaderTypes =
    Collections.newSetFromMap(
        new WeakHashMap<Class<? extends ClassLoader>, Boolean>());

  注意其中的WeakHashMap,这是一个HashMap,其特点是weak,就是说它的key是弱键,可以被垃圾回收器回收,会自动从WeakHashMap中移除
newSetFromMap是将Map转成Set,java没有提供WeakHashSet,通过这种方式生成

4.2.1.2.注册

  通过register接口对并行加载类进行注册,其实就是加入了loaderTypes 队列中。此外,子类注册为并行的前提是父类必须支持。ClassLoader是类加载器的父类,默认加入

static boolean register(Class<? extends ClassLoader> c) {
    synchronized (loaderTypes) {
        if (loaderTypes.contains(c.getSuperclass())) {
            // register the class loader as parallel capable
            // if and only if all of its super classes are.
            // Note: given current classloading sequence, if
            // the immediate super class is parallel capable,
            // all the super classes higher up must be too.
            loaderTypes.add(c);
            return true;
        } else {
            return false;
        }
    }
}

  ClassLoader通过static代码块加入

static {
    synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
}

4.2.2.getClassLoadingLock

  实现并行类加载的核心在于getClassLoadingLock这里,主要在loadClass方法中。老版本java中,直接对loadClass整个方法进行synchronized,现在在方法中进行更细粒度的锁
ClassLoader的loadClass如下

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
FlinkUserCodeClassLoader的loadClass如下
public final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    try {
        synchronized (getClassLoadingLock(name)) {
            return loadClassWithoutExceptionHandling(name, resolve);

  其锁都在getClassLoadingLock方法的返回结果上,看getClassLoadingLock的具体方法,其入参是需要加载的类

protected Object getClassLoadingLock(String className) {
    Object lock = this;
    if (parallelLockMap != null) {
        Object newLock = new Object();
        lock = parallelLockMap.putIfAbsent(className, newLock);
        if (lock == null) {
            lock = newLock;
        }
    }
    return lock;
}

  可以看到,当parallelLockMap非null时,会对每个需要加载的类单独生成一个对象返回,也就是说,锁对每个需要加载的类都是独立的,不同的类就可以并行加载

4.2.3.parallelLockMap

  上节所说,parallelLockMap是能否进行并行加载的判断点,parallelLockMap的初始化在ClassLoader的构造函数中,如下,类加载器支持并行加载则parallelLockMap非null。类加载器支持并行度的判断参见ParallelLoaders

private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    if (ParallelLoaders.isRegistered(this.getClass())) {
        parallelLockMap = new ConcurrentHashMap<>();
        package2certs = new ConcurrentHashMap<>();
        domains =
            Collections.synchronizedSet(new HashSet<ProtectionDomain>());
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }
}

4.3.URLClassLoader

  继承自SecureClassLoader,SecureClassLoader继承自ClassLoader。ExtClassLoader和AppClassLoader均继承自URLClassLoader。这是一个java的类继承父子关系,与前文双亲委派的成员变量确定父子关系是不一样的。
  虽然AppClassLoader继承自URLClassLoader,但用户创建ClassLoader的时候属于自定义类加载器了。按双亲委派的关系,URLClassLoader不传入父类加载器时,其父类加载器为AppClassLoader;传入null使用BootstrapClassLoader
  ClassLoader只能加载classpath的类,URLClassLoader可以加载任意路径下的类,可以实现动态加载类。

4.4.Class.forName

  Class.forName与ClassLoader不是同级别的东西,Class.forName下层也是用ClassLoader来加载的,默认使用调用者(也就是当前类)的类加载器,也就是默认只能加载classpath下的内容。
Class.forName提供了接口支持传入自定义类加载器。

5.其他补充

5.1.Iceberg

  Iceberg使用Catalog时自己进行了类的加载
  在CatalogUtil的loadCatalog进行catalog的加载,核心方法为

ctor = DynConstructors.builder(Catalog.class).impl(impl).buildChecked();

  追踪impl方法,内部进行了动态类加载,使用了Class.forName

Class<?> targetClass = Class.forName(className, true, loader);

  根据Class.forName的用法,loader是其使用的类加载器,追踪其定义,Builder类成员变量直接赋值默认值(也存在手动设置的接口,但没有发现调用,也就是说用的默认值)

private ClassLoader loader = Thread.currentThread().getContextClassLoader();

5.1.1.flink使用时注意的问题

  类加载器是getContextClassLoader获取的,也就是加载iceberg的类加载器。因此,当将iceberg的jar包放在flink的lib下时,其使用的是java默认的类加载器,只能加载到classpath下的jar包。也就是说,iceberg依赖的包,比如hive等也必须放在flink的lib下,否则无法加载到(如上,flink有动态加载客户端包的能力,但是由于iceberg继承使用了java的类加载器,所有客户端包的类其无法加载)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值