类加载详细过程
Java代码的执行顺序遵循一定的规则和步骤,这些规则主要涉及类加载、初始化、静态块执行、实例化过程以及方法调用等方面。以下是Java代码执行顺序的详细说明:(如果不想看可以直接看下面简要说明和例题)
1. 程序入口:main() 方法
Java程序的执行始于指定主类(带有 public static void main(String[] args) 方法的类)中的 main() 方法。这是程序的起点,也是JVM(Java虚拟机)开始执行Java代码的地方。
2. 类加载与初始化
类加载:当首次遇到一个类的引用时(如创建类的实例、访问类的静态成员或调用类的静态方法),JVM会加载该类。加载过程包括:
- 验证:检查字节码的正确性。
- 准备:为类的静态变量分配内存并赋予默认初始值(如整型为0,对象引用为 null)。
- 解析:将符号引用转换为直接引用(如方法表、字段表的偏移量)。
类初始化:当且仅当以下条件之一满足时,JVM会触发类的初始化:
- 当new一个类的实例时。
- 当访问类的静态变量(除了final且编译器可以确定其初始值的情况)或静态方法时。
- 当子类初始化时,其父类未初始化,则先初始化父类。
- 当使用反射API对类进行反射调用时。
- 当初始化某个类的接口时,该接口未初始化
3. 静态初始化块与静态成员变量初始化
在类初始化阶段,按照源代码中出现的顺序,执行以下操作:
- 静态变量初始化:为所有声明时带有显式初始值的静态变量赋予指定值。
- 静态初始化块(如果存在):按照在类定义中出现的顺序,依次执行静态初始化块中的代码。这些代码块通常用于集中处理静态资源的初始化或其他一次性设置。
4. 实例初始化
当创建类的实例时,执行以下步骤:
- 分配内存:JVM为新对象分配内存空间。
- 实例变量初始化:为所有声明时带有显式初始值的实例变量赋予指定值。
- 构造函数调用:调用最合适的构造函数(根据提供的参数列表确定)。构造函数内部的代码按照编写顺序执行。如果构造函数中调用了超类构造函数(通过 super(...)),则先执行超类构造函数。
- 实例初始化块(如果存在):按照在类定义中出现的顺序,依次执行实例初始化块中的代码。这些代码块用于初始化每个对象实例的通用设置,与构造函数互补。
5. 方法调用
对于非静态方法的调用,通常是在对象实例已经创建后,通过对象引用来完成。方法内部的执行顺序遵循语句的书写顺序,同时考虑到控制流(如条件、循环、异常处理等)的影响。
总结
Java代码的执行顺序大致如下:
1.从main()方法开始。
2.按需加载类并进行类初始化:
- 静态变量初始化。
- 静态初始化块执行。
3.创建对象实例:
- 分配内存。
- 实例变量初始化。
- 构造函数调用(可能递归调用超类构造函数)。
- 实例初始化块执行。
4.通过对象引用来调用非静态方法,方法内部代码按书写顺序执行。
简单来说代码块执行的顺序:
静态代码块
构造代码块
构造器
类加载的过程(简单版)
类加载
JVM都知道基本数据类型的大小, 但是对于自定义的数据类型,JVM知不知道大小。因此,对于基本数据类型,JVM 知道占多大空间,能进行哪些操作。 但是对于我们新建的引用数据类型, 它不知道里面有多少变量,有多少方法。所以在JVM刚开始使用的时候,一定会进行一个操作: 认识该类型。 JVM认识这个类的过程,就称为类加载。
public class Demo3 {
public static void main(String[] args) {
Student student = new Student();
int age = student.age;
String name = student.name;
}
}
创建对象的时候。 首先会进行的操作是类加载。 类加载其实就是JVM了解, 里面有哪些属性,有哪些方法, 方法里面的代码是怎样的 Student student = new Student(); 这行代码。 在内存上, 第一步,会需要在堆上开辟一个空间存储对象数据 第二步, 在栈上创建一个引用,引用存储的是堆上的地址。 类加载其实就是JVM认识一个类的过程。 类加载要在创建对象之前进行,换句话说创建一个类的对象必然触发该类的类加载!
例题
第一题
class SuperClass {
static int superStaticVar = 1; // 静态变量初始化
static {
System.out.println("SuperClass: Static block");
}
SuperClass() {
System.out.println("SuperClass: Constructor");
}
}
class SubClass extends SuperClass {
static int subStaticVar = 2; // 静态变量初始化
static {
System.out.println("SubClass: Static block");
}
{
System.out.println("SubClass: Instance initialization block");
}
SubClass(int value) {
super(); // 调用超类无参构造函数
System.out.println("SubClass: Constructor with value " + value);
}
public static void main(String[] args) {
System.out.println("Main method starts");
System.out.println("Accessing static variable: " + SubClass.subStaticVar); // 触发类初始化
SubClass instance = new SubClass(42); // 实例化SubClass对象
System.out.println("Main method ends");
}
}
执行上述代码时,控制台输出将按照以下顺序显示:
Main method starts
SuperClass: Static block
SubClass: Static block
Accessing static variable: 2
SuperClass: Constructor
SubClass: Instance initialization block
SubClass: Constructor with value 42
Main method ends
1.程序从SubClass的main()方法开始执行。
2.访问SubClass.subStaticVar时,触发SubClass及其超类SuperClass的类初始化:
先执行SuperClass的静态变量初始化,设置superStaticVar为1。
执行SuperClass的静态初始化块,打印"SuperClass: Static block"。
执行SubClass的静态变量初始化,设置subStaticVar为2。
执行SubClass的静态初始化块,打印"SubClass: Static block"。
输出访问的静态变量值:2。
3.实例化SubClass对象:
分配内存给新对象。
执行SuperClass的构造函数,打印"SuperClass: Constructor"。
执行SubClass的实例初始化块,打印"SubClass: Instance initialization block"。
执行SubClass构造函数(传入参数42),打印"SubClass: Constructor with value 42"。
4.打印"Main method ends",结束main()方法执行
第二题
public class Demo5 {
static {
System.out.println("Demo5类开始初始化步骤了!");
}
static Cat5 cat5 = new Cat5();
Dog5 dog5 = new Dog5();
public Demo5() {
System.out.println("Demo5 constructor");
}
public static void main(String[] args) {
System.out.println("hello world!");
Demo5 d = new Demo5();
}
}
class Cat5 {
static {
System.out.println("Cat5类开始初始化步骤了!");
}
static Dog5 dog5 = new Dog5();
public Cat5() {
System.out.println("Cat5 constructor");
}
}
class Dog5 {
static {
System.out.println("Dog5类开始初始化步骤了!");
}
static Demo5 demo = new Demo5();
public Dog5() {
System.out.println("Dog5 constructor");
}
}
简述:程序会先进入主函数,进入后第一步会开始类加载,会开始执行各个class中的static语句。
先按顺序加载类,第一个类是Demo4的类加载,开始执行Demo4中的static语句(static只会在类加载过程执行一次,之后不会再次执行),输出Demo4类开始初始化了。
之后开始执行Static Cat5=new Cat5()的语句(这句会分开执行,会先执行Static Cat5,也就是JVM会先进行类加载,加载Cat5的类),进入到class Cat5,开始执行这里的static语句,第一个static语句是输出Cat5类开始初始化步骤了
然后发现第二个static语句又是一个new语句,Static Dog5=new Dog5()的语句(和上个Cat5的new语句一样会先进行Dog5类的类加载)
进入class Dog5来执行static语句,输出Dog5类开始初始化步骤了。Dog5类中第二个static语句是Demo4类的new语句,又会进行Demo4类的部分,Demo4类已经进行了类加载,static语句已经执行了,就不会再次执行了。那么第一个执行的语句就是Dog5的new语句,Dog5已经完成了类加载,就会按顺序执行其中的其他语句,也就是public中的语句,输出Dog5 constructor,之后返回Demo4中执行下一条语句,也就是public Demo4,输出Demo4 constructor。至此class Dog5完成了类加载
返回上一步class Cat5,Cat5也完成了类加载,开始完成newCat5的后半句,执行class Cat5中的语句,只有public Cat5需要执行,输出Cat5 constructor。至此Demo4类加载完成
类加载这也一过程完事,开始执行main函数中的语句,输出hello world!。
开始执行Demo4的语句(Demo4已经完成了类加载,不需要再次执行static语句),开始执行Dog5的语句(类也加载完了),直接执行类中的输出语句,输出Dog5 constructor。然后再执行public的输出语句,输出Demo5 constructor。
输出结果:
Demo5类开始初始化步骤了!
Cat5类开始初始化步骤了!
Dog5类开始初始化步骤了!
Dog5 constructor
Demo5 constructor
Dog5 constructor
Cat5 constructor
hello world!
Dog5 constructor
Demo5 constructor
- static代码块,类加载的时候会直接执行。
- static修饰的成员变量,在类加载的时候也会执行。
- static修饰的方法,在类加载的时候,不会自动执行
静态成员变量
-
和普通成员变量一样,都具有默认值(默认值和普通成员变量是一样的)
-
静态成员变量属于类的,完全不需要创建对象使用。
-
访问和使用静态成员变量不推荐使用"对象名.",而应该使用"类名."!
-
静态成员变量的访问/赋值/使用都不依赖于对象, 而是依赖于类
-
静态成员需要在类加载时期,完成准备,类加载结束就能够使用。所以访问类的静态成员,一定会触发该类的类加载。
内存及原理解析:
静态成员的访问并不依赖于创建对象,可以直接通过类名访问,其原因在于:
随着类加载完毕,静态成员就存在,并且能够使用了!
某个类的某个静态成员变量只有一份,且被所有对象共享,属于类,无需创建对象使用。
注意:只存在静态成员变量,不存在"静态局部变量"
静态成员方法
-
无需创建对象就可以直接通过类名点直接调用。
-
同一个类中的static方法互相调用可以省略类名,直接用方法名调用。(这就是我们之前方法的调用)
-
一个类中,静态方法无法直接调用非静态的方法和属性,也不能使用this,super关键字(super后面会讲),静态的方法只能访问静态的。原因:静态方法调用的时候,有可能没有对象,没有对象普通成员就无法访问。
-
普通成员方法当中,既可以访问静态成员的, 也可以访问非静态成员。普通成员方法访问任意的
-
访问静态成员变量的时候,使用类名.变量名的形式访问,以示区别,增加代码可读性
第三题
public class ExerciseBlock {
static {
System.out.println("main方法静态代码块!");
}
{
System.out.println("main方法构造代码块!");
}
public static void main(String[] args) {
System.out.println("main方法开始执行!");
Star s = new Star(18,"马化腾");
System.out.println(Star.name);
System.out.println(s.age);
}
}
class Star{
{
age = 18;
Star.name = "杨超越";
System.out.println("我喜欢杨超越");
}
static String name = "王菲";
int age = 28;
static {
name = "杨幂";
System.out.println("我喜欢杨幂");
}
public Star(int age,String name) {
this(age);
System.out.println("age,name:构造器!");
Star.name = name;
Star.name = "刘亦菲";
}
public Star(int age) {
System.out.println("age:构造器!");
this.age = age;
}
public Star() {
}
}
简述:先执行main函数,第一步永远是类加载,加载ExerciseBlock类,输出main方法静态代码块(类加载是懒加载用不到就不加载),然后执行主函数中的第一条语句(输出语句),输出main方法开始执行。然后创建s对象,开始加载Star类,执行static语句,name=王菲,然后name=杨幂米,之后输出我喜欢杨幂,之后开始进行new Star。开始传入参数执行public Star(age,name),第一句就是this(age),开始调用下面单参的构造器(先显示赋值,然后再开始构造器赋值)开始从上面class Star按顺序执行代码块,输出我喜欢杨超越, int age=28。开始单参构造器 ,输出age:构造器!,age=18。返回双参构造器,输出age,name:构造器!, 把name赋值成马化腾,在改成刘亦菲。所以最后输出的是刘亦菲和18。
附:构造器赋值顺序(下面详细解释)
1.默认初始化,具有默认值。
2.显示赋值,直接将值写在成员变量声明的后面。
3.构造器赋值
成员变量赋值中,构造器是最后执行的。
构造器的赋值顺序指的是在创建对象时,构造器内部对成员变量进行赋值的先后顺序。构造器内成员变量的赋值遵循以下规则:
- 默认初始化:如果成员变量在声明时指定了默认值,那么首先会进行默认初始化。
- 显式初始化:如果成员变量在声明时进行了赋值(即显式初始化),那么在构造器调用前,这些赋值会被执行。
- 构造器中赋值:在构造器的主体中,按照代码书写的顺序依次对成员变量进行赋值。
public class User {
// 默认初始化为 null
private String name;
// 显式初始化为 0
private int age = 0;
// 显式初始化为 "Unspecified"
private String occupation = "Unspecified";
public User(String newName, int newAge) {
// 构造器中赋值,先赋值 name,再赋值 age
name = newName;
age = newAge;
// 在构造器中更改 occupation 的值
occupation = "Developer";
}
public void displayUserDetails() {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
System.out.println("Occupation: " + occupation);
}
public static void main(String[] args) {
User user = new User("Alice", 30);
user.displayUserDetails();
}
}
Name: Alice
Age: 30
Occupation: Developer
解析赋值顺序:
- 默认初始化:成员变量 name 被默认初始化为 null。
- 显式初始化:
- 成员变量 age 被显式初始化为 0。
- 成员变量 occupation 被显式初始化为 "Unspecified"。
3.构造代码块赋值 (下面详细讲述)
4.构造器中赋值:
- 构造器 User(String newName, int newAge) 被调用时,首先将传入参数 newName 赋给 name,此时 name 的值变为 "Alice"。
- 接着将传入参数 newAge 赋给 age,此时 age 的值变为 30。
- 最后,将 occupation 的值显式改为 "Developer"。
5.显示用户详情:
- 调用 displayUserDetails() 方法,输出当前 User 对象的 name、age 和 occupation 的值,即按上述顺序赋值后的结果。
综上所述,构造器中成员变量的赋值顺序遵循默认初始化 → 显式初始化 → 构造器中赋值的规则。在这个示例中,成员变量 name、age 和 occupation 分别经历了默认初始化、显式初始化和构造器中赋值的过程,最终输出了经过构造器赋值后的值。
构造代码块
定义在类的成员位置,使用以下声明方式声明的代码块,称之为构造代码块。
作用是随着构造器的执行,用于在创建对象过程中,给成员变量赋值
//成员位置
{
// 局部位置
}
//成员位置
构造代码块内部属于局部位置,在里面定义变量,就是一个仅在构造代码块中生效的局部变量。
public class Demo2 {
int a = 20;
// 在这个位置定义块,就是构造代码块
{
// 这个a,仅在局部生效。
int a = 10;
System.out.println(a);
}
}
这里总结给成员变量赋值的几种方式(创建对象过程中):
默认初始化,具有默认值
显式赋值
构造代码块
构造器
学习对象中成员变量的赋值,和赋值顺序要遵循"掐头去尾"的原则:
头:默认初始化,具有默认值,在对象结构存在于对象中,对象中的成员变量就已经具有了默认值。
77777我们程序员所有能干预的赋值方式,都是在默认初始化的基础上进行的。
尾:构造器,构造器在整个对象的成员变量赋值过程中,处在最后的阶段,最后被执行。
明确以上两点后,我们现在只需要研究显式赋值和构造代码块的赋值顺序,
显式赋值和构造代码块的执行顺序,并不是固定的,而是按照代码的书写顺序去执行的:
这两个结构,谁写在代码书写顺序的上面,谁就先执行。
后执行结构的结构,自然会覆盖先执行结构的结果。
通过查看反编译class文件(通过IDEA),我们发现编译后的代码中并不存在构造代码块的结构,而是:
直接将成员变量的显式赋值和构造代码块中的代码智能地加入,类所有的构造器中的前几行:
所谓智能是为了保证:成员变量的显式赋值和构造代码块,按照代码的书写顺序从上到下执行!
于是,我们可以得出以下结论:
使用new对象的方式创建对象,不论使用哪个构造器,构造代码块都会随之执行。
构造器是每一次new对象都会执行一次,所以构造代码块也会随之执行一次。
构造代码块中的代码要放入构造器的首几行,所以在同一个类中,构造代码块总是先于它的构造器而执行。
创建对象过程中的执行顺序
new对象过程中,各种结构的执行顺序:
-
对象结构存在后就进行默认初始化,所有成员变量都具有默认值后,再开始其余赋值操作
-
找到new对象的那个构造器
如果它的首行显式地调用了另一个构造器this(实参)
( 注:显式调用构造器目前指的是this调用自身构造器,其它场景这里先不考虑)
那么程序会先跳转到那个构造器,但是不会立刻执行,而是:
按照类中构造代码块和显式赋值的代码书写顺序,从上到下执行其中的代码,执行完毕后:
跳转回this语句要指示执行的构造器,执行其中的代码,然后:
跳转回new对象构造器,执行完毕后,创建对象结束。
注:整个过程中,构造代码块和显式赋值的代码只会执行一次,不会执行多次!!
如果它的首行没有显式调用另一个构造器
那么会先从上到下执行构造代码块和显式赋值代码
执行完毕后:跳转回new对象构造器,执行完毕后,创建对象结束。
注:整个过程中,构造代码块和显式赋值的代码只会执行一次,不会执行多次!!