Java类的加载和对象创建流程详细分析


实例代码:
Parent类

public class Parent {

	int a = 10;
	static int b = 11;
	
	//静态代码块
	static {
		System.out.println("Parent静态代码块:b=" + b);
		b++;
	}
	
	//代码块
	{
		System.out.println("Parent代码块:a=" + a);
		System.out.println("Parent代码块:b=" + b);
		a++;
		b++;
	}
	
	//无参构造方法
	public Parent(){
		System.out.println("Parent无参构造方法:a=" + a);
		System.out.println("Parent无参构造方法:b=" + b);
	}
	
	//有参构造方法
	public Parent(int a) {
		System.out.println("Parent有参构造方法:a=" + a);
		System.out.println("Parent有参构造方法:b=" + b);
	}
	
	//方法
	public void function() {
		System.out.println("Parent function run......");
	}
}

Child类

public class Child extends Parent {

	int x = 10;
	static int y = 11;
	
	//静态代码块
	static {
		System.out.println("Child静态代码块:y=" + y);
		y++;
	}
	
	//代码块
	{
		System.out.println("Child代码块:x=" + x);
		System.out.println("Child代码块:y=" + y);
		x++;
		y++;
	}
	
	//Child构造方法
	public Child() {
		// TODO Auto-generated constructor stub
		System.out.println("Child构造方法 x=" + x);
		System.out.println("Child构造方法 y=" + y);
	}
	
	public void function() {
		System.out.println("Child function run......");
	}
}

测试方法

 public class Test {
      public static void main(String[] args) {
         Child demo = new Child();
         demo.function();
         System.out.println("…………………………………………………………………………………………………………………………");
         Child child = new Child();
         child.function();
    }
}

输出结果

Parent静态代码块:b=11
Child静态代码块:y=11
Parent代码块: a=10
Parent代码块: b=12
Parent无参构造函数: a=11
Parent无参构造函数: b=13
Child代码块: x=10
Child代码块: y=12
Child构造函数: x=11
Child构造函数: y=13
Child function run ……
…………………………………………………………………………………………………………………………
Parent代码块: a=10
Parent代码块: b=13
Parent无参构造函数: a=11
Parent无参构造函数: b=14
Child代码块: x=10
Child代码块: y=13
Child构造函数: x=11
Child构造函数: y=14
Child function run …… 

结果详细分析

运行Test类的 main 方法

  1. 启动JVM,开始分配内存空间;
  2. 加载 Test.class 文件,加载到方法区中,静态的main方法进入静态区;
  3. 加载完成后运行main方法,这时JVM会把main方法调用到栈中运行;
  4. 运行到new Child();这时JVM会在方法区中查找有没有Child文件,如果没有就加载Child.class文件,并且Child继承了Parent类,也要查找是不是有Parent文件,如果没有也要加载Parent.class文件。
  5. Child.class和Parent.class文件中所有的静态内容会加载到静态方法区中,非静态内容会加载到非静态的区域中。静态内容(静态变量、静态代码块、静态方法)按照书写顺序加载。

说明:类的加载只会执行一次。下次再创建对象时可以从方法区直接获取class信息。

  1. 开始给静态区内所有的成员变量默认初始化,默认初始化完成后,进行显示初始化。
  2. 所有静态成员变量显示初始化完成后,开始执行静态代码块。先执行父类的静态代码块,再执行子类的静态代码块
//这时输出
Parent静态代码块:b=11
Child静态代码块:y=11

说明:静态代码块是在类加载的时候执行的,类加载只执行一次,所以静态代码块也只执行一次;非静态代码块和构造函数是在对象创建的时候执行的,因此对象创建(new)一次,它们就执行一次。

  1. 这时Parent.class和Child.class加载完成。
  2. 开始在堆中创建Child对象。给Child对象分配内存空间,其实就是分配内存地址。
  3. 开始对类中的非静态成员变量进行默认初始化。
  4. 开始加载对应的构造方法,执行隐式三步
①有个隐式的super()
②给非静态成员变量显示初始化
③执行非静态构造代码块(无static修饰的代码块)
之后才执行构造方法

super是调用父类的构造函数,此处为Parent的构造函数,在Parent的构造函数中也有隐式三步:首先super();再执行Parent的显示初始化;执行Parent的非静态构造代码块;最后执行Parent的构造方法。

