Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。
一个类型从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备、解析三个部分统称为连接。
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按步就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性。
当一个JVM在我们通过执行Java命令启动之后,其中可能包含的类非常多,是不是每个类都会被初始化呢?答案是否定的。JVM对类的初始化是一个延迟机制,即使用的是lazy的方式,当一个类在首次使用的时候才会被初始化,在同一个运行时包下,一个Class只会被初始化一次。
关于在什么情况下需要开始类加载过程的第一阶段"加载",《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有6种情况下必须立即对类进行"初始化"(而加载、验证、准备自然需要在此之前开始):
public class Simple {
static {
System.out.println("I will be init");
}
public static int x = 10;
}
这段代码中x是一个静态变量,即使别的类中不new Simple(),直接访问变量x也会导致类的初始化
public class Simple {
static {
System.out.println("I will be init");
}
/**
* 静态方法
*/
public static void test() {
}
}
其他类在调用test的方法时,也会触发Simple类的初始化
public class Parent {
static {
System.out.println("I will be init");
}
public static int x = 10;
}
public class Child extends Parent{
static {
System.out.println("I will be init");
}
public static int y = 10;
}
public class LoadTest {
public static void main(String[] args) {
System.out.println(Child.y);
}
}
注意:通过子类使用父类的静态变量只会导致父类被初始化,子类不会被初始化
public class LoadTest {
public static void main(String[] args) {
System.out.println(Child.x);
}
}
Class.forName()
当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义了常量)才会初始化。
不会导致类的加载和初始化
public class Loader {
public static void main(String[] args) {
Simple[] simples = new Simple[10];
System.out.println(simples.length);
}
}
上边的代码new方法新建了一个Simple类型的数组,但是它并不能导致Simple类的初始化。事实上这个操作只是在堆内存中开辟了一段连续的地址空间4byte*10
ClassLoder的主要职责是负责加载各种class文件到JVM中,ClasLoder是一个抽象的class,给定一个class的二进制文件名,ClassLoder会尝试加载并在JVM中生成这个类的各个数据结构,然后使其分布在JVM对应的内存区域。
类的加载过程分为三个阶段,分别是"加载阶段"、"连接阶段"、"初始化阶段"
主要负责查找并且加载类的二进制数据文件(class文件)。简单来说,类的加载就是查找并加载将class文件中的二进制数据读取到内存之中,然后将该字节流所代表的的静态存储结构转换为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.Class对象,作为访问方法区数据结构的入口。
加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区中了,方法区中的数据存储格式完全由虚拟机实现自定义,《Java虚拟机规范》未规定此区域的具体数据结构。类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的类型数据的外部接口。
主要是确保类文件的正确性,比如class的版本,class文件的魔术因子是否正确
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机的自身安全。
验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从代码量和耗费的执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重。
该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这个阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证之后,这段字节流才会被允许进入Java虚拟机内存的方法去中进行存储,所以后边的这三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。
Class文件并不一定只能由Java源码编译而来,它可以使用包括0和1直接在二进制编辑器中敲出Class文件在内的任何途径产生。所以验证字节码是Java虚拟机保护自身的一项必要措施。
符号引用验证的主要目的是确保解析行为能正常执行
验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过验证,其后就对程序运行期没有任何影响了。
正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置初始值的阶段
当一个class的字节流通过了所有的验证过程之后,就开始为该对象的类变量,也就是静态变量,分配内存并且设置初始值了,类变量的内存会被分配到方法区中。
public class Loader {
private static int a = 10;
private final static int b = 20;
}
其中static int a = 10在准备阶段不是10,而是初始值0,然而final static int b则会是20,为什么呢?因为final修饰的静态变量(可直接计算得出结果)不会导致类的初始化,是一种被动引用,因此就不存在连接阶段了。
当然了更为严谨的解释是final static int b = 20在类的编一阶段javac会将其value生成一个ConstantValue属性,直接赋予20.
首先是这时进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
如果类字段的字段属性表中存在ConstantValue属性,那么准备阶段变量值就会被初始化为实际代码的赋值。编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为开发人员在代码的赋值。
Java虚拟机将常量池内的符号引用替换为直接引用的过程
直到初始化阶段,Java虚拟机才真正开始执行类中的编写Java程序代码,将主导权移交给了应用程序。进行准备阶段时,变量已经赋值过一次系统要求的初始零值,而在初始阶段,则会根据程序员通过程序编码指定的主观计划去初始化类变量和其他资源。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。