第7条.消除过期的对象引用

1.这段代码中有一个不明显的内存泄漏。如果一个栈先是增长,然后再收缩,那么,从栈中弹出的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。因为栈内部维护着对这些对象的过期引用。过期引用,是指永远也不会再被解除的引用。在本例中,凡是在elements数组的”活动部分”之外的任何引用都是过期的。活动部分是指elements中下标小于size的那些元素。

package com.example.ownlearn;

import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack(){
        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);
    }

这类问题修复的方法很简单:一旦对象引用已经过期,只需清空这些引用即可。

public Object pop(){
        if(size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

只要类时自己管理内存,程序要就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。

2.内存泄漏的另一个常见来源是缓存。一旦把对象引用放到缓存中,就很容易被遗忘掉。如果只要在缓存之外存在对某各项的的引用,该项就有意义,那么可以用WeakHashMap(。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。)代表缓存;当缓存中的项过期之后,它们就会自动被删除。

第8条.避免使用终结方法和清除方法

终结方法(finalizer)通常是不可预测的,一般情况要避免使用,在java9中使用cleaner代替了终结方法,但仍然是不可预测的,一般情况也要避免使用。因为无法保证finalizer、cleaner一定会执行。

如果类的对象中封装的资源(例如文件或者线程)确实需要终止,我们可以让类实现AutoCloseable,并要求其客户端在每个实例不在需要的时候调用一下close方法(close方法必须在一个私有域下记录下“该对象已经不再有效”,如果这些方法是在对象已经终止之后被调用,其他的方法就必须检查这个域,并抛出异常)。

举个栗子:

我们可以看到FileOutputStream类继承了OutputStream类。

public
class FileOutputStream extends OutputStream

我们继续查看OutputStream类,可以看到OutputStream实现了Closeable、Flushable接口。

public abstract class OutputStream implements Closeable, Flushable

我们继续查看Closeable的代码,不难看到Closeable继承了AutoCloseable接口

public interface Closeable extends AutoCloseable

我们查看FileOutputStream 的 close方法,可以看到其中有closed标志位来标志是否被关闭。

private volatile boolean closed = false;  
public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {
                return;
            }
            closed = true;
        }

        if (channel != null) {
            channel.close();
        }

        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               close0();
           }
        });
    }

 

finalize有两种合法用途,一种是作为安全网,当资源的所有者忘记调用它的close方法时,但是在考虑使用这种方法时要考虑是否值得,在FileOutputStream 依然可以找到这样的安全网方法,如下:。

 

protected void finalize() throws IOException {
        if (this.fd != null) {
            if (this.fd != FileDescriptor.out && this.fd != FileDescriptor.err) {
                this.close();
            } else {
                this.flush();
            }
        }

    }

2.用作终止非关键的本地资源。

第9条.try-with-resources优先于try-finally

Java类库中包括许多必须通过调用close方法来手动关闭的资源,例如InputStream。根据经验try-finally语句是确保资源会被适时关闭的最佳方法,就算发生异常返回也一样。

package com.example.ownlearn;

import java.io.*;

public class File {
     static String firstLineOfflineOfFile(String path) throws IOException {
        BufferedReader br  = new BufferedReader(new FileReader("123"));
        try {
            return br.readLine();
        } finally {
            br.close();
        }
    }


}

这样资源被释放掉是没有问题的,但是如果底层物理设备异常,那么调用readLine就会抛出异常,基于同样的原因,调用close也会出现异常。在这种情况下,第二个异常完全抹除第一个异常。在异常堆栈轨迹中,将无法找到第一个异常的记录,在我们排查问题的时候带来困难。

当我们有两个对象需要释放的时候,程序就会变得很混乱。

static void copy(String src,String dst) throws IOException {
       final int BUFFER_SIZE = 100;
       InputStream in = new FileInputStream(src);
       try{
           OutputStream outputStream = new FileOutputStream(dst);
           try{
               byte[] buf = new byte[BUFFER_SIZE];
               int n;
               while ((n=in.read(buf)) >= 0){
                   outputStream.write(buf,0,n);
               }
           } finally {
               outputStream.close();
           }
       }finally {
           in.close();
       }
    }

Java7中引入try-with-resources语句时,这些问题就被解决了。要使用这个构造器的资源,必须先实现AutoCloseable接口,其中包含了单个返回void的close方法,如果我们编写了一个类代表的是必须被关闭的资源,那么这个类也应该实现这个接口。

使用 try-with-resources的第一个范例:

static String firstLineOfflineOfFile(String path) throws IOException {
   
    try (BufferedReader br  = new BufferedReader(new FileReader("123"))){
        return br.readLine();
    } 
}

使用try-with-resources的第二个范例:

static void copy(String src,String dst) throws IOException {
        final int BUFFER_SIZE = 100;
        try(InputStream in = new FileInputStream(src);
             OutputStream outputStream = new FileOutputStream(dst)){
                byte[] buf = new byte[BUFFER_SIZE];
                int n;
                while ((n=in.read(buf)) >= 0){
                    outputStream.write(buf,0,n);
                }
        }
    }

这样如果调用readline和(不可见的)close方法都抛出异常,后一个异常就会被禁止,而且被禁止的异常并不是简单的被抛弃了,而是会被打印在堆栈轨迹中并会注明是被禁止的异常。