美文网首页Android开发Android开发经验谈
深入理解Java虚拟机之类加载机制

深入理解Java虚拟机之类加载机制

作者: 4e70992f13e7 | 来源:发表于2018-09-15 17:47 被阅读6次

虚拟机类加载机制

JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,顺序如下

image

在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

加载

加载(Loading)阶段是类加载(Class Loading)过程的第一个阶段,在此阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

加载阶段即可以使用系统提供的类加载器在完成,也可以由用户自定义的类加载器来完成。加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

验证

1. 文件格式验证,是要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。如验证魔数是否0xCAFEBABE;主、次版本号是否正在当前虚拟机处理范围之内;常量池的常量中是否有不被支持的常量类型……该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区中,经过这个阶段的验证后,字节流才会进入内存的方法区中存储,所以后面的三个验证阶段都是基于方法区的存储结构进行的。

2. 元数据验证,是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。可能包括的验证如:这个类是否有父类;这个类的父类是否继承了不允许被继承的类;如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法……

3. 字节码验证,主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的;但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。

4. 符号引用验证,发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析阶段”中发生。验证符号引用中通过字符串描述的权限定名是否能找到对应的类;在指定类中是否存在符合方法字段的描述符及简单名称所描述的方法和字段;符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问

验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。如果所运行的全部代码确认是安全的,可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。

准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

public static int value=123;//在准备阶段value初始值为0 。在初始化阶段才会变为123 。

如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值

//个人理解:类常量分配在常量池
public static final int value=123;//在准备阶段value初始值为0 。在初始化阶段才会变为123 。

解析

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

符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。

初始化

类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。

何时开始 加载
  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令(注意,newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类[Ljava.lang.String的初始化,而直接不会触发String类的初始化)时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:
  • 使用new关键字实例化对象的时候;
  • 读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候;
  • 调用一个类的静态方法的时候。
  1. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  2. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  3. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

  4. 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

注意,对于这五种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这五种场景中的行为称为对一个类进行 主动引用。除此之外,所有引用类的方式,都不会触发初始化,称为 被动引用。

示例1

Parent.class

public class Parent {
    static {
        System.out.println("Parent init!!!");
    }
    public static int value = 23;
}

Child.class

public class Child extends Parent {
    static {
        System.out.println("Child init!!!");
    }
}

Client.class

public class Client {
    public static void main(String[] args) {
        System.out.println(Child.value);
    }
}

结果:

Parent init!!!
23

结论:
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会出发子类的初始化。

示例2

Client.class

public class Client {
    public static void main(String[] args) {
       Parent[] parents = new Parent[10];
    }
}

结果:

什么都没有输出

结论:
通过数组定义来引用类,不会触发类的初始化

示例3

Client.class

public class Client {
    public static void main(String[] args) {
       Child child = new Child();
    }
}

结果:

Parent init!!!
Child init!!!

结论:
当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化

示例4

Child2.Class

public class Child2 {
    static {
        System.out.println("Child2 init!!!");
    }

    public static final String HELLO_WORLD = "hello world";
}

Client.class

public class Client {
    public static void main(String[] args) {
       Child child = new Child();
    }
}

结果:

hello world

结论:
虽然引用了Child2中的HELLO_WORLD,但其实此常量已经被储存到了类的常量池中,,并没有Child类的符号引用

小结:

  1. 子类调用父类的静态变量,子类不会被初始化。只>有父类被初始化。。对于静态字段,只有直接定义这>个字段的类才会被初始化.
  2. 通过数组定义来引用类,不会触发类的初始化
  3. 访问类的常量,不会初始化类
示例5
class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;

    private SingleTon() {
        count1++;
        count2++;
    }

    public static SingleTon getInstance() {
        return singleTon;
    }
}

public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

**Singleton输出结果:1 0 **

分析:

  1. 首先执行main中的Singleton singleton = Singleton.getInstance(); 调用静态方法触发加载
  2. 类的加载:加载类Singleton
  3. 类的验证
  4. 类的准备:为静态变量分配内存,设置默认值。这里为singleton(引用类型)设置为null,value1,value2(基本数据类型)设置默认值0
  5. 类的初始化(按照赋值语句进行修改):
    执行private static Singleton singleton = new Singleton();
    执行Singleton的构造器:value1++;value2++; 此时value1,value2均等于1
    执行
    public static int value1;
    public static int value2 = 0;
    此时value1=1,value2=0
示例6
class SingleTon {
    public static int count1;
    public static int count2 = 0;
  private static SingleTon singleTon = new SingleTon();

    private SingleTon() {
        count1++;
        count2++;
    }

    public static SingleTon getInstance() {
        return singleTon;
    }
}

public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

**Singleton输出结果:1 1 **

分析:

  1. 首先执行main中的Singleton2 singleton2 = Singleton2.getInstance2();
  2. 类的加载:加载类Singleton2
  3. 类的验证
  4. 类的准备:为静态变量分配内存,设置默认值。这里为value1,value2(基本数据类型)设置默认值0,singleton2(引用类型)设置为null,
  5. 类的初始化(按照赋值语句进行修改):
    执行
    public static int value2 = 0;
    此时value2=0(value1不变,依然是0);
    执行
    private static Singleton singleton = new Singleton();
    执行Singleton2的构造器:value1++;value2++;
    此时value1,value2均等于1,即为最后结果

类加载器

虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
  2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
  3. 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

image

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

双亲委派模型的规定通俗来讲就是:子加载器加载的类可以使用父加载器加载的类,但是父加载器加载的类不能使用子加载器加载的类。

双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。

使用双亲委派模型的好处

在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系,保证了系统的安全性,防止内存中出现多份同样的字节码。
例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行,使用自定义类加载器强行加载一个java.开头的类也是会抛出SecurityException。

相关文章

网友评论

    本文标题:深入理解Java虚拟机之类加载机制

    本文链接:https://www.haomeiwen.com/subject/qadtnftx.html