类的生命周期

类的生命周期

一、类的加载与初始化

类的加载过程主要包含 加载、链接、初始化 三个步骤

1、加载(Load)

1)加载的流程

加载:读取字节码文件的二进制数据,并将其转换为JVM内部的数据结构(如Class对象)的过程。

  1. 通常是通过一个类的全限定名,获取定义此类的二进制字节流。
  2. 将 二进制字节流 代表的静态存储结构,转化为方法区的运行时数据结构
  3. 堆内存生成一个代表这个类的 java.lang.Class 对象,指向方法区这个类的元数据。
    • java.lang.Class 由 Java类库 提供,任何类型都有一个对应的Class对象,用于存储类型的信息。

在这里插入图片描述

2)加载的方式

  1. 本地文件系统

    从本地文件系统中读取类文件进行加载,这是最常见的加载方式。

  2. 网络下载

    从远程服务器下载类文件进行加载,典型场景是Web Applet。

  3. 压缩包

    从 JAR(Java Archive)或 WAR(Web Archive)文件中读取类文件进行加载。

  4. 运行时计算生成

    使用最多的是动态代理技术,通过在运行时生成代理类来动态地加载类。

  5. 其他文件生成

    典型场景是JSP(Java Server Pages)应用,JSP文件会被转换为对应的Java类文件然后加载执行。

  6. 专有数据库

    某些情况下可能会将类文件存储在数据库中,然后从数据库中提取加载。较为少见

  7. 加密文件

    一种保护措施,通过将类文件加密存储,在运行时再解密加载。

3)不同类型的加载

在 Java 中,数据类型分为 基本数据类型 和 引用数据类型。

  • 基本数据类型:由虚拟机预先定义。
  • 引用数据类型:需要进行类的加载

数组类的加载

数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要直接创建的,但数组的元素类型仍然是依靠类加载器创建。

  • 如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组 A 的元素类型;
  • JVM 使用指定的 元素类型 和 数组维度 来创建 新的数组类。

如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为 public。

2、类的链接(Link)

1)验证(Verify)

验证:确保加载的字节码符合当前虚拟机要求,保证被加载类的正确性与安全性。

  • 主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
    • 字节码的格式是否正确、语义是否符合语法规定、字节码是否可以被JVM安全执行等

2)准备(Prepare)

准备:为类的静态变量分配内存设置默认初始值的过程。

各类型变量的默认初始值:

数据类型默认初始值
基本数据类型 - 整数类型0
基本数据类型 - 小数类型0.0
基本数据类型 - 字符类型\u0000(空字符)
基本数据类型 - 布尔类型false
引用数据类型null

举例说明:

class A {
  	// 准备阶段:为静态变量a分配4个字节的空间,并设置初始值为0(到初始化阶段才会赋值为10)
    static int a = 10;
}

注意:以下情况会在链接-准备阶段 进行显示初始化(直接赋值,而不是设置默认初始值)

  1. final修饰的基本数据类型:不涉及 方法调用 与 构造器调用 且 赋的值是编译器可以确定的常量。
  2. final修饰的String类型:字面量赋值。

在编译时就已经确定的值,会被直接写入到常量池中,此时会在链接-准备阶段 直接赋予常量池中的值,而不是默认初始值。

下面举例说明:

public static final int INT_CONSTANT1 = 10;                               // 在链接-准备阶段赋值
public static final int INT_CONSTANT2 = new Random().nextInt(10);         // 在初始化阶段<clinit>()中赋值
public static int INT_CONSTANT3 = 1;                                      // 在初始化阶段<clinit>()中赋值

public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);	  // 在链接-准备阶段赋值
public static final Integer INTEGER_CONSTANT2 = Integer.valueOf(500);     // 在初始化阶段<clinit>()中赋值
public static Integer INTEGER_CONSTANT3 = Integer.valueOf(100);           // 在初始化阶段<clinit>()中概值

public static final String s0 = "helloworld0";                            // 在链接-准备阶段赋值
public static final String s1 = new String("helloworld1");                // 在初始化阶段<clinit>()中赋值
public static String s2 = "hellowrold2";                                  // 在初始化阶段<clinit>()中赋值

