【JVM总结】类的加载、连接和初始化

什么是类的加载

jvm将class文读取到内存中,经过对class文件的校验、转换解析、初始化最终在jvm的heap和方法区分配内存形成可以被jvm直接使用的类型的过程。

类的生命周期7个阶段依次为:Loading Verification Preparation Resolution Initialization Using Unloading

 

加载 验证 准备 初始化和卸载 的顺序是确定的,而“解析”不一定在初始化之前很有可能在初始化之后,实现java的伟大特性。

1、加载Loading

这个阶段jvm完成以下动作:

首先  类加载器通过类的全路径限定名读取类的二进制字节流,

然后  将二进制字节流代表的类结构转化到运行时数据区的 方法区中,

最后  在jvm堆中生成代表这个类的java.lang.Class实例(不是这个类的实例)

类加载器:

获取类的二进制流 既可以使用jvm自带的类加载器,也可以自己写加载器来加载,这一小步是完全可控的。不同的加载器可以从各种地方读取:

扫描二维码关注公众号,回复: 2832768 查看本文章

1.从本地文件系统加载class文件;

2.从Jar包加载class文件;

3.通过网络加载class文件;

4.把一个class源文件动态编译,并执行加载。

同一个加载器加载的同源类才是真的同类。不同加载器加载同源类,不是同类!instanceof为FALSE类加载的双亲委派模型各个加载器都是先委托自己的父加载器加载类,若确实没加载到再自己来加载于是java默认的类查找加载顺序是自顶向下的,树状结构双亲委托的意图是保证java类型体系中最基础的行为一致,优先加载JDK中的类。

 

加载器主要有四种:

jvm启动类加载器bootstrap loader,用c++实现为jvm的一部分(仅指sun的hotspot),负责 JAVA_HOME/lib下面的类库中的类的加载,这个加载器,java程序无法引用到。

扩展类加载器Extension Loader,由sun.misc.Launcher$ExtClassLoader类实现,可在java中使用,负责JAVA_HOME/lib/ext 目录和java.ext.dir目录中类库的类的加载。

应用系统类加载器Application System Loader,由sun.misc.Louncher$AppClassLoader实现,负责加载用户类路径中类库中的类,如果没有使用自定义的加载器,这个就是默认的 加载器!

用户自定义加载器 自己定义从哪里加载类的二进制流

2、类的连接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的。

2.1验证verification

Loading和 验证是交叉进行的,验证二进制字节流代表的字节码文件是否合格,主要从一下几方面判断:

文件格式:参看class文件格式详解,经过文件格式验证之后的字节流才能进入方法区分配内存来存储。

元数据验证:是否符合java语言规范

字节码验证:数据流和控制流的分析,这一步最复杂

符号引用验证:符号引用转化为直接引用时(解析阶段),检测对类自身以外的信息进行存在性、可访问性验证

如果确认代码安全无误,可用 -Xverify:none关闭大部分类的验证,加快类加载时间

2.2准备preparation

在方法区中给类的类变量(static修饰)分配内存然后初始化其值,如果类变量是常量,则直接赋值为该常量值否则为java类型的默认的零值。

2.3解析resolution

指将常量池内的符号引用替换为直接引用的过程。

3、类的初始化

这个阶段才真正开始执行java代码,静态代码块和设置变量的初始值为程序员设定的值

JVM首先加载class文件,静态代码段和class文件一同被装载并且只加载一次

主动引用

有且只有下面5种情况才会立即初始化类,称为主动引用:

1new 对象时

2、读取或设置类的静态字段(除了被final,已在编译期把结果放入常量池的静态字段)或调用类的静态方法时;

3用java.lang.reflect包的方法对类进行反射调用没初始化过的类时Class.forname()会进行初始化

       而.classJVM将使用类装载器, 将类装入内存(前提是:类还没有装入内存),不做类的初始化工作.返回Class的对象

4初始化一个类时发现其父类没初始化,则要先初始化其父类

5含main方法的那个类,jvm启动时,需要指定一个执行主类,jvm先初始化这个类

类的被动引用(不会发生类的初始化):

--当访问一个静态变量时,只有真正实现这个静态变量的类才会被初始化(通过子类引用父类的静态变量,不会导致子类初始化)

--通过数组定义类应用,不会触发此类的初始化  A[] a = new A[10];

--引用常量(final类型)不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)

子类继承父类时的初始化顺序

   1.首先初始化父类的static变量和static块,按出现顺序

   2.初始化子类的static变量和static块,按出现顺序

   3.初始化父类的普通变量和构造块,按出现顺序然后调用父类的构造函数

   4.初始化子类的普通变量和构造块,按出现顺序然后调用子类的构造函数

注意:

(1)1.2两步在类加载(只加载一次,除非卸载)的时候执行,并且只执行一次。初始化之前已经分配完内存了。

(2)接口不能使用static{}语句块,但编译器仍然会为接口生成“<clinit>()”类构造器,用于初始化接口中所定义的成员变量。接口和类真正区别是:当一个类在初始化的时候,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对静态Field进行初始化,在Java类中对静态Fieldr指定初始值有两种方式:1. 声明静态Field时指定初始值;2. 使用静态初始化块为静态Field指定初始值。如:

public class Test {

	static int a = 5;
	static int b;
	static int c;
	static
	{
	   b = 6;
	}
	
	public static void main(String[] args) {
		System.out.println("a="+a);
		System.out.println("b="+b);
		System.out.println("c="+c);
	}


}

