对象生命周期
导航 类和对象 主题: ) |
在创建 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()
方法,因此可以获得一些帮助。此基本方法会分配内存,并逐位复制对象的狀態。
您可能会问为什么要使用这种克隆方法。难道不能创建一个构造函数,传入同一个对象,然后逐个变量进行复制吗?例如(请注意,访问 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目录中,会发生什么?
- 如何在应用程序服务器的同一实例中同时使用相同实用程序库的两个不同版本?
- 我目前正在使用哪个版本的实用程序类?
- 为什么我需要更改所有这些类加载内容?