创新互联公司技术团队10多年来致力于为客户提供成都网站建设、成都网站制作、品牌网站建设、全网营销推广、搜索引擎SEO优化等服务。经过多年发展,公司拥有经验丰富的技术团队,先后服务、推广了近千家网站,包括各类中小企业、企事单位、高校等机构单位。
Class文件是各种编译器编译生成的二进制文件,在Class文件中描述了各种与该类相关的信息,但是Class文件本身是一个静态的东西,想要使用某个类的话,需要java虚拟机将该类对应的Class文件加载进虚拟机中之后才能进行运行和使用。
举个例子,Class文件就好比是各个玩具设计商提供的设计方案,这些方案本身是不能直接给小朋友玩的,需要玩具生产商根据方案的相关信息制造出具体的玩具才可以给小朋友玩。那么不同的设计商有他们自己的设计思路,只要最终设计出来的方案符合生产商生产的要求即可。生产商在生产玩具时,首先会根据自己的生产标准对设计商提交来的方案进行阅读,审核,校验等一系列步骤,如果该方案符合生产标准,则会根据方案创建出对应的模具,当经销商需要某个玩具时,生产商则拿出对应的模具生产出具体的玩具,然后把玩具提交给经销商。
对于java而言,虚拟机就是玩具生产商,设计商提交过来的方案就是一个个的Class文件,方案创建的模具就
总的来说,类的加载过程,包括卸载在内的整个生命周期共有以下7个阶段:
加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,但是解析阶段不一定,在某些情况下解析可以在初始化之后再执行,为了支持java的运行时绑定,也成为动态绑定或晚期绑定。invokedynamic指令就是用于动态语言支持,这里“动态”的含义是必须等到城市实际运行到这条指令的时候,解析动作才开始执行。
“加载”是“类加载”过程中的一个阶段,在加载阶段,虚拟机需要做以下3件事情:
通过类的全限定名获得该类的二进制字节流
将这个字节流所代表的静态存储结构转换成方法区中的某个运行时数据结构
加载阶段中“通过类的全限定名获得该类的二进制字节流”这个动作,被放到java虚拟机外部实现,目的是最大限度的让应用程序去决定该如何获取所需的类,而实现该动作的代码模块就是类加载器(ClassLoader),JVM提供了3种类加载器:
启动类加载器(Bootstrap ClassLoader):负责加载 JAVAHOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
扩展类加载器(Extension ClassLoader):负责加载 JAVAHOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中了。
加载完成后,紧接着(更确切的说是交叉执行)虚拟机会对加载的字节流进行验证。虚拟机如果不检查输入的字节流,对其安全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃。验证阶段大致会完成4中不同的检验动作:
文件格式验证
文件格式验证主要是校验该字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机所接受。这个阶段包括但不限于以下验证点:
元数据验证
元数据验证主要是对字节流中的描述信息(描述符)进行语义分析,以确保其描述的信息符合java语言规范的要求。这个阶段包括但不限于以下验证点:
字节码验证
字节码验证主要是对类的方法体进行分析,确保在方法运行时不会有危害虚拟机的事件发生。这个阶段包括但不限于以下验证点:
符号引用验证
符号引用验证主要是对类自身以外的信息进行匹配新校验,包括常量池中的各种符号引用。这个阶段包括但不限于以下验证点:
准备阶段是为类变量(static)在方法区中分配内存并设置初始值(默认值,如int的默认值为0)的阶段。实例变量将会在对象实例化时随着对象一起分配在java堆中。为类变量设置初始值跟该变量是否有final修饰符有关系。 如果没用final进行修饰,如下列的代码:
// 准备阶段执行完成后,value变量的值为int的“零值”,即:0
// 把value赋值为10的putstatic指令是程序被编译后,
// 存放于类构造器()方法中的,所以具体赋值的操作会在初始化阶段执行
public static int value = 10;
如果使用了final进行修饰,如下列的代码:
// 如果类字段的字段属性表中有ConstantValue属性,
// 则会在准备阶段将变量的值初始化为ConstantValue所指定的值
// 准备阶段执行完成后,VALUE变量的值被赋值为20
public final static int VALUE = 20;
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程,在该阶段将会进行符号引用的校验。 符号引用是Class文件中用来描述所引用目标的符号,可以是任何形式的字面量。 直接引用是虚拟机在内存中引用具体类或接口的,可以是直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄。 简单的来说,符号引用是Class类文件用来定位目标的,直接引用是虚拟机用来在内存中定位目标的。
初始化阶段是执行类的构造器方法
java虚拟机规范严格规定了有且只有一下5中情况必须立即对类进行初始化:
以下几种情况,不会触发类初始化:
class Parent {
static int a = 100;
static {
System.out.println("parent init!");
}
}
class Child extends Parent {
static {
System.out.println("child init!");
}
}
public class Init{
public static void main(String[] args){
// 只会执行父类的初始化,不会执行子类的初始化
// 将打印:parent init!
System.out.println(Child.a);
}
}
public class Init{
public static void main(String[] args){
// 不会有任何输出
Parent[] parents = new Parent[10];
}
}
class Const {
static final int A = 100;
static {
System.out.println("Const init");
}
}
public class Init{
public static void main(String[] args){
// Const.A会存入Init类的常量池中,调用时并不会触发Const类的初始化
// 将打印:100
System.out.println(Const.A);
}
}
class Cat {
private string name;
static {
System.out.println("Cat is loaded");
}
}
public class Init{
public static void main(String[] args){
// 不会打印任何信息
Class catClazz = Class.class;
}
}
class Cat {
private string name;
static {
System.out.println("Cat is loaded");
}
}
public class Init{
public static void main(String[] args) throws ClassNotFoundException{
// 不会打印任何信息
Class catClazz = Class.forName("com.test.Cat",false,Cat.class.getClassLoader());
}
}
class Cat {
private string name;
static {
System.out.println("Cat is loaded");
}
}
public class Init{
public static void main(String[] args) throws ClassNotFoundException{
// 不会打印任何信息
new ClassLoader(){}.loadClass("com.test.Cat");
}
}
最后,附上一幅Class类加载过程的思维导图:
是Class文件加载进虚拟机中的类,生产的玩具就是类的实例对象。
因此,从Class文件到对象需要经过的步骤大致为: Class文件-->类-->实例对象 而类的加载机制,就是负责将Class文件转换成虚拟机中的类的一个过程。
总的来说,类的加载过程,包括卸载在内的整个生命周期共有以下7个阶段:
加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,但是解析阶段不一定,在某些情况下解析可以在初始化之后再执行,为了支持java的运行时绑定,也成为动态绑定或晚期绑定。invokedynamic指令就是用于动态语言支持,这里“动态”的含义是必须等到城市实际运行到这条指令的时候,解析动作才开始执行。
“加载”是“类加载”过程中的一个阶段,在加载阶段,虚拟机需要做以下3件事情:
通过类的全限定名获得该类的二进制字节流
将这个字节流所代表的静态存储结构转换成方法区中的某个运行时数据结构
加载阶段中“通过类的全限定名获得该类的二进制字节流”这个动作,被放到java虚拟机外部实现,目的是最大限度的让应用程序去决定该如何获取所需的类,而实现该动作的代码模块就是类加载器(ClassLoader),JVM提供了3种类加载器:
启动类加载器(Bootstrap ClassLoader):负责加载 JAVAHOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
扩展类加载器(Extension ClassLoader):负责加载 JAVAHOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中了。
加载完成后,紧接着(更确切的说是交叉执行)虚拟机会对加载的字节流进行验证。虚拟机如果不检查输入的字节流,对其安全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃。验证阶段大致会完成4中不同的检验动作:
文件格式验证
文件格式验证主要是校验该字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机所接受。这个阶段包括但不限于以下验证点:
元数据验证
元数据验证主要是对字节流中的描述信息(描述符)进行语义分析,以确保其描述的信息符合java语言规范的要求。这个阶段包括但不限于以下验证点:
字节码验证
字节码验证主要是对类的方法体进行分析,确保在方法运行时不会有危害虚拟机的事件发生。这个阶段包括但不限于以下验证点:
符号引用验证
符号引用验证主要是对类自身以外的信息进行匹配新校验,包括常量池中的各种符号引用。这个阶段包括但不限于以下验证点:
准备阶段是为类变量(static)在方法区中分配内存并设置初始值(默认值,如int的默认值为0)的阶段。实例变量将会在对象实例化时随着对象一起分配在java堆中。为类变量设置初始值跟该变量是否有final修饰符有关系。 如果没用final进行修饰,如下列的代码:
// 准备阶段执行完成后,value变量的值为int的“零值”,即:0
// 把value赋值为10的putstatic指令是程序被编译后,
// 存放于类构造器()方法中的,所以具体赋值的操作会在初始化阶段执行
public static int value = 10;
如果使用了final进行修饰,如下列的代码:
// 如果类字段的字段属性表中有ConstantValue属性,
// 则会在准备阶段将变量的值初始化为ConstantValue所指定的值
// 准备阶段执行完成后,VALUE变量的值被赋值为20
public final static int VALUE = 20;
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程,在该阶段将会进行符号引用的校验。 符号引用是Class文件中用来描述所引用目标的符号,可以是任何形式的字面量。 直接引用是虚拟机在内存中引用具体类或接口的,可以是直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄。 简单的来说,符号引用是Class类文件用来定位目标的,直接引用是虚拟机用来在内存中定位目标的。
初始化阶段是执行类的构造器方法
java虚拟机规范严格规定了有且只有一下5中情况必须立即对类进行初始化:
以下几种情况,不会触发类初始化:
class Parent {
static int a = 100;
static {
System.out.println("parent init!");
}
}
class Child extends Parent {
static {
System.out.println("child init!");
}
}
public class Init{
public static void main(String[] args){
// 只会执行父类的初始化,不会执行子类的初始化
// 将打印:parent init!
System.out.println(Child.a);
}
}
public class Init{
public static void main(String[] args){
// 不会有任何输出
Parent[] parents = new Parent[10];
}
}
class Const {
static final int A = 100;
static {
System.out.println("Const init");
}
}
public class Init{
public static void main(String[] args){
// Const.A会存入Init类的常量池中,调用时并不会触发Const类的初始化
// 将打印:100
System.out.println(Const.A);
}
}
class Cat {
private string name;
static {
System.out.println("Cat is loaded");
}
}
public class Init{
public static void main(String[] args){
// 不会打印任何信息
Class catClazz = Class.class;
}
}
class Cat {
private string name;
static {
System.out.println("Cat is loaded");
}
}
public class Init{
public static void main(String[] args) throws ClassNotFoundException{
// 不会打印任何信息
Class catClazz = Class.forName("com.test.Cat",false,Cat.class.getClassLoader());
}
}
class Cat {
private string name;
static {
System.out.println("Cat is loaded");
}
}
public class Init{
public static void main(String[] args) throws ClassNotFoundException{
// 不会打印任何信息
new ClassLoader(){}.loadClass("com.test.Cat");
}
}
最后,附上一幅Class类加载过程的思维导图: