深入理解Java虚拟机之Java类加载机制,Java类实例化过程详解。

目录

Java类加载机制

类加载过程

加载(Loading)

连接(Linking)

初始化(Initialzation)

使用(Useing)

卸载(Unloading)


引言

  1. 什么情况下开始类加载过程的第一个阶段:加载?
    1. 答:Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现去自由把握。但是对于初始化阶段,虚拟机则是严格规定了有且只有5种情况必须立即对类进行“初始化”,而加载、验证、准备这几个过程自然需要在此之前进行。
  2. 类的实例化过程详细执行情况到底是怎样???
  3. 虚拟机何时、如何加载Class文件,Class文件信息进入虚拟机会发生什么变化?
  4. 什么是符号引用与直接引用???
  5. 什么是初始化??
  6. 接口与类在初始化有什么区别??
  7. 类构造器<clinit>()方法与实例构造器<init>()方法的区别??

本文重点

  • 类的实例过程详解

  • 类的初始化条件

欲知详情,还请细看本文


  • 类的生命周期

一 加载(Loading)

  1. 加载的内容

    1. 通过一个类的全限定名来获取定义此类的二进制字节流,具体由哪些呢?

      1. 从zip包中读取,最终成为JAR,EAR,WAR格式的基础

      2. 从网络中获取,典型应用Applet

      3. 运行时计算生成,动态代理技术

      4. 由其他文件生成,典型场景Jsp应用,由jsp文件生成对应的class类

      5. 从数据库中读取,场景较少,如:有些中间件服务器(SAP Netweaver)可以选择把程序安装到数据库中完成程序代码在集群建分发

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

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


二 连接(Linking)

  1. 验证(Verification)

    1. 文件格式验证的内容有哪些?
      1. 是否以魔数CAFFBABE开头
      2. 主次版本号是够处于虚拟机处理范围之内
      3. 常量池中常量是否有不被支持的常量类型
      4. 指向常量的各种索引值是否指向不存在的常量或不符合类型的常量
      5. CONSTANT_Utf8_info型常量是否有不符合UTF8编码的数据等等
    2. 元数据验证的内容有哪些?
      1. 该类是否有父类
      2. 该类的父类是否继承了不允许继承的类(final类)
      3. 如果该类不是抽象类,是否实现了父类中或抽象接口中要求实现的所有方法
      4. 类中的字段是否与父类发生矛盾,如:覆盖了父类的final字段 不符合规则的方法重载
    3. 字节码验证的内容有哪些?
      1. 保证跳转指令不会跳转到方法体以外的字节码指令上
      2. 保证方法体汇中的类型转换是否有效
      3. 保证任意时刻操作数栈中的数据类型与指令代码序列都能配合工作
    4. 符号引用验证
      1. 目的:确保解析动作能正常执行
      2. 内容有哪些?
        1. 符号引用中通过字符串描述的全限定名是否能找到相应的类
        2. 符号引用中的类、字段、方法的访问性是否可以被当前类访问
        3. 在指定类中存在符合方法的字段描述以及简单名称和字段
  2. 准备(Prepareing)

    1. 准备阶段是正式为类变量分配内存并设置类变量(static修饰的变量)初始值的阶段,初始值在通常情况下为数据类型的“零值”
    2. 零值
      1. 整形的零值为: 0
      2. 布尔n类型的零值为: false
      3. 引用类型的零值为: null
    3. 假设一个类变量的定义为:public static int value=1234;那变量value在准备阶段过后的值不是为1234,而是0.只有经历了初始化阶段之后value的值才会为1234。
  3. 解析(Resolution)

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

  1.  
    1. 预备知识
      1. 什么是符号引用与直接引用???
      2. 符号引用(编译原理方面的概念)包括下面三类常量
        1. 类和接口的全限定名
        2. 字段的名称和描述符
        3. 方法的名称和描述符
      3. 直接引用的定义
        1. 直接指向目标的指针
        2. 相对偏移量
        3. 间接定位到目标的句柄
    2. 解析的内容包括哪些?
      1. 类或接口
      2. 字段
      3. 类方法
      4. 接口方法
      5. 方法类型
      6. 方法句柄
      7. 调用点限定符

