类加载机制--浅谈

一、定义:

    类加载(Class Loading)是一种机制,他描述的是将字节码以文件形式加载到内存再经过连接、初始化后,最终形成可以被虚拟机直接使用的Java类型地过程。

    Class Loading 包含了加载(Loading)、连接(Linking)、初始化(Initialization)三大部分,其中Linking又包含了三个部分:校验(Verification)、准备  (Preparation)、解析(Resolution)。而一个类的生命周期只是在Class Loader的基础上多了:使用(Using),卸载(Unloading)两部分。

Class Loaders的组成:

二、加载阶段

虚拟机需要完成以下3件事情:

1)通过一个类的全限定名来获取定义此类的二进制字节流。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

注意:

    1.JVM在加载数组的时候加载的仅仅是数组的类型类(例如String[] 加载器只会加载String这个类型类),而数组的创建则由JVM直接完成。

这里我们多问几个为什么:
    2. JVM为什么只加载数组的类型类:
我认为JVM这样做的目的主要是为了节省时间,我们知道数组里面装的都是同一种类型的元素,JVM没必要将一个重复的内容加载多次浪费时间。
    3. N维数组怎么加载:
如果是N维数组,类加载器会从最外层开始一层一层的递归加载,直到加载到非数组类型为止。
    4. 引用类型与基本类型加载起来会不会有区别:
其实基本类型早已经在javac阶段装箱成封装对象了,例如int会被装箱成Integer,long装箱成Long等等,所以是没有区别的。

三、验证

      验证是连接阶段的第一步,这一阶段目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。

      虚拟机规范对这个阶段的限制和指导非常笼统,仅仅说了一句如果验证到输入的字节流不符合Class文件的存储格式,就抛出一个java.lang.VerifyError异常或者其子类异常。具体应当检查哪些方面,如何检查,何时检查,都没有强制要求或明确说明,所以不同的虚拟机对验证的实现可能会有所不同,但大致上都会完场下面四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

        是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。但从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  4. 符号引用验证:确保解析动作能正确执行。

四、准备阶段

       是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配

       这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

public static int value=123;

那变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。表7-1列出了Java中所有基本数据类型的零值。

假设上面类变量value的定义变为:public static final int value=123;

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。

五、解析阶段

是虚拟机将常量池内的符号引用替换为直接引用的过程

六、初始化

在连接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员自己写的逻辑去初始化类变量和其他资源,举个例子如下:

    public static int value1  = 5;
    public static int value2  = 6;
    static{
        value2 = 66;
    }

在准备阶段value1和value2都等于0;

在初始化阶段value1和value2分别等于5和66;

  • 所有类变量(类变量是声明在class内,method之外,且使用static修饰的变量)初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用;
  • 编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;
  • 如果超类还没有被初始化,那么优先对超类初始化,但在<clinit>方法内部不会显示调用超类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的超类<clinit>方法已经被执行。
  • JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。(所以可以利用静态内部类实现线程安全的单例模式)
  • 如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类<clinit>方法;

何时触发初始化

  1. 为一个类型创建一个新的对象实例时(比如new、反射、序列化)
  2. 调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
  3. 调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式
  4. 调用JavaAPI中的反射方法时(比如调用java.lang.Class中的方法,或者java.lang.reflect包中其他类的方法)
  5. 初始化一个类的派生类时(Java虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作,接口例外)
  6. JVM启动包含main方法的启动类时。

七、系统的类加载器

类加载器名称

加载的范围

启动类加载器

Bootstrap ClassLoader

存放在<JAVA_HOME>\lib目录中的,并且是虚拟机 识别的类库加载到虚拟机内存中

扩展类加载器 Extension ClassLoader

存放在<JAVA_HOME>\lib\ext目录中的所有类库, 开发者可以直接使用;

应用程序加载器 Application ClassLoader

加载用户类路径上指定的类库,开发者可以直 接使用,一般情况下这个就是程序中默认的类 加载器;

八、双亲委派机制

        某个特定的类加载器在接到加载类的请求 时,首先将加载任务委托给父类加载器, 依次递归,如果父类加载器可以完成类加 载任务,就成功返回;只有父类加载器无 法完成此加载任务时,才自己去加载

        双亲委派模型好处:Java类随着它的类加载器一起具备了带 有优先级的层次关系,保证java程序稳 定运行

九、示例代码

public class SuperClazz {

    public SuperClazz(){
        System.out.println("我是父类构造方法");
    }

    static {
        System.out.println("我是父类静态代码块");
    }

    public static int value = 123;

    public static final String STR = "hello world";

    public static final int WHAT = value;
}

public class SonClazz extends SuperClazz {

    public SonClazz(){
        System.out.println("我是子类构造方法");
    }
    static {
        System.out.println("我是子类静态代码块");
    }
}
public class DemoTest {
public static void main(String args[]){ /*
        System.out.println(SonClazz.value);
输出结果:
我是父类静态代码块
123
结论:对于静态字段,只有直接定义这个字段的类,才能被初始化。
*/
/*
SuperClazz[] aa = new SuperClazz[10];
输出结果:(什么也没有输出)
结论:只是声明了一个数组形式的变量,类没有初始化
*/

/*
System.out.println(SonClazz.STR);
输出结果:hello world
结论:编译期的传播优化,因为是常量,所以就会直接编译到了运行的类里面,就不会再去定义的类里面找了
,STR还是在常量池里面的;
*/

     /**

      我是父类静态代码块
      123

    */
//System.out.println(SonClazz.WHAT);


}
 
public class DemoTest2 {
    static
    {
        i=0;
        System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)
    }
    static int i=1;
}
public class DemoTest2 {
    static
    {
        i=2;
//      System.out.println(i);
    }
    static int i=1;

    public static void main(String args[])
    {
        System.out.println(i);
    }
}

输出结果:1

 

posted @ 2019-04-25 23:06  宥宥美美  阅读(162)  评论(0编辑  收藏  举报