Java类及其类成员的初始化(一)—类的初始化





1、类初始化的时机




首次通过下面6种方式使用某个类或者接口时,系统会初始化该类或接口。


  • 创建类的实例(创建实例方式包括:使用new操作符创建实例,通过反射创建实例,通过反序列化创建实例)。
  • 调用某个类的类方法。
  • 访问某个类或接口的类变量,或为该类变量赋值。
  • 使用反射强制创建某个类或接口的java.lang.Class对象。例如代码:Class.forName(Person),若系统还未初始化Person类,这行代码会导致Person类被初始化,并返回Person类对应的java.lang.Class对象
  • 初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化。
  • 直接使用java.exe命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。



除此之外,下面两种情形需要注意:





1. 访问一个final型的类变量





代码:



package calculate;

/**
 * 使用final型类变量和类的初始化的关系
 * @author wangzhh
 *
 */
public class CompileConstantTest 
{
	public static void main(String[] args) {
		System.out.println(MyTest.compileConstant);
		System.out.println(MyTest2.compileConstant);
	}
}

/**
 * 编译时类中final型类变量就确定了值的类
 * @author wangzhh
 *
 */
class MyTest
{
	//静态代码块
	static
	{
		System.out.println("MyTest静态代码块...");
	}
	//final型类变量
	static final String compileConstant = "final型类变量";
}

/**
 * 运行时类中final型类变量才确定值的类
 * @author wangzhh
 *
 */
class MyTest2
{
	//静态代码块
	static
	{
		System.out.println("MyTest2静态代码块...");
	}
	//final型类变量
	static final String compileConstant = System.currentTimeMillis()+"";
}







打印结果:



final型类变量


MyTest2静态代码块...
1486699850774



解析:



观察结果发现MyTest类的静态初始化块没有被执行,说明MyTest并没有被初始化。实际上MyTest和MyTest2的两个final类变量的值是在不同时期被确定的,前者类变量的值在编译时就可以确定下来,Java编译器在编译时候把类变量出现的地方替换成它的值,相当于宏变量,在程序使用该类变量相当于使用常量。而后者类变量的值必须等到运行时候才能确定,如果通过该类来访问它的类变量,则会导致该类被初始化。



结论:对于一个final类型的类变量,它的值如果能在编译时候确定下来,那么程序使用该类变量的时候不会导致该类的初始化,否则会初始化该类。




2. 利用ClassLoader类的loadClass()方法加载类



代码:


package calculate;

/**
 * 类初始化测试类
 * @author wangzhh
 *
 */
class Tester
{
	static
	{
		System.out.println("Tester类的静态初始化块...");
	}
}

/**
 * ClassLoader.loadClass()和Class.forName()与类初始化的关系 
 * @author wangzhh
 *
 */
public class ClassLoaderTest 
{
	public static void main(String[] args) throws ClassNotFoundException
	{
		ClassLoader c = ClassLoader.getSystemClassLoader();
		//仅仅是加载MyTest类,注意写全类名
		c.loadClass("calculate.Tester");
		System.out.println("系统加载Tester类");
		//初始化MyTest类,注意写全类名
		Class.forName("calculate.Tester");
	}
}






打印结果:



系统加载Tester类
Tester类的静态初始化块...




结论:ClassLoader.loadClass()方法只是加载该类,并不会执行该类的初始化,相当于类初始化过程中的第一步,类的加载。






2. 类初始化的过程



当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过 加载连接初始化三个步骤来对该类进行初始化。




1. 类的加载



类的加载:指的是将类的class文件(字节码文件)读入内存,并为之创建一个java.lang.Class对象。



类加载器负责加载所有的类,系统为所有载入内存中的类生成一个java.lang.Class实例。在Java中,一个类用其全限定雷鸣作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识,一旦一个类被载入JVM中,同一个类就不会被再次载入,这和类加载的类加载机制有关。



类加载机制:


  • 全盘负责。当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也将由该类加载器载入,除非显示使用另外的类加载器载入
  • 父类委托。先让父类加载器加载Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制。保证所有加载过的Class都会被缓存,当程序需要使用某个Class时,类加载器先从缓存区搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换为Class对象,存入缓存区中。(这就是为什么修改了Class后,必须重新启动JVM,修改才会生效的原因)。



另外使用不同的类加载器,可以从不同来源加载类的二进制数据


  • 从本地文件系统加载class文件(绝大多数)。
  • 从jar包加载class文件
  • 通过网络加载class文件
  • 把一个java源文件动态变异,并执行加载。

类加载器通常无需等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。




2. 类的连接





类的连接负责把类的二进制数据合并到JRE中,可分为3个阶段:





(1)验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致。



(2)准备:负责为类变量分配内存,并设置默认初始值。



(3)解析:将类的二进制数据中的符号引用替换成直接引用。







3. 类的初始化




在类的初始化阶段,主要就是对静态对象(静态变量,静态初始化块)进行初始化(静态对象的初始化只执行一次,就是首次初始化类的时候)。



JVM初始化一个类有如下步骤:



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


(2)假如该类的直接父类还没有被初始化,则先初始化其直接父类。


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