第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方法都抛出异常,后一个异常就会被禁止,而且被禁止的异常并不是简单的被抛弃了,而是会被打印在堆栈轨迹中并会注明是被禁止的异常。