//这时输出
Parent代码块: a=10
Parent代码块: b=12
Parent无参构造函数: a=11
Parent无参构造函数: b=13 

说明:虽然Parent没有明写entends,但是在Java中有一个超类Object,是所有类的父类,因此此处的super()是调用Object的构造方法。
Parent的执行完之后,回来继续执行Child自己的隐式三步的第二步:显示初始化,然后执行Child的非静态代码块;最后执行Child的构造方法。

//这时输出
Child代码块: x=10
Child代码块: y=12
Child构造函数: x=11
Child构造函数: y=13
  1. 对象创建完成,把内存的地址赋给demo使用
  2. 执行demo的function方法
//这时输出
Child function run ……
  1. 由于后面又创建(new)了一个新Child对象,因此从【9】之后再执行一次,输出结果为:
Parent代码块: a=10
Parent代码块: b=13
Parent无参构造函数: a=11
Parent无参构造函数: b=14
Child代码块: x=10
Child代码块: y=13
Child构造函数: x=11
Child构造函数: y=14
Child function run ……

内存运行示例图
在这里插入图片描述

总结

在创建(new)一个对象的时候,要先去方法区中查找该对象的类信息,如果方法区内没有,则需要先将类信息加载进来,再进行创建。

Java类被编译后,生成一个class文件,在运行时会将class文件加载到JVM中,class文件由类装载器装载,在JVM的方法区里形成一份描述class结构的元信息对象,通过该对象可获得class的结构信息:如构造函数,属性,方法等。

一、类的加载过程

  1. 加载阶段:
  2. 通过类加载的双亲委派模型将Class文件中的字节流加载到方法区中
  3. 在class文件加载到方法区时,先加载父类再加载子类;先加载静态内容再加载非静态内容。
  4. 把class中所有的静态内容加载到静态方法区下;
  5. 链接阶段:
  6. 检查加载的class内容正确性
  7. 静态内容加载完成后,对静态变量进行默认初始化;
  8. 将符号引用解析为直接引用,如果该引用没有加载,则触发该类的加载
  9. 初始化阶段:
  10. 静态变量的显示初始化,执行静态代码块(如果静态变量不是常量的话,会和静态代码块编译到一个方法中,JVM通过加锁的方式来保证该方法只被执行一次);在这个阶段之前会触发父类的加载初始化
  11. 类加载完成

对于静态方法和非静态方法都是被动调用,即系统不会自动执行。

二、对象的创建过程

  1. new一个对象时,在堆中开辟一块空间;
  2. 给开辟的空间分配一个地址;
  3. 把对象的所有非静态内容加载到所开辟的空间下;
  4. 非静态内容加载完成后,对非静态成员变量进行默认初始化;
  5. 默认初始化完成后,调用构造函数入栈执行;
  6. 构造函数入栈执行分为两步:!!!先执行构造函数的隐式三步,再执行构造函数内的代码!!!
①执行super()语句
②对开辟空间下的非静态成员变量进行显示初始化
③执行构造代码块
  1. 在整个构造函数执行完并弹栈后,把空间分配的地址赋值给引用对象。

三、其它

super语句可能出现以下三种情况:

  1. 构造方法体的第一句是this()语句,则不会先执行隐式三步,而是调用this()语句所对应的构造方法,最终肯定会有第一句不是this语句的构造方法。
public class Student {
 
    private String name;
 
    private String age;
 
    Student() {
    };
 
    Student(String name) {
    	第一句不是this(),在这里先执行隐式三步,再执行this.name = name
        this.name = name;
 
    };
 
    Student(String name, String age) {
        // 不会执行隐式三步
        会先执行Student(String name)构造方法
        this(name);
        this.age = age;
    };
 
}
  1. 构造方法的第一行是super(),则调用父类的构造方法
  2. 构造方法体的第一行既不是this()也不是super(),则隐式调用super(),即其父类的构造方法,这也是为什么一个父类通常要提供默认构造方法的原因。

转自【Java基础】Java类的加载和对象创建流程的详细分析

作者:明志健致远

出处:https://www.cnblogs.com/study-everyday/p/6752715.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值