张龙老师在圣思源的基础最后部分讲解了一下java的类装载机制,当时作为初学者,感觉听起来挺费劲的,现在再看了一下这视频,感觉有点恍然大悟,很多知识点也明白了,现在总结一下,结合别人写的以及自己的理解,也给自己做个笔记!
类的加载过程
java虚拟机与程序的生命周期,在如下几种情况下,java虚拟机将结束生命周期:
1、执行了System.exit()方法,我们在Swing中经常使用
2、程序正常执行结束
3、程序在执行过程中遇到了异常或错误而异常终止
4、由于操作系统出现错误而导致java虚拟机进程终止
JVM将类加载过程分为三个步骤:装载(Load),连接(Link)和初始化(Initialize)链接又分为三个步骤,如下图所示:
1) 装载:查找并加载类的二进制数据,将类从硬盘或网络上或其他地方装载到JVM中
2)连接:
验证:确保被加载类的正确性;
准备:为类的静态变量分配内存,并将其初始化为默认值;
解析:把类中的符号引用转换为直接引用;
3)初始化:为类的静态变量赋予正确的初始值;
那为什么我要有验证这一步骤呢?首先如果由编译器生成的class文件,它肯定是符合JVM字节码格式的,但是万一有高手自己写一个class文件,让JVM加载并运行,用于恶意用途,就不妙了,因此这个class文件要先过验证这一关,不符合的话不会让它继续执行的,也是为了安全考虑吧。
准备阶段和初始化阶段看似有点牟盾,其实是不牟盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10;
如下图所示:
我们来看一下下面这段代码:
package com.harderxin.classloader;
class StaticTest{
static StaticTest staticTest = new StaticTest();
public static int a;
public static int b = 0;
public StaticTest(){
a++;
b++;
}
public static StaticTest getInstance(){
return staticTest;
}
}
public class Test1 {
public static void main(String[] args) {
StaticTest.getInstance();
System.out.println(StaticTest.a);
System.out.println(StaticTest.b);
}
}
运行后的结果为:a的值为1,b的值为0,这就用到了我们上面所说的这几个阶段了:
1)装载:把StaticTest.class从本地硬盘装载到虚拟机里面
2) 连接:验证:该类通过Eclipse编译器通过,是属于正确的
准备:为staticTest和a、b静态变量分配内存,并初始化为默认值:staticTest = null,a = 0, b = 0
解析:把类中的符号引用转换为直接引用;
3)初始化:为类的静态变量赋予正确的初始值:自上而下,staticTest = new StaticTest();此时 a++得到1,b++得到1,后面接下来给a赋予正确的初始值,因为a没有显示赋值,所以a的值为1,b显示赋值为0,所以b又等于0了,所以输出结果为:1,0
好,我们把代码的位置换一下,把staticTest = new StaticTest()放在后面,如下所示:
class StaticTest{
public static int a;
public static int b = 0;
static StaticTest staticTest = new StaticTest();
public StaticTest(){
a++;
b++;
}
public static StaticTest getInstance(){
return staticTest;
}
}
输出结果为:1,1
同样我们可以分析,在连接准备阶段,静态变量a、b都赋予默认值为0,静态变量staticTest赋值为null,当进行初始化的时候,自上而下,a没有显示指定默认值,还是为0,b显示指定默认值为0,staticTest 赋值为引用类型new StaticTest(),然后执行a++得到a的值为1,b++得到b的值为1
希望通过这个例子,大家能深入理解类的加载过程!
类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的java.lang.Class对象,用来封装类在方法区类的对象。看下面2图
类的加载的最终产品是位于堆区中的Class对象
Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口
加载类的方式有以下几种:
1)从本地系统直接加载
2)通过网络下载.class文件
3)从zip,jar等归档文件中加载.class文件
4)从专有数据库中提取.class文件
5)将Java源文件动态编译为.class文件(服务器)
类的初始化
java对类的使用分为主动使用和被动使用,所有java虚拟机实现必须在每个类或接口被java程序"首次主动使用"才初始化它们,主动使用即导致类的初始化,那么类什么时候才被初始化,主要有以下六种:
1)创建类的实例,也就是new一个对象:new Test();
2)访问某个类或接口的静态变量,或者对该静态变量赋值 int b = Test.a、Test.b = 6;
3)调用类的静态方法:Test.doSomeThing();
4)反射(Class.forName("com.lyj.load"))
5)初始化一个类的子类(会首先初始化子类的父类),导致对父类的主动使用
6)JVM启动时标明的启动类,即文件名和类名相同的那个类 java com.harderxin.Test
只有这6中情况才会导致类的类的初始化,除了以上六种,都是对类的被动使用,都不会对类进行初始化,也就是不会进行到上面的步骤3)
类的初始化步骤:
1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3)加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句
类加载器
JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
1)Bootstrap ClassLoader
负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
2)Extension ClassLoader
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
3)App ClassLoader
负责记载classpath中指定的jar包及目录中class
4)Custom ClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
父亲委托机制
父类委托机制除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。之前看的很多java教程里面都讲到,这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。
父类委托机制工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层级的类加载都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
JVM的ClassLoader采用的是树形结构,除了根类加载器以外,每个ClassLoader都会有且仅有一个父类加载器,用户自定义的ClassLoader默认的父类加载器是系统类加载器,当然你可以自己指定需要用个ClassLoader的实例,我们来看他们的父子关系:
父类委托机制中,当一个java程序请求加载器loader1加载Hello类时,loader1首先委托自己的父亲加载器加载hello类,若父亲加载器能加载,则由附加器完成加载人物,否则才由加载器loader1本身加载Hello类。下面我们来再次看一下java虚拟机自带的几个加载器:
除了java虚拟机自带的加载器之外,我们用户自己也可以自定义自己的类加载器,根据自己的需要。Java提供了抽象类java.lang.ClassLoder,所有用户自定义的类加载器都要继承这个classloader类。
注:加载器之间的父子关系实际上指的是加载器对象之间的包装关系,而不是类之间的继承关系。一对父子加载器可能是同一个加载器类的两个实例,也可能不是。在子加载器对象中包装了一个父加载器对象.当生成一个自定义的类加载器实例时,如果没有指定它的父加载器,那么系统类加载器就将成为该类加载器的父加载器。如果在构造方法中指定父类加载器那么父类加载器就是指定的加载器。证明如下:
ClassLoader loader1 = new MyClassLoader();
//参数loader1将作为loader2的父加载器
ClassLoader loader2 = new MyClassLoader(loader1);
当Java虚拟机要加载一个类时,到底该派哪个类加载器去加载呢 ?我们看下图:
Loader1和loader2是我们自己定义的两个类加载器,loader1和loader2是父子关系。现在我们想让loader2这个类加载器加载我们自己写的一个Sample类:loader2.loadclass(“sample”),我们来分析一下看看到底应该用哪一个类加载器去加载。当这段代码被执行时,loader2首先到自己的命名空间去查找Sample类是否已经被加载,如果被加载就直接返回这个类的class对象的引用。如果Sample类还没有被加载,loader2首先请求loader1代为加载,loader1再请求系统类加载器代为加载,系统类加载器再请求扩展类加载器,扩展类加载器再请求根类加载器,若根类加载器和扩展类加载器都不能加载,则系统类加载器尝试加载,若能加载,则将Sample类所对应的Class对象的引用返回给loader1,loader1在将引用返回给loader2,从而成功将Sample类加载到虚拟机。若系统类加载器不能加载Sample类,则loader1尝试加载Sample了哦,若loader1不能加载,则loader2尝试,若所有的类加载都不能加载,则抛出ClassNotFoundException异常。
定义类加载器:如果某个类加载器能够加载一个类,那么该类加载器就称作:定义类加载器;定义类加载器及其所有子加载器都称作:初始类加载器
父委托机制的优点就是能够提高软件系统的安全性。因为在词机制下,用户自定义的类加载器不可能加载本应该由父加载器加载的可靠类,从而防止不可靠的恶意代码代替由父类加载器加载的可靠类,从而防止不可靠的甚至恶意的代码代替由父类加载器加载的可靠代码。如,java.lang.Object类总是由根类加载器加载的,其他任何用户自定义的类加载器都不可能加载含有恶意代码的java.lang.Object类。
命名空间,其实这里所说的命名空间就是我们java中常用的package,每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类的组成。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个雷;在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一个运行时包,不仅要看他们的包名称是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类之间才能相互访问可见(默认访问级别)的类和成员。假设用户自定义了一个类java.lang.TestCase并由用于自定义的类加载器加载,由于java.lang.TestCase和核心类库java.lang.*由不同的类加载器加载,他们属于不同的运行时包,所以java.lang.TestCase不能访问核心库java.lang包中的包可见成员。