虚拟机类加载机制

类加载的过程

类从被加载到虚拟机内存中开始,到卸载处内存为止
整个生命周期包括:
1.加载
2.验证
3.准备
4.解析
5.初始化
6.使用
7.卸载

*其中:验证,准备,解析 统称为“连接”

加载,验证,准备,初始化,卸载 这五个阶段的开始顺序是确定的(注意是"开始",不是"完成",因为很多时候是交叉混合的进行)。
(解析则不一定:某些情况下可以在初始化阶段之后再开始)

一.”加载”

(“加载”只是”类加载”过程的5个阶段之一,不要混淆)
此阶段需要完成3件事:
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
以上中,获取(除了数组类)类的二进制字节流是开发人员可控性最强的。
*(如果数组类,本身并不通过类加载器创建,而是由JAVA虚拟机直接创建,但是数组类的元素类型需要类加载器)
加载阶段完成后,二进制字节流就按照虚拟机所需的格式存储在方法区之中。

二.“验证”

验证是连接阶段的第一步。
目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的健康。
  验证的4个检验流程:
    1.文件格式验证:是否符合Class文件格式的规范并且能被当前版本的虚拟机处理。
                    (例如:魔数验证,版本号验证,常量池类型验证,编码等等...)
    2.元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合JAVA规范。
                  (例如:是否有父类,非抽象类是否实现了父类的全部方法等等..)
    3.字节码验证:通过数据流和控制流分析,确定程序语义是合法且符合逻辑的。
                  保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
                  (例如:保证方法体中类型转换是有效的,保证跳转指令不会跳转到方法体以外等等..)
    4.符号引用验证:此校验发生在虚拟机将符号引用化为直接引用的时候。
                  (例如:符号引用中通过字符串描述的全限定名是否能找到对应的类..)

三.“准备”

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。
注意*1.这个阶段进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量。
    *2.初始值在"通常情况"下指的是数据类型的“零值”,而不是定义后的值!
       “特殊情况”:如果类字段中存在ConstantValue属性,就会直接被赋值。
                   (例如:public static final int a = 123;)

四.“解析”

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
  符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量。
  直接引用:可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。
解析过程:具体看P221页
  1.类或接口的解析
  2.字段解析
  3.类方法解析
  4.接口方法解析

五.“初始化”

在准备阶段,变量已经赋过一次系统要求的初始值了,而在初始化阶段,则开始执行类构造器方法<clinit>()。
  <clinit>()方法:
    由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并的。
    收集的顺序是由语句在源文件中出现的顺序所决定的。
    (静态语句块只能访问到定义在其之前的变量,在其之后的变量,静态语句块只能赋值,不能访问。)

“初始化”的五种情况:

    1.遇到new,getstatic,putstatic,invokestatic这四条字节码指令,如果还未初始化就需要初始化。
        (常见代码场景:new关键字,读取或设置静态字段,被final修饰,调用静态方法)
    2.对类进行反射调用的时候
    3.当初始化一个类时,发现父类没有初始化则先触发父类的初始化。
    4.当虚拟机启动时,用户需要指定一个要执行的主类main时。
    5.如果一个java.lang.invoke.MethodHandle实例最后的解析结果的方法句柄对应的类没有初始化时。
以上为主动引用。
***注意:如果是接口的初始化,并不需要父接口完成初始化,而是当使用到了父接口才会初始化。

—-被动引用(并不会触发初始化!!!)
例如:1.通过子类引用父类的静态字段,不会导致子类初始化。
2.通过数组定义来引用类,不会触发此类的初始化。 3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。


类加载器

类加载阶段的“通过全限定名获取二进制流”这个动作在虚拟机外部的类加载器实现。

类与类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在JAVA虚拟机中的唯一性。
(比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。)
这里指的相等,包括代表类的Class对象的equals(),isAssignableFrom(),isInstance()方法。

类加载器

在虚拟机的角度上,只存在两种不同的类加载器:
 1.启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
 2.其它所有的类加载器,这些都由Java语言实现,独立于虚拟机外部,并全部继承自java.lang.ClassLoader。

从开发人员的角度上,可以分的更细
1.启动类加载器:将lib目录中的或被-Xbootclasspath指定的路径中的,且虚拟机能识别的类库加载到虚拟机内存
2.扩展类加载器:负责加载lib\ext目录中的或被java.ext.dirs系统变量所制定的路径中的所有类库
                (开发者可以直接使用的)
3.应用程序类加载器:也称为系统类加载器,负责加载用户类路径上所指定的类库。
                    一般情况下是程序默认的类加载器 (开发者可以直接使用的)

双亲委派模型

双亲委派模型要求除了顶层的启动类加载器以外,其余的类加载器都应当有自己的父类加载器。
(类加载器之间的父子关系不是继承,而是“组合”)

启动类加载器<--扩展类加载器<--应用程序类加载器<--自定义类加载器
*过程:当一个类加载器收到了类加载的请求,它首先不会自己去加载,而是把这个请求委派给父类加载器,因此所有的请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个这个请求(在搜索范围内没有找到所需的类)时,子加载器才会尝试自己加载。

*好处:使用双亲委派模型可以保证JAVA程序的稳定运作。
    举例:Object类在rt.jar包中,无论那个类加载器都要加载这个类,并且最终都是委派给最顶端的启动类加载器来加载,这样就保证了所有的Object类在程序的各种类加载器环境下都是同一个类。

—-破环双亲委派模型
使用线程上下文类加载器…….


两种类的加载方式

通常用这两种方式来动态加载一个 java 类,

Class.forName() 与 ClassLoader.loadClass() 但是两个方法之间也是有一些细微的差别

Class.forName() 方式

查看Class类的具体实现可知,实质上这个方法是调用原生的方法:

private static native Class<?> forName0(String name, boolean initialize,ClassLoader loader);
形式上类似于Class.forName(name,true,currentLoader)。 综上所述,Class.forName 如果调用成功会保证一个Java类被有效得加载到内存中;
类默认会被初始化,即执行内部的静态块代码以及保证静态属性被初始化;

默认会使用当前的类加载器来加载对应的类。

ClassLoader.loadClass方式

如果采用这种方式的类加载策略,由于双亲托管模型的存在,最终都会将类的加载任务交付给Bootstrap ClassLoader进行加载。跟踪源代码,最终会调用原生方法:

private native Class<?> findBootstrapClass(String name);
与此同时,与上一种方式的最本质的不同是,类不会被初始化,只有显式调用才会进行初始化。
综上所述,ClassLoader.loadClass 如果调用成功类会被加载到内存中;
类不会被初始化,只有在之后被第一次调用时类才会被初始化;
之所以采用这种方式的类加载,是提供一种灵活度,可以根据自身的需求继承ClassLoader类实现一个自定义的类加载器实现类的加载。(很多开源Web项目中都有这种情况,比如tomcat,struct2,jboss。原因是根据Java Servlet规范的要求,既要Web应用自己的类的优先级要高于Web容器提供的类,但同时又要保证Java的核心类不被任意覆盖,此时重写一个类加载器就很必要了)