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的类加载器,所有客户端包的类其无法加载)