避免使用Finalizer和Cleaner机制

This commit is contained in:
尉勇强2 2019-09-17 20:17:31 +08:00
parent d6857ddc4c
commit f74869667a
8 changed files with 353 additions and 1 deletions

View File

@ -0,0 +1,48 @@
## 避免创建不必要的对象
### 示例
- [Item06Example01.java](CreatingAndDestroyingObjects/src/main/java/com/jueee/item06/Item06Example01.java):确定一个字符串是否是一个有效的罗马数字。
### 说明
在每次需要时重用一个对象而不是创建一个新的相同功能对象通常是恰当的。重用可以更快更流行。如果对象是不可变的(条目 17),它总是可以被重用。
通过使用静态工厂方法(static factory methods(项目1),可以避免创建不需要的对象。例如,工厂方法`Boolean.valueOf(String)` 比构造方法`Boolean(String`)更可取后者在Java 9中被弃用。构造方法每次调用时都必须创建一个新对象而工厂方法永远不需要这样做在实践中也不需要。除了重用不可变对象如果知道它们不会被修改还可以重用可变对象。
一些对象的创建比其他对象的创建要昂贵得多。 如果要重复使用这样一个“昂贵的对象”,建议将其缓存起来以便重复使用。
#### 缓存起对象以便重复使用
**示例**[Item06Example01.java](CreatingAndDestroyingObjects/src/main/java/com/jueee/item06/Item06Example01.java):确定一个字符串是否是一个有效的罗马数字
如果包含`isRomanNumeral`方法的改进版本的类被初始化,但该方法从未被调用,则`ROMAN`属性则没必要初始化。 在第一次调用`isRomanNumeral`方法时,可以通过延迟初始化( lazily initializing属性条目 83来排除初始化但一般不建议这样做。 延迟初始化常常会导致实现复杂化,而性能没有可衡量的改进(条目 67
#### 自动装箱
自动装箱autoboxing它允许程序员混用基本类型和包装的基本类型根据需要自动装箱和拆箱。
自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。 有微妙的语义区别和不那么细微的性能差异(条目 61
**示例**[Item06Example02.java](CreatingAndDestroyingObjects/src/main/java/com/jueee/item06/Item06Example02.java):计算所有正整数的总和
```
2305843008139952128
UseTime1:10817
2305843008139952128
UseTime2:1405
```
`sumBad()` 由于写错了一个字符,运行的结果要比实际慢很多。变量`sum`被声明成了`Long`而不是`long`这意味着程序构造了大约231不必要的`Long`实例(大约每次往`Long`类型的 `sum`变量中增加一个`long`类型构造的实例)。
`sumGood()` 把`sum`变量的类型由`Long`改为`long`在我的机器上运行时间从10.8 秒降低到0.14秒。
这个教训很明显:**优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱**。
#### 维护自己的对象池
除非池中的对象非常重量级,否则通过维护自己的对象池来避免对象创建是一个坏主意。
对象池的典型例子就是数据库连接。建立连接的成本非常高因此重用这些对象是有意义的。但是一般来说维护自己的对象池会使代码混乱增加内存占用并损害性能。现代JVM实现具有高度优化的垃圾收集器它们在轻量级对象上轻松胜过此类对象池。
这个条目的对应点是针对条目 50的防御性复制defensive copying。 目前的条目说:“当你应该重用一个现有的对象时,不要创建一个新的对象”,而条目 50说“不要重复使用现有的对象当你应该创建一个新的对象时。”请注意重用防御性复制所要求的对象所付出的代价要远远大于不必要地创建重复的对象。 未能在需要的情况下防御性复制会导致潜在的错误和安全漏洞;而不必要地创建对象只会影响程序的风格和性能。

View File

@ -0,0 +1,40 @@
## 消除过期的对象引用
**示例代码**[Item07Example01.java](CreatingAndDestroyingObjects/src/main/java/com/jueee/item07/Item07Example01.java):堆栈实现。
`StackBad` 类没有什么明显的错误(但是对于泛型版本,请参阅条目 29。 你可以对它进行详尽的测试,它都会成功地通过每一项测试。
但它有一个潜在的问题。 笼统地说,程序有一个“内存泄漏”,由于垃圾回收器的活动的增加,或内存占用的增加,静默地表现为性能下降。 在极端的情况下,这样的内存泄漏可能会导致磁盘分页( disk paging甚至导致内存溢出OutOfMemoryError的失败但是这样的故障相对较少。
那么哪里发生了内存泄漏? 如果一个栈增长后收缩,那么从栈弹出的对象不会被垃圾收集,即使使用栈的程序不再引用这些对象。 这是因为栈维护对这些对象的过期引用( obsolete references。 过期引用简单来说就是永远不会解除的引用。 在这种情况下元素数组“活动部分active portion”之外的任何引用都是过期的。 活动部分是由索引下标小于size的元素组成。
垃圾收集语言中的内存泄漏(更适当地称为无意的对象保留 unintentional object retentions是隐蔽的。 如果无意中保留了对象引用,那么不仅这个对象排除在垃圾回收之外,而且该对象引用的任何对象也是如此。 即使只有少数对象引用被无意地保留下来,也可以阻止垃圾回收机制对许多对象的回收,这对性能产生很大的影响。
这类问题的解决方法很简单:一旦对象引用过期,将它们设置为 null。 在我们的`Stack`类的情景下,只要从栈中弹出,元素的引用就设置为过期。 `pop`方法的修正版本如下所示:
```java
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 设置元素过期!
return result;
}
```
取消过期引用的另一个好处是,如果它们随后被错误地引用,程序立即抛出`NullPointerException`异常,而不是悄悄地做继续做错误的事情。尽可能快地发现程序中的错误是有好处的。
#### 清空对象引用应该是例外而不是规范
那么什么时候应该清空一个引用呢?`Stack`类的哪个方面使它容易受到内存泄漏的影响简单地说它管理自己的内存。存储池storage pool由`elements`数组的元素组成(对象引用单元,而不是对象本身)。数组中活动部分的元素(如前面定义的)被分配,其余的元素都是空闲的。垃圾收集器没有办法知道这些;对于垃圾收集器来说,`elements`数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。程序员可以向垃圾收集器传达这样一个事实,一旦数组中的元素变成非活动的一部分,就可以手动清空这些元素的引用。
一般来说,**当一个类自己管理内存时,程序员应该警惕内存泄漏问题**。 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。
**另一个常见的内存泄漏来源是缓存**。一旦将对象引用放入缓存中很容易忘记它的存在并且在它变得无关紧要之后仍然保留在缓存中。对于这个问题有几种解决方案。如果你正好想实现了一个缓存只要在缓存之外存在对某个项entry的键key引用那么这项就是明确有关联的就可以用`WeakHashMap`来表示缓存这些项在过期之后自动删除。记住只有当缓存中某个项的生命周期是由外部引用到键key而不是值value决定时`WeakHashMap`才有用。
更常见的情况是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得越来越没有价值。在这种情况下,缓存应该偶尔清理掉已经废弃的项。这可以通过一个后台线程(也许是`ScheduledThreadPoolExecutor`)或将新的项添加到缓存时顺便清理。`LinkedHashMap`类使用它的`removeEldestEntry`方法实现了后一种方案。对于更复杂的缓存,可能直接需要使用`java.lang.ref`。
第三个常见的内存泄漏来源是**监听器和其他回调**。如果你实现了一个API其客户端注册回调但是没有显式地撤销注册回调除非采取一些操作否则它们将会累积。确保回调是垃圾收集的一种方法是只存储弱引用weak references例如仅将它们保存在`WeakHashMap`的键key中。
因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器( heap profiler的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。

View File

@ -0,0 +1,34 @@
## 避免使用Finalizer和Cleaner机制
Finalizer机制是不可预知的往往是危险的而且通常是不必要的。 它们的使用会导致不稳定的行为,糟糕的性能和移植性问题。 Finalizer机制有一些特殊的用途我们稍后会在这个条目中介绍但是通常应该避免它们。 从Java 9开始Finalizer机制已被弃用但仍被Java类库所使用。 Java 9中 Cleaner机制代替了Finalizer机制。 Cleaner机制不如Finalizer机制那样危险但仍然是不可预测运行缓慢并且通常是不必要的。
Finalizer和Cleaner机制的一个缺点是不能保证他们能够及时执行。 在一个对象变得无法访问时到Finalizer和Cleaner机制开始运行时这期间的时间是任意长的。 这意味着你永远不应该Finalizer和Cleaner机制做任何时间敏感time-critical的事情。 例如依赖于Finalizer和Cleaner机制来关闭文件是严重的错误因为打开的文件描述符是有限的资源。 如果由于系统迟迟没有运行Finalizer和Cleaner机制而导致许多文件被打开程序可能会失败因为它不能再打开文件了。
及时执行Finalizer和 Cleaner机制是垃圾收集算法的一个功能这种算法在不同的实现中有很大的不同。程序的行为依赖于Finalizer和 Cleaner机制的及时执行其行为也可能大不不同。 这样的程序完全可以在你测试的JVM上完美运行然而在你最重要的客户的机器上可能运行就会失败。
延迟终结finalization不只是一个理论问题。为一个类提供一个Finalizer机制可以任意拖延它的实例的回收。一位同事调试了一个长时间运行的GUI应用程序这个应用程序正在被一个OutOfMemoryError错误神秘地死掉。分析显示在它死亡的时候应用程序的Finalizer机制队列上有成千上万的图形对象正在等待被终结和回收。不幸的是Finalizer机制线程的运行优先级低于其他应用程序线程所以对象被回收的速度低于进入队列的速度。语言规范并不保证哪个线程执行Finalizer机制因此除了避免使用Finalizer机制之外没有轻便的方法来防止这类问题。在这方面 Cleaner机制比Finalizer机制要好一些因为Java类的创建者可以控制自己cleaner机制的线程但cleaner机制仍然在后台运行在垃圾回收器的控制下运行但不能保证及时清理。
Java规范不能保证Finalizer和Cleaner机制能及时运行它甚至不能能保证它们是否会运行。当一个程序结束后一些不可达对象上的Finalizer和Cleaner机制仍然没有运行。因此不应该依赖于Finalizer和Cleaner机制来更新持久化状态。例如依赖于Finalizer和Cleaner机制来释放对共享资源(如数据库)的持久锁,这是一个使整个分布式系统陷入停滞的好方法。
不要相信`System.gc`和`System.runFinalization`方法。 他们可能会增加Finalizer和Cleaner机制被执行的几率但不能保证一定会执行。 曾经声称做出这种保证的两个方法:`System.runFinalizersOnExit`和它的孪生兄弟`Runtime.runFinalizersOnExit`,包含致命的缺陷,并已被弃用了几十年[ThreadStop]。
Finalizer机制的另一个问题是在执行Finalizer机制过程中未捕获的异常会被忽略并且该对象的Finalizer机制也会终止。未捕获的异常会使其他对象陷入一种损坏的状态corrupt state。如果另一个线程试图使用这样一个损坏的对象可能会导致任意不确定的行为。通常情况下未捕获的异常将终止线程并打印堆栈跟踪 stacktrace但如果发生在Finalizer机制中则不会发出警告。Cleaner机制没有这个问题因为使用Cleaner机制的类库可以控制其线程。
使用finalizer和cleaner机制会导致严重的性能损失。 在我的机器上,创建一个简单的`AutoCloseable`对象使用try-with-resources关闭它并让垃圾回收器回收它的时间大约是12纳秒。 使用finalizer机制而时间增加到550纳秒。 换句话说使用finalizer机制创建和销毁对象的速度要慢50倍。 这主要是因为finalizer机制会阻碍有效的垃圾收集。 如果使用它们来清理类的所有实例(在我的机器上的每个实例大约是500纳秒)那么cleaner机制的速度与finalizer机制的速度相当但是如果仅将它们用作安全网 safety net则cleaner机制要快得多如下所述。 在这种环境下创建清理和销毁一个对象在我的机器上需要大约66纳秒这意味着如果你不使用安全网的话需要支付5倍(而不是50倍)的保险。
finalizer机制有一个严重的安全问题它们会打开你的类来进行finalizer机制攻击。finalizer机制攻击的想法很简单如果一个异常是从构造方法或它的序列化中抛出的——`readObjec`t和`readResolve`方法(第12章)——恶意子类的finalizer机制可以运行在本应该“中途夭折died on the vine”的部分构造对象上。finalizer机制可以在静态字属性记录对对象的引用防止其被垃圾收集。一旦记录了有缺陷的对象就可以简单地调用该对象上的任意方法而这些方法本来就不应该允许存在。从构造方法中抛出异常应该足以防止对象出现而在finalizer机制存在下则不是。这样的攻击会带来可怕的后果。Final类不受finalizer机制攻击的影响因为没有人可以编写一个final类的恶意子类。为了保护非final类不受finalizer机制攻击编写一个final的`finalize`方法,它什么都不做。
那么,你应该怎样做呢?为对象封装需要结束的资源(如文件或线程)而不是为该类编写Finalizer和Cleaner机制让你的类实现`AutoCloseable`接口即可并要求客户在在不再需要时调用每个实例close方法通常使用try-with-resources确保终止即使面对有异常抛出情况条目 9。一个值得一提的细节是实例必须跟踪是否已经关闭close方法必须记录在对象里不再有效的属性其他方法必须检查该属性如果在对象关闭后调用它们则抛出IllegalStateException异常。
那么Finalizer和Cleaner机制有什么好处呢它们可能有两个合法用途。一个是作为一个安全网safety net以防资源的拥有者忽略了它的`close`方法。虽然不能保证Finalizer和Cleaner机制会迅速运行(或者根本就没有运行)最好是把资源释放晚点出来也要好过客户端没有这样做。如果你正在考虑编写这样的安全网Finalizer机制请仔细考虑一下这样保护是否值得付出对应的代价。一些Java库类如`FileInputStream`、`FileOutputStream`、`ThreadPoolExecutor`和`java.sql.Connection`都有作为安全网的Finalizer机制。
第二种合理使用Cleaner机制的方法与本地对等类native peers有关。本地对等类是一个由普通对象委托的本地(非Java)对象。由于本地对等类不是普通的 Java对象所以垃圾收集器并不知道它当它的Java对等对象被回收时本地对等类也不会回收。假设性能是可以接受的并且本地对等类没有关键的资源那么Finalizer和Cleaner机制可能是这项任务的合适的工具。但如果性能是不可接受的或者本地对等类持有必须迅速回收的资源那么类应该有一个`close`方法,正如前面所述。
#### Cleaner机制示例
**示例代码**[Item08Example01.java](CreatingAndDestroyingObjects/src/main/java/com/jueee/item08/Item08Example01.java):假设`Room`对象必须在被回收前清理干净。
`Room`类实现`AutoCloseable`接口它的自动清理安全网使用的是一个Cleaner机制这仅仅是一个实现细节。
Cleaner机制的规范说“`System.exit`方法期间的清理行为是特定于实现的。 不保证清理行为是否被调用。”虽然规范没有说明,但对于正常的程序退出也是如此。 在我的机器上,将`System.gc()`方法添加到`Teenager`类的`main`方法足以让程序退出之前打印`Cleaning room`,但不能保证在你的机器上会看到相同的行为。

View File

@ -20,10 +20,12 @@
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- https://projectlombok.org/changelog -->
<!-- v1.18.4:PLATFORM: Many improvements for lombok's JDK10/11 support. -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.6</version>
<version>1.18.10</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,23 @@
package com.jueee.item06;
import java.util.regex.Pattern;
public class Item06Example01 {
}
class RomanNumeralsBad {
public static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
}
// 重用昂贵的对象以提高性能
class RomanNumeralsGood {
private static final Pattern ROMAN =
Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}

View File

@ -0,0 +1,29 @@
package com.jueee.item06;
public class Item06Example02 {
// 非常慢 你能发现对象的创建吗
private static long sumBad() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
private static long sumGood() {
long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
public static void main(String[] args) {
long start1 = System.currentTimeMillis();
System.out.println(sumBad());
System.out.println("UseTime1:" + (System.currentTimeMillis() - start1));
long start2 = System.currentTimeMillis();
System.out.println(sumGood());
System.out.println("UseTime2:" + (System.currentTimeMillis() - start2));
}
}

View File

@ -0,0 +1,87 @@
package com.jueee.item07;
import java.util.Arrays;
import java.util.EmptyStackException;
public class Item07Example01 {
public static void main(String[] args) {
StackBad stack = new StackBad();
stack.push("a");
stack.push("b");
stack.push("c");
System.out.println(stack.pop());
System.out.println(stack.pop());
StackGood stack2 = new StackGood();
stack2.push("a");
stack2.push("b");
stack2.push("c");
System.out.println(stack2.pop());
System.out.println(stack2.pop());
}
}
// 堆栈实现
class StackBad {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public StackBad() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* 确保至少有一个元素的空间每次阵列需要增长时容量大致加倍
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
//堆栈实现
class StackGood {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public StackGood() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 设置元素过期
return result;
}
/**
* 确保至少有一个元素的空间每次阵列需要增长时容量大致加倍
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}

View File

@ -0,0 +1,89 @@
package com.jueee.item08;
import java.lang.ref.Cleaner;
public class Item08Example01 {
// Cleaner机制将调用State的run方法
public static void Adult() {
try (Room myRoom = new Room(7)) {
System.out.println("Goodbye");
}
}
public static void Teenager() {
new Room(99);
System.out.println("Peace out");
}
public static void NoClean() {
Room myRoom = new Room(99);
System.out.println("Peace out");
}
// 通过调用Room的close方法内调用Cleanable的clean方法来触发State类的run()方法
public static void Clean() {
Room myRoom = new Room(99);
myRoom.close();
System.out.println("Peace out");
}
public static void main(String[] args) {
System.out.println("Adult:");
Adult();
System.out.println("-----");
System.out.println("Teenager:");
Teenager();
System.out.println("-----");
System.out.println("NoClean:");
NoClean();
System.out.println("-----");
System.out.println("Clean:");
Clean();
}
}
// 使用 Cleaner 作为安全网的自动关闭类
class Room implements AutoCloseable {
// Cleaner 类位于 JDK 9java.lang.ref.Cleaner
private static final Cleaner cleaner = Cleaner.create();
// 静态内部State类拥有Cleaner机制清理房间所需的资源
// State类实现了Runnable接口其run方法最多只能调用一次只能被我们在Room构造方法中用Cleaner机制注册State实例时得到的Cleanable调用
private static class State implements Runnable {
int numJunkPiles; // 代表混乱房间的数量
State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}
// Invoked by close method or cleaner
@Override
public void run() {
System.out.println("Cleaning room");
numJunkPiles = 0;
}
}
// The state of this room, shared with our cleanable
private final State state;
// Our cleanable. Cleans the room when its eligible for gc
private final Cleaner.Cleanable cleanable;
public Room(int numJunkPiles) {
state = new State(numJunkPiles);
cleanable = cleaner.register(this, state);
}
@Override
public void close() {
cleanable.clean();
}
}