上述例子中可能比较难理解的是 INTEGER_CONSTANT1INTEGER_CONSTANT2

原因是:[-128, 127] 之间256个整数所有的包装对象提前创建好了,放在了整数常量池中。

3)解析(Resolve)

解析:将类中的 符号引用 转换为 直接引用 的过程

# 类中的符号引用
	就是字节码中的原始信息,比如类名,方法名,变量名等
	主要针对 类或接口、字段、类方法、接口方法、方法类型、方法句柄 和 调用限定符 7类符号引用进行。

# 直接引用
	运行时内存中相关的地址引用

# 说明
	调用一个方法时,在java语言层面就是一个方法名这个符号,
	但是在底层应该要根据这个符号找到内存中的直接地址来调用。

3、类的初始化(Initial)

初始化:静态变量的初始化(赋值操作)、静态代码块中语句的执行

  • 如果初始化一个类的时候,其父类尚未初始化,则优先初始化父类
  • 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
class A {
    // 初始化阶段:才会将10赋值给a(在此之前都是默认值)
    static int a = 10;
}

这个阶段其实是 <clinit> 方法执行过程的体现。这个方法中的逻辑,就是当前类中 静态变量 和 静态代码块 初始化逻辑的整合

1)<clinit>方法

一个类编译之后,字节码文件中会产生一个类构造器方法<cinit>(),类初始化阶段底层就由该方法完成。

<cinit>()方法中的逻辑,就是当前类中 静态变量静态代码块 初始化逻辑的整合。

在这里插入图片描述

2)初始化顺序

如果有多个静态代码块和静态变量,初始化顺序又会是怎样的呢?

在这里插入图片描述

结论:

  1. 如果静态变量只有定义,没有做赋值初始化(如上述案例中的静态变量height

    那么只会设置默认值(链接-准备 阶段),在 <clinit> 方法中不会看到初始化的指令。

  2. 静态代码块和静态变量直接赋值(如上述案例中的静态变量 agename),

    按照 编码顺序 从上到下 进行初始化,静态变量的值以最后一次赋值为准。

  3. 当一个类存在父类时,一定是先对父类进行初始化,然后再初始化子类,因为子类是依赖父类而存在的。

  4. 如果既没有静态变量的直接赋值,也没有静态代码块,就不会产生<clinit>方法

二、类初始化的时机

在Java中,类的使用方式可以分为主动使用被动使用两种情况。

Java 虚拟机规定,一个类或接口在初次使用(指的是主动使用)之前,必须要进行初始化。

1、类的主动使用(加载和初始化)

主动使用是指:在程序运行过程中,显式地对类进行操作或引用,导致类的加载和初始化。包括以下几种情况:

  1. 创建类的实例(如:new反射clone反序列化

  2. 使用反射方式对类进行操作(如: Class.forName("ClassName")

  3. 使用 类 的 静态变量(non-final)静态方法

  4. 初始化子类时,如果其父类还没有初始化,需要先初始化其父类。

  5. 启动类。当执行 Java 应用程序的 main方法 时,JVM 首先加载的就是 main方法所在的类。

  6. MethodHandle 是 JDK 7 引入的一种轻量级的方法引用机制,可以动态地执行方法。

    如果 MethodHandleREF_getStaticREF_putStaticREF_invokeStatic 句柄对应的类没有初始化,则初始化。

2、接口的加载

在 Java 中,接口是不会像类一样被初始化的。接口中不允许有静态代码块,因此在加载接口的时候不会执行任何初始化操作。

在 Java 虚拟机中,接口是被动加载的,只有在需要使用接口时才会加载它们,而不是像类那样在引用时加载。

  1. 当一个类被加载,而该类通过继承或实现关系引用了一个接口时,该接口会被加载。
  2. 当使用 Class.forName() 方法动态加载类时,如果这个类实现了某个接口,那么该接口也会被加载。
  3. 当接口的静态字段(即 static final 字段)被使用时,会触发接口的加载。
  4. 当调用接口中的静态方法时,会触发接口的加载。

总的来说,接口在被其实现类或者其它方式直接引用时会被加载,这是因为Java虚拟机需要检查接口的方法签名以及静态字段的定义。

3、类的被动使用(不初始化)

被动使用是指:在程序运行过程中,类的加载和初始化是由其它类的主动使用所引起的,而不是因为对类自身的直接引用导致的。

  • 除了主动使用,其他的情况均属于被动使用。
  • 被动使用不会引起类的初始化,但可能会进行类的加载。

1)定义数组引用

定义数组引用不会触发类的初始化。例如:

public class MyClass {
    static {
        System.out.println("MyClass 初始化");
    }
}

// 在另一个类中定义 MyClass 的数组引用,不会引起类的初始化
public class AnotherClass {
    public static void main(String[] args) {
        MyClass[] array = new MyClass[5];
    }
}

以下情况才会引起类的初始化

array[0] = new MyClass();

2)访问类的静态常量

访问类的静态常量(static final)不会导致类的初始化,因为常量在链接阶段就已经被显式赋值了。

public class MyClass {
    public static final int CONSTANT = 10;
}

// 在另一个类中访问 MyClass 的静态常量,不会引起类的初始化
public class AnotherClass {
    public static void main(String[] args) {
        System.out.println(MyClass.CONSTANT);
    }
}

3)子类引用父类的静态变量

