虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换、解析和初始化,最终可以变成被虚拟机使用的Java类型,这就是虚拟机的类加载机制。
在java中,类型的加载、连接和初始化都是在程序运行期间完成的,不同于c/c++,这种策略的缺点就是在类加载的时候会造成性能开销,但是它保证了java应用程序的灵活性。例如:用户可以通过自定义的类加载器,让一个应用程序可以在运行时从网络或者其它地方加载一个二进制流作为程序代码的一部分。例如JSP技术。
一、类的加载时机与过程
1. 类的加载时机
类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。
2. 何时开始类的初始化
什么情况下需要开始类加载过程的第一个阶段:"加载"。虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握,但是对于初始化阶段虚拟机规范是严格规定了如下几种情况,如果类未初始化会对类进行初始化。
- 创建类的实例
- 访问类的静态变量(除常量即被
final
表示的静态变量),因为常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译。 - 访问类的静态方法
- 反射(Class.forName("com.zcl.main"))
- 当初始化一个类时,发现其父类还未初始化,则先对父类进行初始化
- 虚拟机启动时,先对
main()
方法的那个类进行初始化
以上情况称为称对一个类进行“主动引用”,除此种情况之外,均不会触发类的初始化,称为“被动引用”
接口的加载过程与类的加载过程稍有不同。接口中不能使用static{}块。当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有真正在使用到父接口时(例如引用接口中定义的常量)才会初始化。
被动引用的例子
- 子类调用父类的静态变量,子类不会被初始化。只有父类被初始化。。对于静态字段,只有直接定义这个字段的类才会被初始化.
- 通过数组定义来引用类,不会触发类的初始化
- 访问类的常量,不会初始化类
class SuperClass {
static {
System.out.println("superclass init");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("subclass init");
}
}
public class Test {
public static void main(String[] args) {
System.out.println(SubClass.value);// 被动应用1
SubClass[] sca = new SubClass[10];// 被动引用2
}
}
程序输出:证明了被动引用1 2
superclass init
123
class ConstClass {
static {
System.out.println("ConstClass init");
}
public static final String HELLOWORLD = "hello world";
}
public class Test {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);// 调用类常量
}
}
程序输出:证明了被动引用3
hello world
2. 类的加载过程
2.1验证
验证class文件的内容是否符合jvm虚拟机的标准要求,包括class文件的格式验证和语义验证等
2.2 准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
public static int value=123;//在准备阶段value初始值为0 。在初始化阶段才会变为123 。
2.3 解析
该阶段主要完成符号引用到直接引用的转换动作,例如:根据类的全限定名找到该类具体的地址。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。
2.4 初始化
初始化顺序:
我们大家都知道,对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序以此是(静态变量、静态初始化块)>(变量、初始化块)>构造器。
类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
初始化阶段是执行类构造器
题目分析
class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0;
private SingleTon() {
count1++;
count2++;
}
public static SingleTon getInstance() {
return singleTon;
}
}
public class Test {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}
1:SingleTon singleTon = SingleTon.getInstance();调用了类的SingleTon调用了类的静态方法,触发类的初始化
2:类加载的时候在准备过程中为类的静态变量分配内存并初始化默认值 singleton=null count1=0,count2=0
3:类初始化,为类的静态变量赋值和执行静态代码块。singleton赋值为new SingleTon()调用类的构造方法
4:调用类的构造方法后count=1;count2=1
5:继续为count1与count2赋值,此时count1没有赋值操作,所有count1为1,但是count2执行赋值操作就变为0
若将代码修改为下:
class Singleton {
private static Singleton singleTon = new Singleton();
public static int count1 = 1;
public int count2 = 2 ;
private Singleton() {
System.out.println(count1 + " - "+count2);
count1++;
count2++;
}
public static Singleton getInstance() {
return singleTon;
}
}
public class Test {
public static void main(String[] args) {
Singleton singleTon = Singleton.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}
则有结果为:
0 - 2
count1=1
count2=3
分析:因为普通常量只有在对象被实例化后才分配内存空间进行赋值,所有当进行new Sigleton()
时,count2已经被赋值为2,而静态变量在类加载的准备阶段已经被初始化为0,这时还没有被进行赋值操作,所以最终结果为赋值后的结果1
本文摘自https://blog.csdn.net/m0_38075425/article/details/81627349
https://www.cnblogs.com/shinubi/articles/6116993.html