一、概述
在Java中,一个对象只要被正确的实例化之后才能被使用,在对象实例化的时候,会先检查相关的类信息是否已经被加载并初始化,在类初始化完毕之后才会继续完成对象的实例化,类的一生主要经历加载、连接(验证、准备、解析)、初始化、使用和卸载五个过程,创建类的对象就是在使用这个阶段。
加载(Loading):通过类的全限定名查找和读取class文件,将读取的字节流所代表的静态存储结构转化为方法区的运行时数据结构,然后在内存中生成一个代表这个类的Class对象,作为方法区这个类的访问入口,并将这个Class对象存放在方法区中(方法区主要存储类信息、常量、静态变量)。这个过程可以通过自定义的类加载器进行操作。
连接(Linking):把类的二进制数据合并入JRE中,这个过程包括三个阶段:
- 验证:该阶段验证被加载后的类的结构是否正确,类数据是否符合虚拟机的规定,保证该类不会危险到虚拟机的安全。
- 准备:验证完成之后立即为类的静态变量(static)在方法区分配内存,并给变量赋默认值(o、false、0.0f、’’、null或指定的值等),比如
static boolean flag = true;
,在准备阶段就会给变量flag赋默认值false,然后在初始化阶段将变量flag赋值于true;但是对于静态常量(static final)来说,会在此阶段将常量赋予最终值,比如static final int num = 10;
,在该阶段之后,静态常量num的值就一直是10了。- 解析:将类的二进制数据中的符号引用转换为直接饮用。符号引用是以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关,引用的目标不一定已经存在于内存中;直接饮用是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄,与虚拟机实现的内存布局有关,引用的目标必定已经存在于内存中。
初始化(Initialization):类初始化的主要工作是为静态变量赋代码设定的值,如上面讲到的
static boolean flag = true;
,在连接的准备阶段,静态变量flag的值被设置为false,但是在初始化阶段会将flag的值设置为true,也就是我们代码中设定的初始值。初始化节点会调用类构造器<cInit>(),该构造器是由编译器自动收集类中的所有类变量的赋值和静态代码块中的语句合并生成的,编译器收集的顺序由代码语句在源文件中的顺序决定的。☆除了加载阶段可以用户自定义之外,其他阶段的动作完全由虚拟机主导控制。
二、对象的创建方式
在Java代码中可以通过多种方式完成对象的创建,最常用也是最直观的一种方式就是通过new关键字来调用类的构造器显示的创建对象,在Java规范中称此方式为:由执行类实例创建表达式而引起的对象创建。另外我们还可以通过使用反射、clone方法、序列化等方式去创建对象。
-
使用new关键字
该方式是我们最常见也是最简单的一种方式,我们可以调用一个类的任意构造器(无参或有参)去创建对象。
1
2
3Cc c = new Cc();
或
Cc c = new Cc("cc", 3);当虚拟机执行到new指令时,首先在常量池中查找Cc的符号引用,若能定位到Cc类的符号引用,说明这个类已经被加载到方法区了,若没有找到则先去加载Cc这个类。
-
使用反射
-
使用Class类的newInstance方法
可以利用反射机制通过调用Class类的newInstance方法来创建对象,并且这个newInstance方法调用的是目标类无参的构造器,所以要是想使用该方式创建某个类的对象,就必须要保证这个类要有一个无参的构造器,否则会报错
NoSuchMethodException: cc.kevinlu.ccspringbootwar.Cc.<init>()
,1
2
3Cc c = (Cc) Class.forName("cc.lu.clazz.ni.Cc").newInstance();
或
Cc cc = Cc.class.newInstance();我们会发现在使用
Class.forName().newInstance()
的时候需要进行一次强制类型转换,Class类的newInstance方法内部也是通过调用Constructor的newInstance无参构造器。 -
使用Constructor类的newInstance方法
java.lang.reflect.Constructor
类中也有一个newInstance方法可以用来创建对象,该方法与Class类的newInstance方法类似,但是Constructor中的newInstance方法更加强大,因为它既可以调用无参的构造器也可以调用有参数的构造器,通吃。并且Constructor是一个泛型类,不需要我们再进行对象的强制类型转换。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
// 调用无参构造器
Constructor<Cc> c1 = Cc.class.getConstructor();
Cc cc1 = c1.newInstance();
System.out.println(cc1);
// 调用有参构造器
Constructor<Cc> constructor = Cc.class.getConstructor(String.class);
Cc ccc = constructor.newInstance("cc");
System.out.println(ccc);
}
}
class Cc {
private String name;
// 无参构造器
public Cc() {
}
// 有参构造器
public Cc(String name) {
this.name = name;
}
public String toString() {
return "Cc{" + "name='" + name + '\'' + '}';
}
}
-
-
使用clone方法
我们可以调用一个对象的clone方法进行对象的深拷贝或浅拷贝,并且如果我们想要使用clone方法,必须实现接口
java.lang.Cloneable
,这是一个标识性质的接口,告诉虚拟机这个类的实例对象支持克隆,就像接口java.io.Serializable
用来标识类的实例对象可以被序列化一样,没有实现Cloneable的类的对象在调用clone方法的时候会报java.lang.CloneNotSupportedException
异常。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
Constructor<Cc> constructor = Cc.class.getConstructor(String.class);
Cc c = constructor.newInstance("cc");
System.out.println(c);
Cc cc = (Cc) c.clone();
System.out.println(cc);
}
}
/**
* 重点:必须实现java.lang.Cloneable接口
*/
class Cc implements Cloneable {
private String name;
public Cc() {
}
public Cc(String name) {
this.name = name;
}
public String toString() {
return "Cc{" + "name='" + name + '\'' + '}';
}
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
} -
使用序列化
当我们反序列化一个对象时,虚拟机会帮我们创建一个单独的对象,过程中不会调用任何的构造器,被序列化和反序列化的类必须实现接口
java.io.Serializable
,目的是告诉虚拟机这个类的实例对象可以被序列化和反序列化。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41import java.io.*;
import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
Constructor<Cc> constructor = Cc.class.getConstructor(String.class);
Cc c = constructor.newInstance("cc");
System.out.println(c);
// 序列化对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc.bf"));
oos.writeObject(c);
oos.close();
// 反序列化对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc.bf"));
Cc cc = (Cc) ois.readObject();
System.out.println(cc);
}
}
class Cc implements Serializable {
private String name;
public Cc() {
}
public Cc(String name) {
this.name = name;
}
public String toString() {
return "Cc{" + "name='" + name + '\'' + '}';
}
}
完整代码:
1 | package cc.lu.clazz.ni; |
三、对象的创建过程
-
实例变量与实例代码块的初始化
对象被创建时,虚拟机为其分配内存来存放实例变量(自己的和继承而来的),在分配内存的同时会对实例变量进行默认值设置,默认值有两种设置方式,一种是直接赋值,另一种是使用实例代码块赋值,编译后查看字节码会发现实例代码块会按照声明的顺序对实例变量赋值操作去重之后,将最后实例代码块中的语句放到类的构造器中,例如下方代码,最终实例变量的值为:name=“cc”,sex=“男1”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Cc {
private String name;
private String sex;
{
sex = "男";
name = "cc";
}
{
sex = "男1";
}
public Cc() {
}
}实例代码块中如果使用一个实例变量赋值给另外一个实例变量的话,那么作为值的这个实例变量的声明必须要在实例代码块之前,否则会在编译的时候就报错
Illegal forward reference
,例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Cc {
private String name;
{
name = "cc";
}
{
// 此处sex1会报错
sex = sex1;
}
private String sex;
private String sex1;
public Cc() {
}
}如果我们想要绕过这种检查,可以创建sex1的getter方法,在代码块中调用这个getter方法,但是会出现另外一种情况:sex会获取到sex1在连接阶段设置的默认值,所以慎用。
-
构造器初始化
Java的类是具有继承关系的,所有类的超类都是Object,在字节码中,构造器会被命名为<init>()方法,参数和代码中声明的一致,没有我们没有在类中显式定义构造器的话,虚拟机会为类创建一个默认的无参构造器。Java规则规定在实例化类之前,必须先要实例化其父类,以保证实例的完整性。Java强制要求除Object之外的所有类的构造器的第一句必须是父类的构造器调用语句(super(…)),如果把super()语句放到其他行,那么编译的时候就会提示错误
Call to 'super()' must be first statement in constructor body
。我们分析一个情景:看如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Cc {
private String name;
public Cc() {
// ① super();
this("cc");
}
public Cc(String name) {
// ②
super();
this.name = name;
}
}对于这种情况,Java只允许在构造器Cc(String name)内调用超类的构造器,①处的代码会报错。
四、小结
类实例化的过程可以总结为:
- 首先在常量池中定位类的符号引用
- 判定是否需要进行类加载
- 虚拟机为新生对象分配内存(对象所需要的内存大小在类加载完就可以确定)
- 初始化实例变量
- 虚拟机设置对象头信息
- 执行代码块进行自定义初始化
- 完成
五、对象创建过程中内存的分配
文字理解起来不如看图,直接看图理解吧: