目录
Java类加载机制
引言
- 什么情况下开始类加载过程的第一个阶段:加载?
- 答:Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现去自由把握。但是对于初始化阶段,虚拟机则是严格规定了有且只有5种情况必须立即对类进行“初始化”,而加载、验证、准备这几个过程自然需要在此之前进行。
- 类的实例化过程详细执行情况到底是怎样???
- 虚拟机何时、如何加载Class文件,Class文件信息进入虚拟机会发生什么变化?
- 什么是符号引用与直接引用???
- 什么是初始化??
- 接口与类在初始化有什么区别??
- 类构造器<clinit>()方法与实例构造器<init>()方法的区别??
本文重点
-
类的实例过程详解
-
类的初始化条件
欲知详情,还请细看本文
-
类的生命周期
一 加载(Loading)
-
加载的内容
-
通过一个类的全限定名来获取定义此类的二进制字节流,具体由哪些呢?
-
从zip包中读取,最终成为JAR,EAR,WAR格式的基础
-
从网络中获取,典型应用Applet
-
运行时计算生成,动态代理技术
-
由其他文件生成,典型场景Jsp应用,由jsp文件生成对应的class类
-
从数据库中读取,场景较少,如:有些中间件服务器(SAP Netweaver)可以选择把程序安装到数据库中完成程序代码在集群建分发
-
-
将这个字节流所代表的静态结构转化为方法区的运行时数据结构
-
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
-
二 连接(Linking)
-
验证(Verification)
- 文件格式验证的内容有哪些?
- 是否以魔数CAFFBABE开头
- 主次版本号是够处于虚拟机处理范围之内
- 常量池中常量是否有不被支持的常量类型
- 指向常量的各种索引值是否指向不存在的常量或不符合类型的常量
- CONSTANT_Utf8_info型常量是否有不符合UTF8编码的数据等等
- 元数据验证的内容有哪些?
- 该类是否有父类
- 该类的父类是否继承了不允许继承的类(final类)
- 如果该类不是抽象类,是否实现了父类中或抽象接口中要求实现的所有方法
- 类中的字段是否与父类发生矛盾,如:覆盖了父类的final字段 不符合规则的方法重载
- 字节码验证的内容有哪些?
- 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法体汇中的类型转换是否有效
- 保证任意时刻操作数栈中的数据类型与指令代码序列都能配合工作
- 符号引用验证
- 目的:确保解析动作能正常执行
- 内容有哪些?
- 符号引用中通过字符串描述的全限定名是否能找到相应的类
- 符号引用中的类、字段、方法的访问性是否可以被当前类访问
- 在指定类中存在符合方法的字段描述以及简单名称和字段
- 文件格式验证的内容有哪些?
-
准备(Prepareing)
- 准备阶段是正式为类变量分配内存并设置类变量(static修饰的变量)初始值的阶段,初始值在通常情况下为数据类型的“零值”
- 零值
- 整形的零值为: 0
- 布尔n类型的零值为: false
- 引用类型的零值为: null
- 假设一个类变量的定义为:public static int value=1234;那变量value在准备阶段过后的值不是为1234,而是0.只有经历了初始化阶段之后value的值才会为1234。
-
解析(Resolution)
解析过程是虚拟机将常量池内的符号引用替换为直接引用的过程。
-
- 预备知识
- 什么是符号引用与直接引用???
- 符号引用(编译原理方面的概念)包括下面三类常量
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 直接引用的定义
- 直接指向目标的指针
- 相对偏移量
- 间接定位到目标的句柄
- 解析的内容包括哪些?
- 类或接口
- 字段
- 类方法
- 接口方法
- 方法类型
- 方法句柄
- 调用点限定符
- 预备知识
三 初始化(Initialzation)
- 预备知识
- 什么是初始化?
-
即执行类构造器<clinit>()方法的过程。类加载过程最后一步,前面的过程完全由虚拟机主导和控制(除了通过自定义加载器参与外),初始化阶段才真正开始执行类中定义的Java代码(或者说是字节码)。
-
-
执行类构造器<clinit>()方法的过程是什么?
- <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(Static{}块)中的的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在静态语句块可以赋值,但是不能访问。
- <clinit>()方法对于类和接口不是必需的,如果没有static修饰的方法、代码块、变量。编译器可以不为其生成<clinit>方法
- 接口与类在初始化有什么区别?
- 只有该真正使用父接口的时候才会初始化,如:引用父接口中定义的常量
- 类构造器<clinit>()方法与实例构造器<init>()方法的区别?
- 无需显式的调用父类构造器,虚拟机会保证在子类<clinit>()方法执行前,父类的<clinit>方法已经执行完。
- 因此意味着虚拟机中第一个执行<clinit>()方法的肯定是java.lang.Object,父类中定义的静态语句块先于子类的变量赋值操作
- 什么是初始化?
- 初始化的条件
- 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时。
- 使用java.lang.reflect包对类进行反射调用时,如果类没有进行初始化,则需要先触发其进行初始化
- 初始化一个类时,如果其父类还没有进行初始化,则需先触发其父类进行初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的),虚拟机会先初始化这个主类、
- 如果java.lang.invoke.MethodHandle实例最后的解析结果是REF_getstatic的句柄、REF_putstatic的句柄、REF_invokestatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需先触发其初始化。
- 不会触发初始化的情况,被动引用,后文举了三个例子。
- 子类引用父类的静态字段,对于静态字段,只有直接定义这个字段的类才会初始化。
- 引用其他类的常量字段,不会触发此类初始化,编译阶段,常量传播优化。
- 通过数组定义来引用类,不会触发此类初始化
实例代码演示
演示一:类的实例创建过程:
预备知识
- 初始化的含义:
执行类构造器<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();
}
}
类的实例过程
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的),虚拟机会先初始化这个主类。即SubClass类
- 初始化一个类时,如果其父类还没有进行初始化,则需先触发其父类进行初始化。即需先初始化SubClass类的父类SuperClass。
- 由于父类没有显式的继承一个类,虚拟机默认其继承所有类的祖先类即java.lang.Objec类。即需对Object类进行初始化。总而言之,虚拟机中第一个执行<clinit>()方法的肯定是java.lang.Object,父类中定义的静态语句块和类的变量赋值操作都要先于子类。
- 初始化SubClass类的父类SuperClass,输出“SuperClass类 正在初始化”
- 初始化父类SuperClass的子类SubClasst,输出“SubClass类 正在初始化”
- 执行SubClasst类的静态语句块时,遇到new关键字,调用子类的构造方法,子类构造器的第一行会隐式的调用 super();,即先调用父类的构造方法。
- 调用父类的构造方法,为类中的成员变量分配内存,执行构造方法中的语句。
- 即先输出“正执行SuperClass类的构造方法对成员变量初始化,为其成员变量C分配内存空间,成员变量赋值先于构造方法中的语句”
- 再输出“正在执行SuperClass的无参构造方法中的语句”
- 调用子类的构造方法,为类中的成员变量分配内存,执行构造方法中的语句。输出“正在执行子类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关键字修饰的变量,前两个阶段不会对实例变量设置初始值。
觉得本文不错的话对你有帮助的可以点个赞哦!