// 最终结果 a=5,b=6,c=0

静态初始化块被当成类的初始化语句,JVM会按这些语句在程序中的排列顺序依次执行它们,如:

public class Test {
	static int a = 5;
	
	static
	{
	   b = 6;
	}
	
	static int b=9;
	static int c;
	
	public static void main(String[] args) {
		System.out.println("a="+a);
		System.out.println("b="+b);
		System.out.println("c="+c);
	}

}

// 最终结果 a=5,b=9,c=0

JVM初始化一个类包含如下几个步骤:

1、假如这个类还没有被加载和连接,则程序先加载并连接该类;

2、假如该类的直接父类还没有被初始化,则先初始化其直接父类(直接父类也依次执行1,2,3 保证类依赖的所有父类都会被初始化);

3、假如类中有初始化语句,则系统依次执行这些初始化语句。

类初始化的时机:

1、创建类的实例,包括使用new操作符来创建,通过反射来创建,通过反序列化的方式来创建;

2、调用某个类的静态方法;

3、访问某个类或者接口的静态Field,或者为该静态Field赋值;

4、通过反射方式来强制创建某个类或接口对应的java.lang.Class对象,例如,Class.forName(“Person”),如果系统还未初始化Person类,则这行代码将会导致该Person类被初始化,并返回Person类对应的java.lang.Class对象;

5、初始化某个类的子类;

6、直接使用java.exe命令来运行某个主类。 

另外:对于一个Field型的静态Field,如果该Field的值在编译时就可以确定下来,那么这个Field相当于“宏变量“,Java编译器会在编译时直接把这个Field出现的地方替换成它的值,因为即使程序使用该静态Field,也不会导致该类的初始化。

类、变量初始化的顺序

一般的,我们很清楚类需要在被实例化之前初始化,而对象的初始化则是运行构造方法中的代码。

public class T  implements Cloneable{
	  public static int k = 0;
	  public static T t1 = new T("t1");
	  public static T t2 = new T("t2");
	  public static int i = print("i");
	  public static int n = 99;
	  
	  public int j = print("j");
	  
	  {
	      print("构造快");
	  }
	  
	  static {
	      print("静态块");
	      //m=2;
	      //System.out.println(m);//静态代码块不可以访问该静态代码块之后的静态变量,但可以赋值
	  }
	  //public static int m = 99;
	  
	  public T(String str) {
	      System.out.println((++k) + ":" + str + "    i=" + i + "  n=" + n);
	      ++n; ++ i;
	  }
	  
	  public static int print(String str){
	      System.out.println((++k) +":" + str + "   i=" + i + "   n=" + n);
	      ++n;
	      return ++ i;
	  }
	  
	  public static void main(String[] args){
	      T t = new T("init");
	  }
	}

运行结果如下:

1:j   i=0   n=0
2:构造快   i=1   n=1
3:t1    i=2  n=2
4:j   i=3   n=3
5:构造快   i=4   n=4
6:t2    i=5  n=5
7:i   i=6   n=6
8:静态块   i=7   n=99
9:j   i=8   n=100
10:构造快   i=9   n=101
11:init    i=10  n=102

代码组成:

成员变量 2~6 行的变量是 static 的,为类 T 的静态成员变量,需要在类加载的过程中被执行初始化;第 8 行的int j则为实例成员变量,只再类被实例化的过程中初始化。

代码段 9~11 行为实例化的代码段,在类被实例化的过程中执行;13~15 行为静态的代码段,在类被加载、初始化的过程中执行。

方法 方法public static int print(String str) 为静态方法,其实现中牵涉到 k,i,n 三个静态成员变量,实际上,这个方法是专门用来标记执行顺序的方法;T 的构造方法是个实例化方法,在 T 被实例化时调用。

main 方法 main 方法中实例化了一个 T 的实例。

执行顺序分析

在一个对象被使用之前,需要经历的过程有:类的装载->链接(验证 ->准备-> 解析)->初始化->对象实例化。这里需要注意的点主要有:

在类链接之后,类初始化之前,实际上类已经可以被实例化了

就如代码中所述,在众多静态成员变量被初始化完成之前,已经有两个实例的初始化了。实际上,此时对类的实例化,除了无法正常使用类的静态承运变量以外(还没有保证完全被初始化),JVM 中已经加载了类的内存结构布局,只是没有执行初始化的过程。比如第 3 行public static T t1 = new T("t1");,在链接过程中,JVM 中已经存在了一个 t1,它的值为 null,还没有执行new T("t1")。又比如第 5 行的public static int i = print("i");,在没有执行初始化时,i 的值为 0.

先执行成员变量自身初始化,后执行static {…}、{…}代码块中的内容。

如此策略的意义在于让代码块能处理成员变量相关的逻辑。如果不使用这种策略,而是相反先执行代码块,那么在执行代码块的过程中,成员变量并没有意义,代码块的执行也是多余。

类实例化的过程中,先执行隐式的构造代码,再执行构造方法中的代码 这里隐式的构造代码包括了{}代码块中的代码,以及实例成员变量声明中的初始化代码,以及父类的对应的代码(还好本题中没有考察到父类这一继承关系,否则更复杂;))。为何不是先执行显示的构造方法中的代码,再执行隐式的代码呢?这也很容易解释:构造方法中可能就需要使用到实例成员变量,而这时候,我们是期待实例变量能正常使用的。

 

猜你喜欢

转载自blog.csdn.net/fxkcsdn/article/details/81487032