对象生命周期
导航 类和对象 主题: ) |
在创建 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
对象引用在 {
}
内具有作用域。在 }
之后,该对象将成为垃圾。对象引用可以传递到方法中,也可以从方法中返回。
使用 new
关键字创建对象
[edit | edit source]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
变量只存储一次,无论创建多少个对象。当创建对象时,会为对象成员变量分配内存。因此,对象的尺寸不是由其代码的尺寸决定的,而是由它存储成员变量所需的内存决定的。
通过克隆对象创建对象
[edit | edit source]克隆功能不是所有类都自动具备的。不过有一些帮助,因为所有 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
,因为在 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 目录中,会发生什么?
- 如何在应用程序服务器的同一实例中同时使用相同实用程序库的两个不同版本?
- 我当前使用的是哪个版本的实用程序类?
- 为什么我需要以这种方式处理所有这些类加载问题?