ClassLoader 类加载-type checking 对类加载的影响

Type Checking

Type Checking (类型检测) 的作用是分析程序在编译或者运行期间,其类型表达是否一致的一个过程。举个例子:如果一个变量被声明为 int 类型,那么他就不能被赋值为实际的值(或者字符串类型、或者其他任何类型)。java 语言的类型检测分为两种:

  • 静态类型检测(static checking): 问题在程序运行之前被自动找到,也就是在编译阶段完成的检查。静态类型检测更多的是关注在”类型“上。
  • 动态类型检测(dynamic checking): 问题在运行期间被检测,动态运行检测关注的是在”值“上。

本文主要介绍静态类型检测。java 语言在编译时会做大量的类型检测,只要你声明了一个变量的类型,编译器将会确保只有相应类型的值可以被赋值给这个变量(或者这个值的类型是变量类型的子类型)。比如,如果你声明了如下变量:

1
int x;

这里可以确保它只保存 int 值。但是,如果将变量声明为 List,则该变量可能包含列表的子类型,包括 ArrayList、LinkedList 等。

Type Checking 对类加载的影响

前面提到静态类型检测主要是对类型的检测,而 java 语言中,类型一致表示的是 类全限定名+ClassLoader 一致,所以在做类型检测时就必定会涉及到某些类的 class load 操作。下面我们就从几个方面来分析下类型检测对于类加载的影响。

在 jvm 参数中配置 -verbose:class 可以观察类加载过程

方法的返回类型

在下面的例子中, Main 执行过程,check 方法没有被调用,但是该方法返回了一个非 ClassA 的类型,也就是类型 ClassB。那么类型检测就要求就提前加载 ClassA 和 ClassB 类型,加以验证,因此加载顺序如下(ClassA –> ClassB –> ClassC –> ClassD)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ClassA {}
public class ClassB extends ClassA{}
public class ClassC {}
public class ClassD {}
public class Main {
static ClassC c;
static {
c = new ClassC();
}
public static void main(String[] args) {
new ClassD();
}
ClassA check(ClassA a) {
return new ClassB();
}
}

执行查看类加载顺序如下:

1
2
3
4
5
6
7
8
[Loaded com.glmapper.bridge.boot.methodreturn.Main from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.methodreturn.ClassA from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded com.glmapper.bridge.boot.methodreturn.ClassB from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.methodreturn.ClassC from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded com.glmapper.bridge.boot.methodreturn.ClassD from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]

从这里可以看到,静态域不一定会比非静态域先加载,这里就是因为静态检测提前出发了类的加载导致。

方法参数

先来看下下面这段代码,大家可以想一下类加载顺序是什么样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ClassA {}
public class ClassB extends ClassA{}
public class ClassC {}
public class Main {
static ClassC c;
static {
c = new ClassC();
}
public static void main(String[] args) {
Main main = new Main();
main.m(new ClassB());
}
void m(ClassA a) {}
}

按照我们惯性理解,Main 加载之后,会加载 ClassC,然后再加载 ClassA 和 ClassB。但是事实是这样吗?通过 -verbose:class 参数执行结果如下:

1
2
3
4
5
6
7
8
[Loaded com.glmapper.bridge.boot.paramscheck.Main from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.paramscheck.ClassA from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded com.glmapper.bridge.boot.paramscheck.ClassB from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.paramscheck.ClassC from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]

但是从这里看到,Main 执行时, ClassA ,ClassB 先于 ClassC 加载了。原因是类型检测过程中,会一行行先行的看你的代码,在这个场景中,它发现有 m(ClassA a) 方法,但是代码中传入了 ClassB 这个类型,那么在真正运行 main 方法之前,在运行 Main 的 static 块之前,先行加载了 ClassA 和 ClassB 两个类型,然后验证它们之间的关系。所以看到的类加载顺序是 ClassA -> ClassB -> ClassC ,而非我们概念中的 ClassC -> ClassA -> ClassB。

变量赋值

最后一种场景是变量赋值,来看下面的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ClassA {}
public class ClassB extends ClassA{}
public class ClassC {}
public class ClassD {
ClassA a;
}

public class Main {
static ClassC c;

static {
c = new ClassC();
}

public static void main(String[] args) {
ClassD d = new ClassD();
d.a = new ClassB();
}
}

启动 main 方法时,在 jvm 参数中配置 -verbose:class 来观察类型加载顺序;

1
2
3
4
5
6
7
8
9
[Loaded com.glmapper.bridge.boot.variableassign.Main from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.variableassign.ClassA from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded com.glmapper.bridge.boot.variableassign.ClassB from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.variableassign.ClassC from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded com.glmapper.bridge.boot.variableassign.ClassD from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]

是不是又有点出乎意料呢?类型检测发现 Main 中包含了 d.a = new ClassB() 的语句,其中 d.a 的类型不是 ClassB,因此会先于 main 方法执行以及先于 Main 中的 static 块执行进行加载。 类型检测,将类型 ClassA 和 ClassB 的加载“提前”了。

小结

本文主要介绍了静态类型检测对于 Class Loader 加载类顺序的影响,了解此逻辑对于在考虑多 class loader 场景处理问题非常有用,对于常规的类似 ClassCastExcetion, LinkageError 等异常排查有一定的意义。

ClassLoader 类加载-type checking 对类加载的影响

http://www.glmapper.com/2020/05/01/jvm/java-base-classloader-typecheck/

作者

卫恒

发布于

2020-05-01

更新于

2022-04-23

许可协议

评论