类的生命周期:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析可统称为连接。

wKiom1h9qhnwLr_YAADkMj3URi8535.png

加载与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。但是这两个阶段总的开始时间和完成时间总是固定的,加载总是在连接之前开始,连接总是在加载完成之后完成。-Xverify:none关闭验证,只有加载阶段用户可控,其它都由JVM完成。

四个验证阶段:文件格式、元数据、字节码、符号引用。

类加载过程的第一个阶段:加载,此时虚拟机需要完成三件事情:

       1、 通过类的全限定名来获取类的二进制字节流。

           执行文件格式验证,验证字节流能正确地解析,验证通过后,字节流存贮在方法区,后面的三个验证都是基于方法区的存储结构进行。

       2、 将字节流的静态存储结构转化方法区的运行时数据结构。

       3、 在堆中生成一个代表该类的java.lang.Class对象,作为方法区这些数据的访问入口。

准备阶段:为类的静态变量分配内存并将其初始化为默认值,如果字节码含有ConstantValue属性的字段(final 属性),准备阶段会将其初始化为指定值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在堆中。

类加载器
类加载由JVM外部实现,让应用程序决定如何获取所需的类,JVM提供了3种类加载器:
1、Bootstrap:根加载器,本地代码(C++)实现,加载基础核心类库(rt.jar);
2、Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
3、System:应用类加载器,父类是Extension,是应用最广泛的类加载器。从classpath或者系统属性java.class.path所指定的目录中加载类,是用户自定义加载器的默认父加载器。

若使用自定义的类加载器(java.lang.ClassLoader的子类),则在字节码的方法表存储classLoader的引用,jvm在动态链接的时候,用该加载器加载引用类。为了正确动态链接和维护多个命名空间,jvm需要知道方法表里存贮的类加载器。

java.lang.ClassLoader内部维护着一个线程安全的HashTable<String,Class>,用于实现对Class字节流解码后的缓存,如果HashTable中已经有了缓存,则直接返回缓存。
当class已经被Application类加载器加载过了,然后如果想要使用Extension类加载器加载这个类,将会抛出java.lang.ClassNotFoundException异常。

注意:父加载器不能查找子加载器里的类。

类加载器可以装载一个类,却不可以卸载它,可以删除当前的类加载器,然后创建一个新的类加载器。


当一个类加载器被请求加载类时,在缓存里查看这个类是否已经被自己装载过了,如果没有的话,继续查找父类的缓存,直到在bootstrap类装载器里也没有找到的话,它就会自己在文件系统里去查找并且加载这个类。


类的预加载与首次主动使用
类加载器并不需要等到某个类被“首次主动使用”时再加载它。JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误) 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误 。
类的加载会导致父类的类和接口也会被加载进来。
连接阶段的符号引用的解析:

符号引用:
符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样,对于其他类的符号引用必须给出类的全名。对于其他类的字段,必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。

直接引用

直接引用解析后,放到运行时常量池里。

1、对于类的Class对象、类变量、类方法的直接引用可能是指向方法区的本地指针。

2、对于实例变量、实例方法的直接引用都是偏移量。

实例变量的直接引用可能是从对象的映像开始算起到这个实例变量位置的偏移量。

实例方法的直接引用可能是方法表的偏移量。

子类中方法表的偏移量和父类中的方法表的偏移量是一致的,比如说父类中有一个say()方法的偏移量是7,那么子类中say方法的偏移量也是7。
通过“接口引用”来调用一个方法,jvm必须搜索对象的类的方法表才能找到一个合适的方法。这是因为实现同一个接口的这些类中,不一定所有的接口中的方法在类方法区中的偏移量都是一样的。他们有可能会不一样。这样的话可能就要搜索方法表才能确认要调用的方法在哪里。
而通过“类引用”来调用一个方法的时候,直接通过偏移量就可以找到要调用的方法的位置了。【因为子类中的方法的偏移量跟父类中的偏移量是一致的】
所以,通过接口引用调用方法会比类引用慢一些。

初始化:

如果碰到在本类中声明本类的静态对象,且实例化,<cinit>()嵌套<init>()方法,则实例初始化可能在类初始化之前。

到了初始化阶段,才真正开始执行类中定义的Java程序代码。

1、static final int VAL = 100,编译时确定的常量:基本数据类型的常量、String,不包括任何new对象和需要在运行时才能确定的值。编译阶段会为VAL生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将VAL赋值为100。

基本数据类型(不含包装类)的常量拷贝,不进入常量池,编译器把他们当作值(value)而不是域(field)来对待。直接把这个值插入到字节码中。这是一种很有用的优化,如果是byte、short、int 数据,还会根据实际精度选择不同类型的字节码命令,如bipush、sipush、iconst,和定义的类型没关系。long类型是ldc命令。String的ldc #常量池编号。

2、类初始化:执行<clinit>()方法,是由javac自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,如:

 static int i=1; static{i=0;}  //i最终是0

3、接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。

     接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。 

4、虚拟机规范严格规定了有且只有5中情况(jdk1.7)如果类没有进行过初始化,必须对类进行“初始化”:

 1.  new:创建对象(通过数组定义来引用类,不触发初始化)。

    getstatic、putstatic读取/设置静态非final变量,如:static int a = 1,准备阶段赋初始值0,初始化阶段赋定义值1,谁定义初始化谁,和调用者无关。

    invokestatic:执行静态方法。

 2.使用java.lang.reflect包的方法对类进行反射调用。

 3.子类初始化,触发父类的初始化,虚拟机会保证父类的<clinit>优先执行,则父类中定义的静态语句块要优先于子类的变量赋值操作。

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

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

5、以下情况不会触发初始化:

定义对象数组,不会触发该类的初始化

通过类名获取Class对象,不会触发类的初始化。

通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化

通过ClassLoader默认的loadClass方法,也不会触发初始化动作,new ClassLoader(){}.loadClass("xxx.Cat");

对象创建过程:

1在堆内存中开辟一块空间,并给空间分配一个地址

2把对象的所有非静态成员加载到所开辟的空间下,并进行默认初始化,然后调用构造函数。

在构造函数入栈执行时,分为两部分:先执行构造函数中的隐式三步,再执行构造函数中书写的代码

  6.1、隐式三步:

      1,执行super语句

      2,对开辟空间下的所有非静态成员变量进行显式初始化

      3,执行构造代码块

  6.2、在隐式三步执行完之后,执行构造函数中书写的代码

7在整个构造函数执行完并弹栈后,把空间分配的地址赋值给一个引用对象