JVM知识点总结(三)——对象的创建与类加载过程

首先需要区别开对象的创建和类加载的含义,对象的创建是指实例的创建过程,而类加载机制指的是一个类的类信息的加载过程。
有许多的面试题,关于成员变量、构造函数、static变量、static方法的加载顺序,都与本文的内容息息相关,例如:Java中构造方法的执行顺序,这篇博文就是一个常见的考题。实际上static修饰的成员和非static修饰的成员的初始化是两个过程。

对象的创建

对象创建流程
上图显示的就是通常所说的一个实例的创建的过程,可以看到,其中包含了类加载过程。

类加载过程

类的声明周期
加载、验证、准备、初始化和卸载这5个阶段是确定的,但解析阶段的顺序不是固定的,它有时可以在初始化之后再开始,这是为了支持Java的动态绑定。注意,这里的顺序都是指的开始顺序,而不是完成或者进行,下一个流程开始的时候,上一个流程未必结束,甚至可能存在多个流程交叉运行的情况。

加载

加载过程主要完成3件事:

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

二进制字节流可以从文件中、zip包、网络上、运行时计算生成(动态代理)、JSP(由JSP文件生成对应的Class类)。相对于类加载过程的其他阶段,一个非数组类的加载阶段是程序员可控性最强的,因为可以使用程序员自定义的类加载器(ClassLoader)去完成加载。数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但是数组类的元素类型最终还是要靠类加载器去创建。Hotspot虚拟机中,Class对象比较特殊,虽然它是对象,但是存放在方法区中。这个对象将作为程序访问方法区中的这些类型数据的外部接口。

验证

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

  1. 文件格式验证:常量池的常量中是否有不被支持的类型、指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量等。
  2. 元数据验证:这个类是否有父类、父类是否继承了不允许被继承的类、是否实现了父类或接口中要求实现的方法、类中成员是否与父类冲突等。
  3. 字节码验证:一些字节指令合法性等方面的验证。
  4. 符号引用验证:发生在连接的解析阶段,即虚拟机将符号引用转化为直接引用的时候。
准备

准备阶段是正式为类变量(指的是static变量,不包括实例变量)分配内存并设置类变量初始值的阶段,这些变量分配在方法区。需要注意:该阶段进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量,实例变量会在对象实例化的时候随着对象一起分配在Java堆中。
例如:public static int value=666;类变量value在准备阶段后的值为0,把value赋值为666的动作将在初始化阶段才会执行。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用是用一组符号来描述所引用的目标,符号可以使任何形式的字面量,只要使用时能无歧义地定位到目标即可;直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

初始化

初始化是类加载过程的最后一个阶段,在前面的各个阶段中,除了加载阶段可以通过自定义类加载器参与外,其他都是由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java代码。
在准备阶段,类变量经过了一次系统要求的初始化操作,而在初始化阶段,则会根据程序员定制的计划去初始化类变量和其他资源,这里会调用自动生成的< clinit>方法。< clinit>方法是由编译器自动收集类中的所有类变量(static变量)赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。

public class Test{
    static{
        i=0;
        System.out.println(i);  //非法向前引用
    } 
    static int i=1;
}

< clinit>方法不是必需的,如果类中不包含静态语句块也不包含对类变量的赋值操作,那么编译器可以不生成< clinit>()方法。< clinit >方法会自动调用父类的< clinit>方法,无需手动调用。这意味着第一个调用的一定是Object类的< clinit>方法。

类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。一般的说,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必不相等。

类加载器分类

前面在加载的阶段时提到过类加载器,从虚拟机的角度来说,只存在两种类型:Bootstrap ClassLoader,由C++语言实现,是虚拟机的一部分;其他类加载器,由Java语言实现,独立于虚拟机外部,且全部继承自java.lang.ClassLoader。
从开发者角度可以分为:
1. Bootstrap ClassLoader: 负责加载JAVA_HOME\lib目录下和-Xbootclasspath参数指定路径下能被虚拟机识别的类库到虚拟机内存中。
2. Extension ClassLoader:负责加载JAVA_HOME\lib\ext目录下和java.ext.dirs系统变量指定的路径的类库。
3. Application ClassLoader:负责加载用户路径(ClassPath)上的所有指定的类库。
如图:
类加载器

双亲委派模型

双亲委派模型要求除了顶层的Bootstrap ClassLoader以外,其余的类加载器都应当有自己的父类加载器,这里的父子关系一般不会以继承来实现,而是使用组合。双亲委派模型不是一个强制性的约束么事Java设计者推荐给开发者的一种类加载器实现方式。
双亲委派模型的工作过程:一个类加载器收到类加载请求后,它不会自己尝试去加载,而是把这个请求委托给父类,以此类推,因此所有的加载请求最终都应该传到顶层的Bootstrap ClassLoader中,只有当父加载器反馈无法加载,子加载器才会尝试自己去加载。

破坏双亲委派模型

1.JNDI,JDBC等线程上下文类加载器。
2.代码热替换,不需要重启系统,完成部分代码的替换工作。

参考链接:

  1. Java中构造方法的执行顺序
  2. 《深入理解Java虚拟机》

猜你喜欢

转载自blog.csdn.net/xtick/article/details/81026848