从类文件分析Java类装载过程<clinit>方法的调用和<init>的调用

本文分析了Java类的装载过程,从执行开始,详细阐述了类加载的各个阶段,包括静态成员变量的初始化、成员变量的存储以及<clinit>和<init>方法的调用顺序。在类初始化时,系统首先执行<clinit>方法,确保父类的静态初始化完成,然后执行成员变量初始化和构造方法。

从Java执行过程开始

由于在eclipse中编写Java代码很容易忽视Java的运行流程,eclipse会自动编译源文件,屏蔽掉了生成字节码的过程,所以很容易忽视对于类的加载和运行的过程。
当在父类的构造方法中调用了子类的方法时,子类中成员变量初始化出错,代码如下:
class Print{
	Print(String s){
		System.out.println("Step "+s);
	}
	Print(String s,double i){
		System.out.println("Step "+s+"  随机数 "+i);
	}
}

abstract class Glyph {
	abstract void draw();
	Print p2 = new Print("父类成员变量");
	static Print p3= new Print("父类静态成员变量");
	static Print p6= new Print("父类静态成员变量",Math.random());
	Glyph() {
		System.out.println("父类构造方法");
		System.out.println("Glyph() before draw()");
		draw(); 
		System.out.println("Glyph() after draw()");
	}
}

class RoundGlyph extends Glyph {
      int radius = 1;
      String s = "abcdefg";
	  static Print p4= new Print("子类静态成员变量");
	  static Print p5= new Print("子类静态成员变量",Math.random());
	  Print p1 = new Print("子类成员变量");
	  RoundGlyph(int r) {
		  System.out.println("子类构造方法");
		  radius = r;
		  System.out.println(
	      "RoundGlyph.RoundGlyph(), radius = "
	      + radius);
	  }
	  void draw() { 
		  System.out.println("子类draw方法, radius = " + radius+" 字符串 "+s);
	  }
}

public class PolyConstructors {
	public static void main(String[] args) {
		new RoundGlyph(5);
	}
}

运行结果如下:

Step 父类静态成员变量
Step 父类静态成员变量  随机数 0.8817076586261154
Step 子类静态成员变量
Step 子类静态成员变量  随机数 0.937529828526132
Step 父类成员变量
父类构造方法
Glyph() before draw()
子类draw方法, radius = 0 字符串 null
Glyph() after draw()
Step 子类成员变量
子类构造方法
RoundGlyph.RoundGlyph(), radius = 5

结果分析:(根据执行结果大概分析)

当调用子类的构造方法时,会使触发类的初始化。首先类装载器装载子类,发现子类有继承时再装载父类,此时并没有对类进行初始化,在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。然后再按类的初始化顺序依次初始化成员变量:

首先初始化父类静态语句,其次是成员变量,最后是构造方法,然后初始化子类。

这是大概流程,还有很多问题:

1.为什么子类加载到jvm知道要去加载父类,应为类的加载只是把类的二进文件流加载到内存,还没有执行任何代码,怎么知道去加载父类?

类的具体加载过程

首先,那些情况会使系统去加载一个类:

1.遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,假如类还没进行初始化,则马上对其进行初始化工作。其实就是3种情况:用new实例化一个类时、读取或者设置类的静态字段时(不包括被final修饰的静态字段,因为他们已经被塞进常量池了)、以及执行静态方法的时候。

2.使用java.lang.reflect.*的方法对类进行反射调用的时候,如果类还没有进行过初始化,马上对其进行。

3.初始化一个类的时候,如果他的父亲还没有被初始化,则先去初始化其父亲。

4.当jvm启动时,用户需要指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm会先去初始化这个类。

然后,系统正式加载一个类:

这一块虚拟机要完成3件事:

1.通过一个类的全限定名来获取定义此类的二进制字节流。

2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

此时系统还没有执行任何代码,接下来是对类的检验,验证是否符合规范等

包括文件格式验证->元数据验证->字节码验证->符号引用验证

接下来进入准备阶段,这时应该就是上面粗略结果分析的:在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零

此时也并没有对静态成员赋值

在对二进制文件进行解析后就进入初始化了:

此时就回到了会使系统加载一个类的第3条,当初始化一个类时,如果他的父亲还没有被初始化,则先去初始化其父亲。

Object应该是第一个被初始化的

此时开始执行真正的代码,第一个被执行的是编译时系统自动添加的<clinit>()方法,该方法是类加载时执行的第一个方法,<clinit>();方法与类构造方法不一样,他不需要显示得调用父类的<clinit>();方法,虚拟机会保证子类的<clinit>();方法在执行前父类的这个方法已经执行完毕了,也就是说,虚拟机中第一个被执行的<clinit>();方法肯定是Object类的<clinit>()方法。该方法只执行一次。

RoundGlyph类的<clinit>()方法:


可以看出,所有静态成员都在这里初始化

到上一步,类的加载已经完成,接下来就是类的初始化:

类的初始化是系统执行<init>方法(这个也是系统自动添加)查看init方法可以看出,init执行的顺序是:先初始化成员变量,最后再调用类的构造方法,所以构造方法总是最后调用


由上图可以看出,在程序退出之前调用了构造方法,所以类的初始化顺序总是先成员变量,然后再构造方法。在这之前已经在<clinit>中初始化了静态变量。对于final常量比较特别,它是在类装载的时候初始化的。

由上图可以看出,对于类中的域,常量有一个特别的分类,其中有一个specific info信息,是其他域没有的,可以让其在类装载时初始化。

类文件中成员变量的存贮

对一个简单的类成员在类常量池中的存储
public class TestMain {
	private int i = 222222;
	private int i1 = 11;
	private int i22 = 22;
	private long l = 66666;
	private double d = 99999;
	private Other o = new Other();
	private static String s = "abc";
	private static String s2 = "abcdef";
	private  String s3 = "abcdefghijk";
	TestMain(){
		i = 2;
	}
	public void f(){
		i = 111111;
		System.out.println("test");
	}
	public static void main(String[] args) {
		new TestMain();
	}
}
对于类文件中的常量池,当类加载后会被放置到内存中的方法区中的常量池,其中保存了各种常量和字面值。
但是当int型数值很大是,会被放置到常量池中,比较小时不会出现在常量池,double,float直接放置在常量池,还有static等,对于基本数据类型的值应该存储在运行时堆栈的。怎么在常量池中也存在。

其中对于int型的i,定义值为222222,存放在常量池,i1和i22直接用立即数入栈,直接在指令中保存数据。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值