对象生命周期
导航 类和对象 主题: ) |
在创建 Java 对象之前,必须从文件系统(扩展名为 .class
)加载类的字节码到内存。这个过程称为类加载,它包括找到给定类名的字节码并将其转换为 Java 类 实例。对于每种类型的 Java 类,都会创建一个类。
Java 程序中的所有对象都在堆内存中创建。对象是根据其类创建的。您可以将类视为创建对象的蓝图、模板或描述。创建对象时,会分配内存来保存对象的属性。还会创建一个指向该内存位置的对象引用。要将来使用该对象,必须将该对象引用存储为局部变量或对象成员变量。
Java 虚拟机 (JVM) 会跟踪对象引用的使用情况。如果不再存在指向该对象的引用,则该对象将不再可用并成为垃圾。一段时间后,堆内存将充满未使用的对象。JVM 会收集这些垃圾对象并释放它们分配的内存,以便在创建新对象时可以再次使用该内存。请参见以下简单示例
代码部分 4.30: 对象创建。
{
// Create an object
MyObject obj = new MyObject();
// Use the object
obj.printMyValues();
}
|
obj
变量包含指向从 MyObject
类创建的对象的对象引用。obj
对象引用在 {
}
内的范围内。在 }
之后,该对象将成为垃圾。对象引用可以传递给方法,也可以从方法返回。
99% 的新对象都是使用 new
关键字创建的。
代码清单 4.13: MyProgram.java
public class MyProgram {
public static void main(String[] args) {
// Create 'MyObject' for the first time the application is started
MyObject obj = new MyObject();
}
}
|
当第一次从 MyObject
类创建对象时,JVM 会在文件系统中搜索类的定义,也就是 Java 字节码。该文件扩展名为 *.class
。CLASSPATH 环境变量包含存储 Java 类的位置。JVM 正在查找 MyObject.class
文件。根据类所属的包,包名将被转换为目录路径。
找到 MyObject.class
文件后,JVM 的类加载器会将该类加载到内存中,并创建一个 java.lang.Class 对象。JVM 会将代码存储在内存中,为 static
变量分配内存,并执行任何静态初始化块。此时不会为对象成员变量分配内存,只有在创建类的实例(对象)时才会为它们分配内存。
可以从同一类创建的对象数量没有限制。代码和 static
变量仅存储一次,无论创建了多少个对象。在创建对象时,会为对象成员变量分配内存。因此,对象的尺寸不是由代码的尺寸决定的,而是由存储其成员变量所需的内存决定的。
克隆功能不是类的自动功能。但有一些帮助,因为所有 Java 对象都继承了 protected Object clone()
方法。此基本方法将分配内存并逐位复制对象的各个状态。
您可能会问为什么要使用这个克隆方法。我们不能创建一个构造函数,传入同一个对象,并逐个变量进行复制吗?例如(请注意,访问 obj
的私有 memberVar
变量是合法的,因为它们在同一个类中)
代码清单 4.14: MyObject.java
public class MyObject {
private int memberVar;
...
MyObject(MyObject obj) {
this.memberVar = obj.memberVar;
...
}
...
}
|
此方法有效,但使用 new
关键字创建对象很耗时。clone()
方法会一次性复制整个对象的内存,这比使用 new 关键字并复制每个变量快得多,因此,如果您需要创建许多相同类型的对象,则使用克隆来创建第一个对象,再从它克隆新的对象,性能会更好。请参见以下使用克隆返回新对象的工厂方法。
代码部分 4.31: 对象克隆。
HashTable cacheTemplate = new HashTable();
...
/** Clone Customer object for performance reason */
public Customer createCustomerObject() {
// See if a template object exists in our cache
Customer template = cacheTemplate.get("Customer");
if (template == null) {
// Create template
template = new Customer();
cacheTemplate.put("Customer", template);
}
return template.clone();
}
|
现在,让我们看看如何使 Customer 对象可克隆。
- 首先,
Customer
类需要实现Cloneable
接口。 - 覆盖并使
clone()
方法为public
,因为在Object类中它是protected
。 - 在您的
clone
方法的开头调用super.clone()
方法。 - 覆盖
Customer
的所有子类中的clone()
方法。
代码清单 4.15: Customer.java
public class Customer implements Cloneable {
...
public Object clone() throws CloneNotSupportedException {
Object obj = super.clone();
return obj;
}
}
|
在代码清单 4.15中,我们使用克隆来加快对象创建速度。克隆的另一个用途可能是对一个会随时间变化的对象进行快照。假设我们想将Customer对象存储在集合中,但我们希望将其与'实时'对象分离。因此,在添加对象之前,我们克隆它们,这样如果原始对象从那时起发生变化,添加的对象就不会发生变化。另外,假设Customer对象引用了一个包含客户活动的Activity对象。现在我们面临一个问题,仅仅克隆Customer对象是不够的,我们还需要克隆被引用的对象。解决方案
- 使Activity类也可克隆
- 确保如果Activity类有其他'可变'对象引用,这些引用也必须被克隆,如下所示
- 更改Customer类的
clone()
方法,如下所示
代码清单 4.16: Customer.java
public class Customer implements Cloneable {
Activity activity;
...
public Customer clone() throws CloneNotSupportedException {
Customer clonedCustomer = (Customer) super.clone();
// Clone the object referenced objects
if (activity != null) {
clonedCustomer.setActivity((Activity) activity.clone());
}
return clonedCustomer;
}
}
|
请注意,只有可变对象需要被克隆。对不可变对象(如String)的引用可以在克隆对象中使用,无需担心。
重新创建从远程源接收的对象
[edit | edit source]当对象通过网络发送时,需要在接收主机上重新创建该对象。
- 对象序列化
- 术语对象序列化指的是将对象转换为字节流的过程。字节流可以存储在文件系统中,也可以通过网络发送。
- 稍后可以从该字节流重新创建对象。唯一的要求是,在序列化对象和重新创建对象时,必须使用相同的类。如果这发生在不同的服务器上,那么两台服务器上必须存在相同的类。相同类意味着必须存在完全相同的类版本,否则无法重新创建对象。对于那些使用java序列化来使对象持久化或通过网络发送对象的应用程序来说,这是一个维护问题。
- 当类被修改时,可能会出现使用早期版本的类序列化的对象无法重新创建的问题。
Java使用Serializable
接口内置支持序列化;但是,类必须首先实现Serializable
接口。
默认情况下,当类被转换为数据流时,它所有的字段都会被序列化(transient
字段会被跳过)。如果除了写入所有字段的默认操作之外还需要额外的处理,则需要为以下三个方法提供实现
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
如果对象需要在序列化期间写入或提供替换对象,则需要实现以下两个方法,可以使用任何访问说明符
Object writeReplace() throws ObjectStreamException;
Object readResolve() throws ObjectStreamException;
通常,对类的微小更改会导致序列化失败。您仍然可以通过定义序列化版本 ID 来允许加载该类
代码段 4.32:序列化版本 ID。
private static final long serialVersionUID = 42L;
|
销毁对象
[edit | edit source]与许多其他面向对象编程语言不同,Java执行自动垃圾收集——任何未被引用的对象都会自动从内存中删除——并且禁止用户手动销毁对象。
finalize()
[edit | edit source]当对象被垃圾回收时,程序员可能希望手动执行清理,例如关闭任何打开的输入/输出流。要实现这一点,可以使用finalize()
方法。请注意,finalize()
不应该手动调用,除非从派生类的finalize方法中调用超类的finalize方法。此外,我们不能依赖于何时调用finalize()
方法。如果Java应用程序在对象被垃圾回收之前退出,则可能永远不会调用finalize()
方法。
代码段 4.33:终结。
protected void finalize() throws Throwable {
try {
doCleanup(); // Perform some cleanup. If it fails for some reason, it is ignored.
} finally {
super.finalize(); // Call finalize on the parent object
}
}
|
垃圾回收器线程的优先级低于其他线程。如果应用程序比垃圾回收器更快地创建对象,则程序可能会耗尽内存。
只有当需要清理超出Java虚拟机直接控制范围内的资源时,才需要finalize方法。特别是,无需显式关闭OutputStream,因为OutputStream在被终结时会自行关闭。相反,finalize方法用于释放类控制的本地或远程资源。
类加载
[edit | edit source]编写热重新部署应用程序的开发人员最关注的问题之一是了解类加载的工作原理。类加载机制的内部机制包含了对以下问题的答案:
- 如果我将更新版本的实用程序库打包到我的应用程序中,而旧版本的相同库仍然存在于服务器的lib目录中的某个位置,会发生什么?
- 如何在应用程序服务器的同一个实例中同时使用相同实用程序库的两个不同版本?
- 我当前使用的是哪个版本的实用程序类?
- 为什么我需要以这种方式使用所有这些类加载内容?