Java类加载过程
1.Java的类加载阶段通常认为主要包含五大阶段:加载阶段、链接阶段、初始化阶段、使用阶段、卸载阶段。其中链接阶段又可以细分为三大主要过程:验证、准备、解析。也即是:加载阶段、链接阶段(验证、准备、解析)、初始化阶段、使用阶段、卸载阶段。
2.加载阶段:主要查找加载二进制的数据文件(.class文件),一、通常是通过全限定名(包名+类名)来加载class文件,但是并没有限制二进制文件的涞源,如:动态代理生成的二进制字节流,ASM同样也可以生成class。二、通过网络获取(热部署)。三、同过压缩文件zip获取,像平时开发过程中的jar、war。四、从数据库中读取。
3.链接阶段-验证:验证阶段主要包括一些基础信息的检验,如文件格式,像Java文件的魔术因子(0xCAFEBABE)。文件的版本号,因为存在兼容问题,高版本的JDK编译的class并不能被低版本的JDK所兼容的。文件的MD5指纹信息等等。元数据验证:主要目的啊是确保class字节流符合JVM的规范,如检查类是不是继承类final类(final类不可以被继承,同样final方法不可以被重写),检查重写的方法是否符合规定(参数、方法名);检查被加载的类是否存在父类,是否实现了接口,判断父类和接口的合法性。检查是否继承自抽象类,并实现所有的抽象方法等等。当然还包括了字节码验证,符号引用的验证。
4.链接阶段-准备:验证阶段没有问题则会进入到准备阶段,注意此时一个类的静态变量会被分配内存(方法区)并且设置初始值,而这个初始值指的是如基本类型的默认值(int默认值为0等),如果引用类型则为null。并不是我们在代码中显式的设置的值。静态的变量被分配到方法区区别于类的实例被分配到堆内存中。需要注意的是静态常量final static
的值是确定的(不需要通过计算获得)。1
2
3
4
5
6public class Test{
private static int a = 1;
private final static int b = 2;
}
在准备阶段a的值并不是1而是0,常量b不需要通过计算来获得是确定的,所以b的值为2.
5.链接阶段-解析:主要将类的符号引用转换为直接引用,包括类接口解析、字段解析、类方法解析、接口方法的解析。
6.初始化阶段:为类的静态变量赋于正确的值,代码中显式设置的值。另外为了有更好的性能JVM的class初始化是一个懒加载机制,并不是一次性加载所有的类。按需加载,JVM规定了class的主动使用和被动使用。
类的主动使用
1.使用关键字new
创建类的实例触发类的初始化。但是区别于对于数组的初始化:1
2
3
4
5
6
7public class Array{
public static void main(String[] args) {
Array[] array = new Array[10];
//只不过开辟类一块连续的内存空间,但是并不会触发Array类的初始化操作
}
}
2.访问一个类的静态变量,包括读取更新等操作会触发类的初始化。同样的还有访问一个类的静态方法。特别注意的是,访问静态的常量(不需要通过计算获得的值)是不会触发初始化操作的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import java.util.Random;
public class Test {
static {
System.out.println("will be initialized");
}
public final static int a = 10;
public final static int RANDOM = new Random().nextInt();
}
public static void main(String[] args) {
System.out.println(Test.a); step1
// System.out.println(Test.RANDOM); step2
}
step1打印的信息:
> Task :FinalTest.main()
10
可以发现当打印a的值时并没有触发Test类的初始化,因为a为静态的常量,在编译期就已经确定了值
step2打印的信息:
> Task :FinalTest.main()
will be initialized
1458173345
虽然RANDOM被final修饰,但是RANDOM的值需要随机数计算来获得,而加载和链接阶段是无法对其计算的,因此触发初始化后才能获得值
3.对类进行反射操作。
4.初始化子类会导致父类的初始化操作。
5.设定为main
方法的类会触发初始化。
类的被动使用
1.除了JVM规定的这几种主动使用的情况,其余的情况则为被动使用。
ClassLoader
1.JVM内置了三大类的加载器BootStrapClassLoader ExtClassLoader ApplicationClassLoader
,并且类的加载过程是按照双亲委托机制来执行的。使用双亲委托机制可以保证一个类在JVM虚拟机中只存在一份,同时也可以避免Java中的核心库被人为的修改。
BootStrapClassLoader
1.根加载器,作为顶层的加载器,没有父类,并且使用的是C++编写实现的。负责Java虚拟机核心类库的加载工作,可以通过-Xbootclasspath来指定路径。
ExtClassLoader
1.扩展类加载器,父类为BootStrapClassLoader,主要加载JAVA_HOME下的jre\lb\ext
的类库,由Java语言实现。
ApplicationClassLoader
1.系统类加载器,主要加载classpath路径下的类库资源,包括引入的一些jar包。当我们自定义加载器时,系统类加载器是作为默认的父加载器的(上图)。
双亲委托机制
1.父委托机制,JVM虚拟机规定,当需要加载一个类时(loadClass),并不会直接加载,而是先交由父类加载一直尝试到顶层的加载器(BootStrap),然后在向下加载:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
2.源码中可以看到resolve
默认是false的,因此加载类默认是不触发初始化类的。整个流程非常清晰,总结主要分为:
1.从当前类加载器缓存中查找被加载类是否已经存在(已经被加载过)直接返回class。
2.判断当前classLoader是否存在父classLoader,存在则调用父classLoader进行加载操作。
3.当前classLoader不存在父classLoader则调用根classLoader加载操作(BootStrapclassLoader)。
4.当所有类classLoader均没有成功加载,则调用当前类classLoader来加载。
3.破坏双亲委托机制,经过流程图的分析,只要将核心方法loadClass
重写。只要区别系统类文件java,javax
开头的交给系统加载。否则由自定义的classloader来实现加载,自定义的无法加载,在委托给跟父classloader,如此向上,只是对系统的加载流程的一开始就委托的流程改变为开始尝试自身区加载。
4.类的卸载:既然类被加载到JVM虚拟机,那么类何时被卸载的呢?JVM规定:
1.当一个class的所有实例都已经被GC,那么class会被卸载(GC)。
2.加载一个类的ClassLoader实例被回收。
3.该类的class实例没有被引用。
Question
1.是否可以实现自定义的Java类库中的同名类(包名、类名相同)而被自定义的类加载器加载呢?按照自定义实现classloader,显然只要绕过同名检查按理说应该是可以的。但是ClassLoader源码对定义的名字同样做了强校验:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
2.可见双亲委托机制同样也是避免自身核心类库被修改。