通过子类引用父类的静态变量,不会导致子类初始化,只有真正声明这个字段的类才会被初始化。

class Parent {
    static {
        System.out.println("Parent类初始化");
    }
    public static int parentStaticVar = 100;
}

class Child extends Parent {
    static {
        System.out.println("Child类初始化");
    }
}

public class Test {
    public static void main(String[] args) {
        System.out.println(Child.parentStaticVar); // 只会初始化父类Parent,不会初始化子类Child
    }
}

4)ClassLoader的loadClass()

调用 ClassLoader 类的 loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.test.java.Person");

三、对象的创建与初始化

1、对象的创建方式

  1. new(单例模式、工厂模式、建造者模式等都是其变形)
  2. 反射(Class对象 和 Constructor对象的 newInstance方法)
  3. clone(实现 Cloneable 接口,重写 clone 方法)
  4. 反序列化:获取一个二进制流,读取文件中序列化的对象数据,还原成内存中的对象。
  5. 第三方库:利用一些字节码技术生成对象。例如 Objenesis

2、对象的内存分配

首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果是引用变量,仅分配引用变量空间即可(4个字节)

根据内存是否规整,分为以下两种分配方式:

  • 内存规整:JVM 采用指针碰撞法(Bump The Point)来为对象分配内存。
    • 所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器。
    • 分配内存就是 把指针移动一段与对象大小相等的距离。
  • 内存不规整:JVM 维护一个 空闲列表(Free List)来为对象分配内存。
    • 空闲列表记录了哪些内存空间是可用的。
    • 分配内存就是 从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。

内存是否规整取决于 JVM 采用的垃圾收集器是否带有压缩整理功能决定。

  • 一般使用 基于 复制算法标记-整理算法 的垃圾收集器时,会采用 指针碰撞法 来分配。
  • 一般使用 基于 标记-清除算法 的垃圾收集器时,会采用 空闲列表 来分配。

3、内存分配的并发安全

对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。

解决这个问题有两种方案:

  • 同步处理

    对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性)

  • 使用 TLAB(Thread Local Allocation Buffer) 本地线程分配缓冲区

    TLAB 是 JVM 在 堆空间的 Eden区 为 每个线程 分配的一块 线程私有的区域,每个线程在各自的 TLAB 上进行内存的分配,避免并发问题的情况下还能够提升内存分配的吞吐量。

只有当 TLAB 空间用完 或 TLAB分配失败 时,才需要进行同步处理。

可以通过 -XX:+/-UserTLAB 参数来设定虚拟机是否使用TLAB(默认是开启的)

4、对象的准备(默认值)

为对象的所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用。

各类型变量的默认初始值:

数据类型默认初始值
整数类型0
小数类型0.0
字符类型\u0000(空字符)
布尔类型false
引用数据类型null

5、对象的对象头

将对象的所属类(即类的元数据信息)、对象的 HashCode 和 对象的 GC 信息、锁信息等数据存储在对象的对象头中。

