原文链接1:https://blog.csdn.net/nazeniwaresakini/article/details/107728576
原文链接2:https://blog.csdn.net/chenxyz707/article/details/109043868
在JVM中,一个类加载的过程大致分为加载、链接(验证、准备、解析)、初始化5个阶段。
而我们通常提到类的加载,就是指利用类加载器(ClassLoader)通过类的全限定名来获取定义此类的二进制字节码流,进而构造出类的定义。
Flink作为基于JVM的框架,在flink-conf.yaml中提供了控制类加载策略的参数classloader.resolve-order
,可选项有child-first
(默认)和parent-first
。
parent-first类加载策略
ParentFirstClassLoader和ChildFirstClassLoader类的父类均为FlinkUserCodeClassLoader抽象类,先来看看这个抽象类,代码很短。
public abstract class FlinkUserCodeClassLoader extends URLClassLoader {
public static final Consumer<Throwable> NOOP_EXCEPTION_HANDLER = classLoadingException -> {};
private final Consumer<Throwable> classLoadingExceptionHandler;
protected FlinkUserCodeClassLoader(URL[] urls, ClassLoader parent) {
this(urls, parent, NOOP_EXCEPTION_HANDLER);
}
protected FlinkUserCodeClassLoader(
URL[] urls,
ClassLoader parent,
Consumer<Throwable> classLoadingExceptionHandler) {
super(urls, parent);
this.classLoadingExceptionHandler = classLoadingExceptionHandler;
}
@Override
protected final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
try {
return loadClassWithoutExceptionHandling(name, resolve);
} catch (Throwable classLoadingException) {
classLoadingExceptionHandler.accept(classLoadingException);
throw classLoadingException;
}
}
protected Class<?> loadClassWithoutExceptionHandling(String name, boolean resolve) throws ClassNotFoundException {
return super.loadClass(name, resolve);
}
}
FlinkUserCodeClassLoader继承自URLClassLoader。因为Flink App的用户代码在运行期才能确定,所以通过URL在JAR包内寻找全限定名对应的类是比较合适的。而ParentFirstClassLoader仅仅是一个继承FlinkUserCodeClassLoader的空类而已。
static class ParentFirstClassLoader extends FlinkUserCodeClassLoader {
ParentFirstClassLoader(URL[] urls, ClassLoader parent, Consumer<Throwable> classLoadingExceptionHandler) {
super(urls, parent, classLoadingExceptionHandler);
}
}
这样就相当于ParentFirstClassLoader直接调用了父加载器的loadClass()方法。之前已经讲过,JVM中类加载器的层次关系和默认loadClass()方法的逻辑由双亲委派模型(parents delegation model)来体现,复习一下含义:
如果一个类加载器要加载一个类,它首先不会自己尝试加载这个类,而是把加载的请求委托给父加载器完成,所有的类加载请求最终都应该传递给最顶层的启动类加载器。只有当父加载器无法加载到这个类时,子加载器才会尝试自己加载。
可见,Flink的parent-first类加载策略就是照搬双亲委派模型的。也就是说,用户代码的类加载器是Custom ClassLoader,Flink框架本身的类加载器是Application ClassLoader。用户代码中的类先由Flink框架的类加载器加载,再由用户代码的类加载器加载。但是,Flink默认并不采用parent-first策略,而是采用下面的child-first策略,继续看。
child-first类加载策略
我们已经了解到,双亲委派模型的好处就是随着类加载器的层次关系保证了被加载类的层次关系,从而保证了Java运行环境的安全性。但是在Flink App这种依赖纷繁复杂的环境中,双亲委派模型可能并不适用。例如,程序中引入的Flink-Cassandra Connector总是依赖于固定的Cassandra版本,用户代码中为了兼容实际使用的Cassandra版本,会引入一个更低或更高的依赖。而同一个组件不同版本的类定义有可能会不同(即使类的全限定名是相同的),如果仍然用双亲委派模型,就会因为Flink框架指定版本的类先加载,而出现莫名其妙的兼容性问题,如NoSuchMethodError、IllegalAccessError等。
鉴于此,Flink实现了ChildFirstClassLoader类加载器并作为默认策略。它打破了双亲委派模型,使得用户代码的类先加载,官方文档中将这个操作称为"Inverted Class Loading"。代码仍然不长,录如下。
public final class ChildFirstClassLoader extends FlinkUserCodeClassLoader {
private final String[] alwaysParentFirstPatterns;
public ChildFirstClassLoader(
URL[] urls,
ClassLoader parent,
String[] alwaysParentFirstPatterns,
Consumer<Throwable> classLoadingExceptionHandler) {
super(urls, parent, classLoadingExceptionHandler);
this.alwaysParentFirstPatterns = alwaysParentFirstPatterns;
}
@Override
protected synchronized 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);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
@Override
public URL getResource(String name) {
// first, try and find it via the URLClassloader
URL urlClassLoaderResource = findResource(name);
if (urlClassLoaderResource != null) {
return urlClassLoaderResource;
}
// delegate to super
return super.getResource(name);
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
// first get resources from URLClassloader
Enumeration<URL> urlClassLoaderResources = findResources(name);
final List<URL> result = new ArrayList<>();
while (urlClassLoaderResources.hasMoreElements()) {
result.add(urlClassLoaderResources.nextElement());
}
// get parent urls
Enumeration<URL> parentResources = getParent().getResources(name);
while (parentResources.hasMoreElements()) {
result.add(parentResources.nextElement());
}
return new Enumeration<URL>() {
Iterator<URL> iter = result.iterator();
public boolean hasMoreElements() {
return iter.hasNext();
}
public URL nextElement() {
return iter.next();
}
};
}
}
核心逻辑位于loadClassWithoutExceptionHandling()方法中,简述如下:
调用findLoadedClass()方法检查全限定名name对应的类是否已经加载过,若没有加载过,再继续往下执行。
检查要加载的类是否以alwaysParentFirstPatterns集合中的前缀开头。如果是,则调用父类的对应方法,以parent-first的方式来加载它。
如果类不符合alwaysParentFirstPatterns集合的条件,就调用findClass()方法在用户代码中查找并获取该类的定义(该方法在URLClassLoader中有默认实现)。如果找不到,再fallback到父加载器来加载。
最后,若resolve参数为true,就调用resolveClass()方法链接该类,最后返回对应的Class对象。
可见,child-first策略避开了“先把加载的请求委托给父加载器完成”这一步骤,只有特定的某些类一定要“遵循旧制”。alwaysParentFirstPatterns集合中的这些类都是Java、Flink等组件的基础,不能被用户代码冲掉。它由以下两个参数来指定:
classloader.parent-first-patterns.default,不建议修改,固定为以下这些值:
java.;
scala.;
org.apache.flink.;
com.esotericsoftware.kryo;
org.apache.hadoop.;
javax.annotation.;
org.slf4j;
org.apache.log4j;
org.apache.logging;
org.apache.commons.logging;
ch.qos.logback;
org.xml;
javax.xml;
org.apache.xerces;
org.w3c
classloader.parent-first-patterns.additional
:除了上一个参数指定的类之外,用户如果有其他类以child-first模式会发生冲突,而希望以双亲委派模型来加载的话,可以额外指定(分号分隔)。
Flink中的类加载机制
Flink中的类加载配置项
Flink中关于类加载有以下配置
配置项 | 默认值 | 说明 |
---|---|---|
classloader.resolve-order | “child-first” | 类加载顺序。child-first优先从Flink任务(jar包)中加载类,parent-first优先从Flink集群加载类。 |
classloader.parent-first-patterns.default | “java.; scala.; org.apache.flink.; com.esotericsoftware.kryo; org.apache.hadoop.;javax.annotation.; org.slf4j;org.apache.log4j; org.apache.logging; org.apache.commons.logging; ch.qos.logback; org.xml; javax.xml; org.apache.xerces; org.w3c” | 优先从Flink集群加载的类,以分号分隔的类前缀匹配 |
classloader.parent-first-patterns.additional | (none) | 额外需要优先从Flink集群加载的类 |
classloader.fail-on-metaspace-oom-error | true | 如果尝试加载用户代码类时抛出“OutOfMemoryError:Metaspace”,则Flink JVM进程失败。 |
parent-first类加载
parent-first的含义就是优先从Flink集群加载类,如果没有该类就从用户的jar包中加载类。
ParentFirstClassLoader类加载器的实现特别简,说白了就是一个有FlinkUserCodeClassLoader特性的URLClassLoader,通过指定的URL加载类文件查找对应的类。
那么,ParentFirstClassLoader是如何保证同一个Flink的类,优先加载集群的而非用户jar包中的?其实这里是利用Java的类加载机制——双亲委派模型。
我们知道用户提交的Flink任务运行于TaskManager进程内,TaskManager运行时的类加载器是Application ClassLoader(用户程序运行时的默认类加载器),当我们使用自定义类加载器加载需要的类时,会自底向上逐一查找,如果ParentFirstClassLoader中有这个类就再次从Application ClassLoader中查找这个类,如果Application ClassLoader中也有这个类,就会继续从Extension ClassLoader中查找,直到父加载器加载不到这个类时,才会使用当前类加载器加载这个类。这样就巧妙的利用双亲委派模型实现了parent-first的类加载机制。
child-first类加载
接着我们来看child-first是如何打破双亲委派机制的。
不少人应该都用过Tomcat,Tomcat中可以部署多个Java程序,每个Java程序都包含了各自不同版本的jar包,但是他们却能各自正常的工作。同样地,同一个Flink集群也需要运行各种各样的Flink任务,而且如果用户使用的某个jar和Flink集群的jar版本不一致,需要优先加载用户的jar怎么办?Flink的ChildFirstClassLoader类加载提供了优先从用户jar包加载类的机制。
这里alwaysParentFirstPatterns就是classloader.parent-first-patterns.default和classloader.parent-first-patterns.additional配置的类前缀集合,即这两个配置项中的类就算使用child-first类加载方式,也会从父加载器中加载,而非用户jar包中加载。这也是为什么Flink官网的例子中,flink相关的jar在pom引入的时候scope都采用了provided,既减少了用户jar包的大小,又能在集群上正常运行,最主要的是如果集群更新了版本,程序可以直接享受更新后的功能,而不用重新打包。
类冲突处理
上面介绍了Flink中的类加载机制,实际开发中可能会因为类加载出现ClassNotFound或者NoSuchMethodError和NoSuchFieldError等异常。这种情况可能是因为你的jar包中的版本和集群的版本不一致,优先加载了集群的class。在不改动集群默认配置的情况下,同时也防止对其他任务的影响,可以考虑对项目中引入的冲突类作shade。如com.typesafe的类发生冲突,maven项目加入如下配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>com.typesafe</pattern>
<shadedPattern>com.mycompany.com.typesafe</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
项目打包后,就会自动把引入和源码中相关的com.typesafe
开头的类修改为com.mycompany.com.typesafe
,由于类的路径不同,类加载器在加载用户jar中的类时就不会与集群中的其他版本冲突。
————————————————
版权声明:本文为CSDN博主「chenxyz707」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/chenxyz707/article/details/109043868