对象生命周期
导航 类和对象 主题: ) |
在创建 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()
方法。此基本方法会分配内存并逐位复制对象的状态。
您可能会问为什么我们需要这个 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
,因为它是protected
在Object
类中。 - 在您的
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;
}
}
|
请注意,只有可变对象需要克隆。可以对克隆对象使用对不可变对象的引用(例如字符串),而无需担心。
重新创建从远程源接收到的对象
[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
目录中的某个位置,会发生什么情况? - 如何在应用程序服务器的同一实例中同时使用同一实用库的两个不同版本?
- 我当前使用的是实用库的哪个版本?
- 为什么我需要处理所有这些类加载问题?