三 初始化(Initialzation)

  1. 预备知识
    1. 什么是初始化?
      1. 即执行类构造器<clinit>()方法的过程。类加载过程最后一步,前面的过程完全由虚拟机主导和控制(除了通过自定义加载器参与外),初始化阶段才真正开始执行类中定义的Java代码(或者说是字节码)。

    2. 执行类构造器<clinit>()方法的过程是什么?

      1. <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(Static{}块)中的的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在静态语句块可以赋值,但是不能访问。
      2. <clinit>()方法对于类和接口不是必需的,如果没有static修饰的方法、代码块、变量。编译器可以不为其生成<clinit>方法
    3. 接口与类在初始化有什么区别?
      1. 只有该真正使用父接口的时候才会初始化,如:引用父接口中定义的常量
    4. 类构造器<clinit>()方法与实例构造器<init>()方法的区别?
      1. 无需显式的调用父类构造器,虚拟机会保证在子类<clinit>()方法执行前,父类的<clinit>方法已经执行完。
      2. 因此意味着虚拟机中第一个执行<clinit>()方法的肯定是java.lang.Object,父类中定义的静态语句块先于子类的变量赋值操作
  2. 初始化的条件
    1. 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时。
    2.  使用java.lang.reflect包对类进行反射调用时,如果类没有进行初始化,则需要先触发其进行初始化
    3. 初始化一个类时,如果其父类还没有进行初始化,则需先触发其父类进行初始化
    4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的),虚拟机会先初始化这个主类、
    5. 如果java.lang.invoke.MethodHandle实例最后的解析结果是REF_getstatic的句柄、REF_putstatic的句柄、REF_invokestatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需先触发其初始化。
  1. 不会触发初始化的情况,被动引用,后文举了三个例子。
    1. 子类引用父类的静态字段,对于静态字段,只有直接定义这个字段的类才会初始化。
    2. 引用其他类的常量字段,不会触发此类初始化,编译阶段,常量传播优化。
    3. 通过数组定义来引用类,不会触发此类初始化

实例代码演示


演示一:类的实例创建过程:  

预备知识

  • 初始化的含义:

执行类构造器<clinit>()方法,即为类变量赋值+执行静态语句块 static{}的过程。

  • 为什么在实例化子类的对象时,会先调用父类的构造器?

答:子类继承父类后,获取到父类的属性和方法,这些属性和方法在使用前必须先初始化,所以须先调用父类的构造器进行初始化

  • 如何调用父类的构造器?

在子类构造器的第一行会隐式的调用 super();,即调用父类的构造器, 如果父类中没有定义空参的构造器,则必须在子类的构造器的第一行显示的调用super(参数); ,以调用父类中构造器, 如果子类中构造器的第一行写了this();,则就隐式的super();会消失,因为super()和this()都只能在构造器的第一行定义


class  C{
	C() {
        System.out.println("正执行SuperClass类的构造方法对成员变量初始化,为其成员变量C分配内存空间");
    }

}

class SuperClass {
    C c = new C();
	static{
	System.out.println("SuperClass类 正在初始化");
}
	
	SuperClass() {
        this("正在调用SuperClass的有参构造方法");
        System.out.println("正在执行SuperClass的无参构造方法");
    }
 
	SuperClass(String s) {
        System.out.println(s);
    }
}

public class SubClass extends SuperClass{
	static{
	System.out.println("SubClass类 正在初始化");
}
	SubClass() {
        /*在子类构造方法的第一句,隐式的调用父类的构造方法;*/
        System.out.println("正在执行子类SubClass的构造方法");
    }
	 public static void main(String[] args) {
	        new SubClass();
	    }
}

 