这个过程的具体设置方式取决于 JVM 实现。

6、对象的初始化

执行 <init> 方法(实例变量的初始化、实例代码块中初始化、构造器中初始化)并把堆内对象的首地址赋值给引用变量。

1)<init>方法

我们通过下面这个例子分析一下<init>方法:

在这里插入图片描述

从反编译后的结果可以推导出以下结论

  1. 每个构造方法都会对应一个 <init> 方法

    实例变量初始化、实例代码块初始化、构造函数初始化的逻辑,都会统一的放到<init>方法完成。

  2. 每个 <init> 方法内部都会先执行父类的 <init> 方法。(默认继承Object,先执行Object.<init>

  3. 执行完父类的 <init> 方法后,按编码顺序从上到下再执行 实例变量直接赋值实例代码块。(没有则跳过)

  4. 最后执行构造方法内部的赋值语句

2)初始化顺序

  • <init>方法中,优先执行父类的<init>方法
  • 然后按编码顺序从上到下执行 实例变量直接赋值实例代码块构造方法内部的赋值语句

在这里插入图片描述

3)类 与 对象 的初始化 混合

一般情况:类初始化 优先于 对象初始化。

特殊情况:类初始化阶段,涉及到本类的对象实例化 —> 先进行对象实例化(混合进行)

package test;

public class A {
    // 静态变量
    static int a = getA();
    // 类初始化阶段,涉及到本类的对象实例化
    static A obj = new A();		

    // 静态方法
    private static int getA() {
        System.out.print(1);
        return 10;
    }

    // 静态代码块
    static {
        System.out.print(2);
        a = 20;
    }

    // 实例变量
    int b = getB();

    // 实例方法
    private int getB() {
        System.out.print(3);
        return 20;
    }

    // 实例代码块
    {
        System.out.print(4);
        b = 40;
    }

    // 构造方法
    public A() {
        System.out.print(5);
    }

    // main方法
    public static void main(String[] args) {
        System.out.print(6);
        new A();
    }
}

【过程分析】

在这里插入图片描述

// main方法执行前:类初始化(静态变量)
1
// 类初始化阶段,涉及到本类的对象实例化 -> 先进行对象实例化
3 4 5
// 对象实例化结束 -> 继续进行类初始化(静态代码块)
2
// main方法执行:控制台打印
6
// 创建对象:对象实例化(实例变量 -> 实例代码块 -> 构造方法)
3 4 5

四、对象的内存布局

1、对象头(Header)

对象头包含的内容:

  • 运行时元数据(Mark Word)
    • 哈希值(HashCode)
    • GC 分代年龄
    • 锁状态标志
    • 线程持有的锁
    • 偏向线程 ID
    • 偏向时间戳
  • 类型指针(Klass Word)
    • 指向类元数据 InstanceKlass,确定该对象所属的类型。
  • 如果是数组,还需要记录数组的长度(array length)

1)32位虚拟机

普通对象

|------------------------------------------------------------| 
|                    Object Header (64 bits)                 | 
|-----------------------------|------------------------------| 
|     Mark Word (32 bits)     |     Klass Word (32 bits)     | 
|-----------------------------|------------------------------|

数组对象

|-------------------------------------------------------------------------------------------|
|                                  Object Header (96 bits)                 					|
|-----------------------------|------------------------------|------------------------------| 
|     Mark Word (32 bits)     |     Klass Word (32 bits)     | 	  Array Length (32 bits)	|
|-----------------------------|------------------------------|------------------------------|

Mark Word 结构

|-------------------------------------------------------|--------------------| 
| 			 	Mark Word (32 bits) 					|  	 	State 	     | 
|-------------------------------------------------------|--------------------| 
|       hashcode:25   | age:4 |  biased_lock:0 	|  01   | 		Normal 		 | 
|-------------------------------------------------------|--------------------| 
| thread:23 | epoch:2 | age:4 |  biased_lock:1  |  01   | 		Biased 		 | 
|-------------------------------------------------------|--------------------| 
| 				ptr_to_lock_record:30 			|  00 	| Lightweight Locked | 
|-------------------------------------------------------|--------------------| 
|  			ptr_to_heavyweight_monitor:30		|  10 	| Heavyweight Locked | 
|-------------------------------------------------------|--------------------| 
| 												|  11 	| 	Marked for GC 	 | 
|-------------------------------------------------------|--------------------|

