从一个实例看 Java 类加载机制

一个实例

今天在在研究这样一份代码:

public class debug{

	// b 的定义
	Thread b;
	public debug(){
		Thread a=new Thread(new Runnable(){
			int count=0;
			public void run() {
				while(true) {
					count++;
					System.out.println("a进程");
					try {
						Thread.sleep(2000);
						System.out.println("222");

						// b的使用,调用其join方法,使其他线程等待b执行完再执行
						b.join();
						System.out.println("333");
					}catch(Exception e) {
						e.printStackTrace();
					}
					if(count==5) {
						break;
					}
				}
			}
		});
		a.start();

		//b的初始化
		b=new MyThread();
		b.start();
		System.out.println("主进程");
	}
	public static void main(String[] args) {
		new debug();
	}
	
	class MyThread extends Thread{
		public MyThread() {
			System.out.println(111);
		}
	}
}

注意看我上面的三行注释,从顺序上来看,貌似这份代码线程 b 的生命周期是定义后,先使用,再初始化的,按道理是会报错,但是上面的代码并没有问题,其输出如下:

111
主进程
a进程
222
333
a进程
222
333
a进程
222
333
a进程
222
333
a进程
222
333

注意看打印语句的111222行,111行是来标注线程 b 的初始化时间的,而222行是标注 b 的使用时间的,这样看,似乎代码执行不按顺序来吗,我深入研究一番后,发现是 Java 类的加载机制在***从中作梗***

Java 类的加载机制

Java类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。其中准备、验证、解析3个部分统称为连接(Linking)

在这里我们仅仅关注准备、解析、初始化阶段阶段即可

准备(Preparation)

准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

public static int value = 123;

那么,变量value在准备阶段过后的值为0而不是123。因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器方法()之中,所以把value赋值为123的动作将在初始化阶段才会执行。至于“特殊情况”是指:当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0。

public static final int value = 123;

解析(Resolution)

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化(Initialization)

类初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。

在准备阶段,变量已经赋过一次系统要求的初始值(零值);而在初始化阶段,则根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,或者更直接地说:<font size = 6,color = “red”>初始化阶段是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下:

public class Test{
	static{
    	i=0;
    	System.out.println(i);//Error:Cannot reference a field before it is defined(非法向前引用)
	}
	static int i=1;
}

那么注释报错的那行代码,改成下面情形,程序就可以编译通过并可以正常运行了。

public class Test{
	static{
    	i=0;
    	//System.out.println(i);
	}

	static int i=1;

	public static void main(String args[]){
    	System.out.println(i);
	}
}/* Output:	1

类构造器()与实例构造器()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器()执行之前,父类的类构造()执行完毕。由于父类的构造器()先执行,也就意味着父类中定义的静态语句块/静态变量的初始化要优先于子类的静态语句块/静态变量的初始化执行。特别地,类构造器()对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器()。

Java 对象创建过程

看上面的大红字很激动, 以为得到了解决,但上面说的是类的静态变量,而程序中是实例变量。其实,上面程序的原理依据是:

<font size = 6,color = “red”>实例变量初始化与实例代码块初始化总是发生在构造函数初始化之前

示例代码:

public class debug{
	// b 的定义
	Thread b;

	public debug(){
		
		// 构造函数  fourth
		Thread a=new Thread(new Runnable(){
			int count=0;
			public void run() {
				while(true) {
					count++;
					System.out.println("a进程");
					try {
						Thread.sleep(2000);
						System.out.println("222");
						// b的使用,调用其join方法,使其他线程等待b执行完再执行
						b.join();
						System.out.println("333");
					}catch(Exception e) {
						e.printStackTrace();
					}
					if(count==5) {
						break;
					}
				}
			}
		});
		a.start();
		//b的初始化           third
		b=new MyThread();
		b.start();
		System.out.println("主进程");
	}
	
	//实例代码块             second
	{
	System.out.println(1.5);
	}
	//静态代码块             first
	static {
		System.out.println(0.5);
	}
	
	public static void main(String[] args) {
		new debug();
	}
	
	class MyThread extends Thread{
		public MyThread() {
			System.out.println(111);
		}
	}
}

示例输出:

0.5
1.5
111
a进程
主进程
222
333
a进程
222
333
a进程
222
333
a进程
222
333
a进程
222
333

由此我得出

一个类中新建实例时代码的执行顺序:静态代码块 > 实例代码块 > 实例变量初始化(因为静态变量在类加载的时候被初始化了,然后这个初始化还有一个初始化语句的收集过程,与初始化语句的具体位置没有太大关系(除了在静态/实例代码块和构造函数)所以造成上面的假象)>构造函数。

发布了84 篇原创文章 · 获赞 250 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/ygdxt/article/details/88143030