类的实例过程

  1. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的),虚拟机会先初始化这个主类。即SubClass类
  2. 初始化一个类时,如果其父类还没有进行初始化,则需先触发其父类进行初始化。即需先初始化SubClass类的父类SuperClass。
  3. 由于父类没有显式的继承一个类,虚拟机默认其继承所有类的祖先类即java.lang.Objec类。即需对Object类进行初始化。总而言之,虚拟机中第一个执行<clinit>()方法的肯定是java.lang.Object,父类中定义的静态语句块和类的变量赋值操作都要先于子类。
  4. 初始化SubClass类的父类SuperClass,输出“SuperClass类 正在初始化
  5. 初始化父类SuperClass的子类SubClasst,输出“SubClass类 正在初始化
  6. 执行SubClasst类的静态语句块时,遇到new关键字,调用子类的构造方法,子类构造器的第一行会隐式的调用 super();,即先调用父类的构造方法。
  7. 调用父类的构造方法,为类中的成员变量分配内存,执行构造方法中的语句。
    1. 即先输出“正执行SuperClass类的构造方法对成员变量初始化,为其成员变量C分配内存空间,成员变量赋值先于构造方法中的语句
    2. 再输出“正在执行SuperClass的无参构造方法中的语句
  8. 调用子类的构造方法,为类中的成员变量分配内存,执行构造方法中的语句。输出“正在执行子类SubClass的无参构造方法中的语句

  • 被动引用演示一 子类引用父类的静态字段,对于静态字段,只有直接定义这个字段的类才会初始化。

package negtive_reference;
/*
 * 被动使用字段演示一
      通过子类引用父类的静态字段,不会导致子类初始化
*/

public class SuperClass {
	static{
	System.out.println("SuperClass Init");
}
	public static int value=123;

}

package negtive_reference;

public class SubClass extends SuperClass{
	static{
		System.out.println("SubClass Init");
	}
}

package negtive_reference;

public class NotInitialization {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.println(SubClass.value);
	}

}

输出内容  :


  • 被动引用演示二 引用其他类的常量字段,不会触发此类初始化,编译阶段,常量传播优化。

package negtive_reference;
	/*常量在编译阶段会调入类的常量池,
	 *本质上并没有直接引用到定义常量的表,
	 *因此不会触发定义常量的类的初始化*/
public class ConstantClass {
	static{
	System.out.println("ConstantClass Init");
}
	public static final String HELLOWOELD="hello world";
}
package negtive_reference;

public class NotInitialization {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		/*System.out.println(SubClass.value);*/
		/*SuperClass[] sca= new SuperClass[10];*/
		System.out.println(ConstantClass.HELLOWOELD);
	}

}

 输出:无输出

 

  • 被动引用演示三 通过数组定义来引用类,不会触发此类初始化

package negtive_reference;
/*
 * 被动使用字段演示一
      通过数组来引用类,不会触发此类的初始化
*/

public class SuperClass {
	static{
	System.out.println("SuperClass Init");
}
	public static int value=123;

}



package negtive_reference;

public class NotInitialization {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		SuperClass[] sca= new SuperClass[10];
	}

}

输出:无输出

总结:有三个可以给变量赋值的阶段

  • 准备阶段,为类变量设置初始值,即赋予其对应数据类型的“零值”。
  • 初始化阶段,为类变量赋予用户在程序中声明的值。前文中将value赋值为1234
  • 类创建实例时,执行其构造方法init()时,如果成员变量声明时没有指定初值,所使用的构造方法也没有对成员变量进行初始化操作,会为成员变量赋予其相应数据类型的“零值”。

注意:前面两个阶段针对的是类变量,即static关键字修饰的变量,前两个阶段不会对实例变量设置初始值。

觉得本文不错的话对你有帮助的可以点个赞哦!



猜你喜欢

转载自blog.csdn.net/qq_38537709/article/details/88750605