2)64位虚拟机

普通对象

|------------------------------------------------------------| 
|                    Object Header (128 bits)                | 
|-----------------------------|------------------------------| 
|     Mark Word (64 bits)     |     Klass Word (64 bits)     | 
|-----------------------------|------------------------------|

数组对象

|-------------------------------------------------------------------------------------------|
|                                  Object Header (160 bits)                 				|
|-----------------------------|------------------------------|------------------------------| 
|     Mark Word (64 bits)     |     Klass Word (64 bits)     | 	  Array Length (32 bits)	|
|-----------------------------|------------------------------|------------------------------|

Mark Word 结构

|-------------------------------------------------------------------|--------------------| 
| 			 			  Mark Word (64 bits) 					    |  	 	State 	     | 
|-------------------------------------------------------------------|--------------------| 
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 |  01  | 		Normal 		 | 
|-------------------------------------------------------------------|--------------------| 
| thread:54 |   epoch:2   | unused:1 | age:4 | biased_lock:1 |  01  | 		Biased 		 | 
|-------------------------------------------------------------------|--------------------| 
| 					ptr_to_lock_record:62 					 |  00  | Lightweight Locked | 
|-------------------------------------------------------------------|--------------------| 
|  				ptr_to_heavyweight_monitor:62				 |  10 	| Heavyweight Locked | 
|-------------------------------------------------------------------|--------------------| 
| 															 |  11 	| 	Marked for GC 	 | 
|-------------------------------------------------------------------|--------------------|

2、实例数据(Instance Data)

它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)

  • 相同宽度的字段总是被分配在一起
  • 父类中定义的变量会出现在子类之前
  • 子类的窄变量(占用较少字节的变量)可能插入到父类变量的空隙

3、对齐填充(Padding)

64位的操作系统,一次性读取 64bit(8 byte)整数倍的数据。所以 HotSpot 为了高效读取对象,就做了"对齐":

  • 如果一个对象实际占用的内存大小不是 8 byte 的整数倍,就"补位"到 8 byte 的整数倍。

所以对齐填充区域的大小不是固定的。

4、对象的内存大小分析

我们以一个 Integer 对象 在 64位虚拟机 中占用的内存大小为例:

  1. 对象头:Mark Word(8个字节) + Klass Word(8个字节) = 16 字节
  2. 实例数据:Integer 对象实际存储的数据是一个 int 类型的值(4个字节)
  3. 对其填充:这种情况下,是4个字节

综上所述,一个 Integer 对象占用的总字节数大致为 24 个字节。

需要注意的是,这只是一个估计值,具体的占用字节数可能会因 JVM 实现、平台和对象内存对齐等因素而有所不同。

5、图解对象的内存布局

public class Customer {
    int id = 1001;
    String name;
    Account acct;

    {
        name = "匿名客户";
    }

    public Customer() {
        acct = new Account();
    }
}

public class Account {}

public class CustomerTest {
    public static void main(string[] args){
        Customer cust = new Customer();
    }
}

在这里插入图片描述

五、对象的访问定位

JVM 是如何通过栈帧中的对象引用访问到其内部的对象实例呢?

1、句柄访问

对象引用 指向堆中的句柄,通过句柄来分别访问 对象实例数据(堆)对象类型数据(方法区)

  • 优点:
    • 对象被移动时(GC时很普遍)只要改变句柄中实例数据指针即可,reference 本身不需要修改。
  • 缺点:
    • 在堆空间中开辟了一块空间作为句柄池,句柄池本身也会占用空间。
    • 通过两次指针访问才能访问到堆中的对象,效率低。

在这里插入图片描述

2、直接指针(HotSpot 采用)

对象引用 直接指向 对象实例数据(堆) ,对象实例中有类型指针,指向对象类型数据(方法区)

  • 优点:不需要开辟一块空间给句柄池;访问效率更高。
  • 缺点:对象被移动时(GC时很普遍)需要修改 reference 的值

