在Java中,对象创建是一个至关重要的过程,它涉及类加载、内存分配、初始化、赋值等多个阶段。理解这些过程不仅有助于编写更高效的代码,还能帮助我们优化程序的性能。本文将深入分析Java对象创建的全过程,重点讨论类加载、对象初始化、静态与非静态初始化的顺序,以及JVM堆栈内存的分配机制,并结合代码进行解析。
一、类加载过程
Java虚拟机(JVM)在执行Java程序时,需要首先将类加载到内存中。类加载分为以下几个步骤:
- 加载(Loading):JVM通过类加载器(ClassLoader)加载字节码文件(
.class
文件)到内存中,并将其转换为Class
对象。 - 链接(Linking):
- 验证(Verification):确保类的字节码符合JVM规范。
- 准备(Preparation):为类的静态变量分配内存,并初始化为默认值。
- 解析(Resolution):将常量池中的符号引用替换为直接引用。
- 初始化(Initialization):执行类的静态代码块以及静态变量的初始化。
示例代码:
java
代码解读
复制代码
public class InitializeDemo2 { private static int k = 1; private static int n = 10; private static int i; private String s = new String("-----"); static { System.out.println("静态代码块:执行了"); System.out.println("k=" + k + ", n=" + n + ", i=" + i); } private int h = 2; { System.out.println("普通代码块:执行了"); System.out.println(s); } public static void m1() { System.out.println("静态方法:执行了"); System.out.println("k=" + (k++) + ", n=" + n + ", i=" + i); } public void m2() { System.out.println("普通方法:执行了"); System.out.println("k=" + (k++) + ", n=" + n + ", i=" + i); } public InitializeDemo2(String str) { System.out.println("构造器:执行了"); System.out.println("k=" + (k++) + ", n=" + n + ", i=" + i); } }
在上述代码中,InitializeDemo2
类的加载过程会按上述顺序进行。静态变量 k
在“准备”阶段被分配内存并初始化为默认值 0
,在“初始化”阶段被赋值为 1
。
说到这里,就要暂停下。我们怎么验证类的加载过程呢(本文仅从程序员更容易看懂的角度)?我们知道,如果调用类内部的静态方法,会触发类加载过程。
通过在另一个类调用IInitializeDemo2
的静态方法:
这边整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】免费获取
java
代码解读
复制代码
public static void main(String args[]) { InitializeDemo2.m1(); }
可以验证到输出是:
shell
代码解读
复制代码
静态代码块:执行了 k=1, n=10, i=0 静态方法:执行了 k=1, n=10, i=0
说明,静态变量在类加载阶段就已经完成了!
二、对象初始化
在对象创建过程中,核心的过程是初始化,其分为两个主要部分:静态初始化和非静态初始化。
- 静态初始化:只在类第一次加载时执行,用于初始化静态变量和执行静态代码块。
- 非静态初始化:每次创建对象时执行,用于初始化实例变量和执行非静态代码块。
初始化顺序:
- 静态变量和静态代码块:按它们在类中出现的顺序执行,只在类加载时执行一次,在前面已经通过代码得到验证。
- 实例变量和实例代码块:在每次创建对象时执行,执行顺序同样是按照它们在类中出现的顺序。
- 构造函数:实例变量和实例代码块执行后,才会执行构造函数。
示例代码:
java
代码解读
复制代码
public class InitOrder { static { System.out.println("Static Block 1"); } static int x = print("Static Variable x"); static { System.out.println("Static Block 2"); } int y = print("Instance Variable y"); { System.out.println("Instance Block"); } public InitOrder() { System.out.println("Constructor"); } static int print(String message) { System.out.println(message); return 0; } public static void main(String[] args) { new InitOrder(); } }
输出结果:
scss
代码解读
复制代码
Static Block 1 Static Variable x Static Block 2 Instance Variable y Instance Block Constructor
从上面的例子中可以看出,静态块和静态变量按照声明顺序初始化(读者可以调整顺序验证),然后执行实例变量的初始化,接着执行实例块,最后调用构造函数。
三、JVM堆栈内存的分配
JVM在创建对象时,主要使用堆内存、栈内存进行分配:
-
堆内存:用于存储对象实例和数组。每当我们用
new
关键字创建一个对象时,都会在堆中分配内存,其实这是一个动态的过程。 -
栈内存:用于存储方法调用相关的信息,包括方法的参数、局部变量、操作数栈和返回地址。每次方法调用都会在栈中创建一个栈帧(Stack Frame)。
对象创建时的堆、栈内存分配过程:
- 当
new
关键字用于创建对象时,JVM首先会在堆中为该对象分配内存空间,并初始化默认值(如int
默认值为0
,对象引用默认值为null
)。 - 然后,JVM会将对象的引用地址存储在栈中的局部变量表中。
示例代码:
java
代码解读
复制代码
public class MemoryAllocation { public static void main(String[] args) { Example example = new Example(); } }
在这段代码中,new Example()
会在堆中分配 Example
对象的内存,并在栈中存储指向这个对象的引用 example
。当方法执行结束,栈帧被销毁,但堆中的对象只要有引用指向它,就不会被垃圾回收。
对于内存分配,这里本文不计划深入探讨了,计划未来再写篇文章。
四、赋值过程
在Java中,对象的赋值过程包括两个主要部分:默认初始化 和 显式初始化。
- 默认初始化:在对象内存分配后,JVM自动将对象的所有实例变量设置为其默认值。
- 显式初始化:在默认初始化之后,Java按照代码中出现的顺序进行显式初始化。如果有构造函数,则在显式初始化完成后执行构造函数中的赋值操作。
示例代码:
java
代码解读
复制代码
public class ValueAssignment { int i = 5; String s = "Hello"; public ValueAssignment() { i = 10; s = "World"; } public static void main(String[] args) { ValueAssignment va = new ValueAssignment(); System.out.println("i: " + va.i + ", s: " + va.s); } }
输出结果:
yaml
代码解读
复制代码
i: 10, s: World
这里的过程如下:
- 默认初始化:
i
被设置为0
,s
被设置为null
。 - 显式初始化:
i
被赋值为5
,s
被赋值为"Hello"
。 - 构造函数初始化:
i
被重新赋值为10
,s
被重新赋值为"World"
。
结论
对象创建是Java程序中非常基础但复杂的过程,涉及类加载、静态与非静态初始化的顺序、内存分配和赋值等多个方面。理解这些细节对于编写高效、健壮的Java代码至关重要。通过本文的分析和示例代码,我们深入探讨了对象创建的每一个阶段以及它们在JVM中的具体实现方式,为深入掌握Java的内存模型和对象生命周期奠定了基础。