在这里插入图片描述

六、类的卸载

1、类加载器、类、类实例

  • 在类加载器的内部实现中,用一个 Java 集合来存放所加载类的引用。
  • 一个 Class 对象总是会引用它的类加载器,调用 Class 对象的 getClassLoader()方法,就能获得它的类加载器。
  • 一个类的实例 总是引用 代表这个类的Class对象,通过 Object类 中定义的 getClass() 方法,就能获得它的Class对象。

在这里插入图片描述

2、类卸载的条件

通过以上的关系分析,可以分析出类的卸载需要同时满足 3 个要求:

  • 该类所有的实例都已经被回收(堆中不存在该类及其任何派生子类的实例)
  • 加载该类的类加载器已经被回收(除非是自定义类加载器,JDK自带的类加载器一般是不会被回收的)
  • 该类对应的 Class对象 没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

由此可见,只有我们自定义的类加载器加载的类是可能被卸载的。

七、类的生命周期(小结)

Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的 class 文件加载到内存生成 class 对象。

按照 Java 虚拟机规范,从 class 文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下 7 个阶段:

在这里插入图片描述

类的生命周期小结:

  1. 加载(Loading) —> 这个过程由「类加载器」完成

    根据类全名,获取类或接口的二进制字节流,转化为方法区数据结构,并在堆创建Class对象,指向方法区的类的元数据

  2. 链接(Linking)

    1. 验证(Verify)

      确保加载的字节码符合当前虚拟机要求,保证被加载类的正确性与安全性(验证 文件格式、元数据、字节码、符号引用)

    2. 准备(Prepare)

      静态变量(non-final static) 分配内存设置默认初始值

    3. 解析(Resolve)

      符号引用(变量名,方法名,类名) 转换为 直接引用(地址指针) 的过程。

  3. 初始化(Initial)

    进行 静态变量 的初始化(赋值)、执行静态代码块中的语句。( <clinit> 方法)

  4. 使用(Use)

    1. 创建实例对象
    2. 实例对象 在堆空间分配内存设置默认初始值(对象空间的开辟是由new指令完成的)
    3. 进行 实例变量的初始化(赋值)、执行实例代码块构造方法中的语句。(<init>方法)
  5. 卸载(Unload)

    • 该类所有的实例都已经被回收(堆中不存在该类及其任何派生子类的实例)
    • 加载该类的类加载器已经被回收(除非是自定义类加载器,JDK自带的类加载器一般是不会被回收的)
    • 该类对应的 Class对象 没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    当以上条件都满足时,Class 对象才会结束生命周期,类在方法区内的数据才会被卸载。

八、经典笔试案例

阅读代码,分析出打印结果:

public class Test1 {
    public static int k = 0;
    public static Test1 t1 = new Test1("t1");
    public static Test1 t2 = new Test1("t2");
    public static int i = print("i");//3
    public static int n = 99;
    
    // 实例变量
    public int j = print("j");

    // 静态代码块
    static {
        print("静态块");
    }
  
    // 构造方法
    public Test1(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++i;
        ++n;
    }

    // 实例代码块
    {
        print("构造块");
    }

    // 静态方法
    public static int print(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++n;
        return ++i;
    }

    // main方法
    public static void main(String[] args) {
        Test1 t = new Test1("init");
    }
}

【分析】

  1. main方法之前,进行类初始化
  2. 类初始化中间有两次对象创建,要进行对象初始化
  3. 对象初始化结束,继续类初始化
  4. 类初始化结束,执行main方法
  5. main方法中有一次对象创建,要进行对象初始化

类初始化:从上至下加载 静态变量/静态代码块

对象初始化:从上至下加载 实例变量/实例代码块

【参考答案】

1:j i=0 n=0
2:构造块 i=1 n=1
3:t1 i=2 n=2

4:j i=3 n=3
5:构造块 i=4 n=4
6:t2 i=5 n=5

7:i i=6 n=6

8:静态块 i=7 n=99

9:j i=8 n=100
10:构造块 i=9 n=101
11:init i=10 n=102
  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

scj1022

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值