目录
这篇文章将第二章的7、8、9条进行了合并,总结一下在处理资源销毁时的注意事项。
资源的分类
要弄明白资源销毁的方法,首先要知道对于内存资源和非内存资源采用的是不同的销毁方式。那就有必要先了解一下什么是内存资源和非内存资源。
内存资源
内存资源只要指的是程序运行时所占用的系统内存空间,比如:
-
堆内存(Heap Memory):用于动态分配的对象实例存储区域,如创建的类实例、数组等。Java等高级语言中的new操作通常就是在堆上分配内存。
-
栈内存(Stack Memory):存储函数调用时的局部变量、函数参数和返回地址等。栈内存的分配和释放通常由编译器自动管理,速度快且高效。
-
静态内存(Static Memory):用于存储全局变量和静态变量的空间,这部分内存通常在程序启动时分配,并在整个程序运行期间保持有效。
-
代码段(Code Segment):存放程序的机器指令,虽然也是内存的一部分,但通常不被视为常规意义上的“资源”,因为它不随程序运行时的行为动态变化。
非内存资源
非内存资源则涵盖了除内存之外,程序执行过程中需要访问或管理的所有其他类型资源,主要包括:
-
文件资源:程序可能需要打开、读取、写入或创建文件,这些文件句柄或文件描述符是有限的系统资源。
-
网络资源:如网络连接、端口、套接字等,用于进行网络通信。
-
数据库连接:应用程序与数据库交互时建立的连接,每个连接都需要消耗数据库服务器的资源。
-
硬件资源:如GPU、声卡、打印机等外设的访问权限或控制权。
-
线程和进程:线程和进程也是资源的一种,它们消耗CPU时间片、内存以及其他系统资源。
-
锁和信号量:用于同步和并发控制的机制,如互斥锁、信号量等,它们虽不直接占用大量内存,但管理这些同步工具也需要系统资源。
内存资源的销毁
在Java中,内存资源通过作用域限制以及JVM的垃圾回收机制进行回收销毁:
-
通过作用域回收:当变量超出其作用域时,其生命周期自然结束。例如,在一个函数内部定义的局部变量,当函数执行完毕后,该变量的作用域结束,变量生命周期也随之结束,内存将被自动回收。
-
通过垃圾回收机制回收:对于不再可达的对象(即没有任何引用指向它们),垃圾回收器会在适当的时机回收这些对象占用的内存,从而结束这些对象的生命周期。尽管开发者不能直接控制垃圾回收的具体时刻,但可以通过解除对对象的引用(如将引用设为null)来暗示垃圾回收器对象可以被回收。
这里重点描述垃圾回收机制,程序员只要保证需要销毁的资源及时被垃圾回收机制识别就可以。由于JVM主要依据资源是否有对应的强引用来识别是否需要进行回收,所以程序员要做的核心动作就是要及时消除过期的对象引用。
及时消除过期的对象引用
一般大部分的内存资源都可以通过作用域回收机制进行回收,但也有一些漏网之鱼就需要通过垃圾回收机制进行回收,否则就会导致内存溢出,文中作者举了几个典型的例子:
当类自行管理自己内存的时候
当我们在设计类的时候对于类内部的属性有自行定义的生命周期管理机制,但是自行定义的回收JVM是不能直接理解的,这里就需要警惕内存泄露问题,作者给出了一个自定义Stack类收缩的例子。
package BuilderModel.Stack;
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();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
public void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
当Stack类弹出栈顶元素的时候,如果没有显式的将占顶元素的引用设置为null(elements[size]==null),虽然在类中意图将该元素从栈中移出(elements[--size]),但是由于栈顶元素的引用仍然存在,因此垃圾回收机制不会将该元素真正的回收,长此以往,栈内过期引用会越来越多最终导致内存溢出。
当对象引用存放在缓存中时
首先明确一下这里说的缓存不是我们之前学的CPU缓存和RAM缓存等偏底层的概念,也不是Redis这些外部缓存的概念,他是指内存中用于临时存储数据的一块存储区域、存储结构以及所带的一套管理方法,这里就明确了单纯声明的一个集合(无论是Map、List还是Set)都只是一个数据结构而不是缓存的概念,它只明确了存储结构,并没有相关的管理方法(存储策略、过期策略、容量限制、并发控制等),所以缓存一定是包括了这些东西的一个类(这个概念搞错了你会不知道作者这段在说什么)。
重新回来,文中说一旦我们将对象引用从放到缓存中,它就很容易被遗忘掉(这里作者应该想表达的是程序员很容易遗忘掉缓存中存在对于对象的引用),导致很多内存中会存在很多过期引用。这里建议使用WeakHashMap来作为缓存的数据结构,举一个简单的例子:
import java.util.WeakHashMap;
public class example {
public static void main(String[] args)
{
WeakHashMap<String, Integer> Wallet = new WeakHashMap<String, Integer>();
String key = "USD";
Wallet.put(key, 12000);
System.out.println(Wallet.get("USD"));
key = null;
System.gc();
System.out.println(Wallet.get("key"));
}
}
输出如下:
(base) MacBook-Pro:WeakHashEx$ java WeakHashEx/example
12000
null
这里的WeakHashMap中有一个键值对{“USD”,12000},而且这个键值对有一个外部引用“key”,他们的关系如下所示(WeakHashMap中的每一个元素都有一个WeakReference<K>是对于键的引用以及一个V是对于值的引用):
当外部引用key存在的时候WeakHashMap中的该元素存在,外部引用消失后,该元素的键由于只有WeakHashMap内部的弱引用会被垃圾回收,而一旦键被回收,依据WeakHashMap的回收机制整个键值对就会被回收。
客户端的监听与回调
这个因素和上面的缓存类似,要记得只保存回监听器的弱引用(比如使用WeakHashMap来实现),不然就需要显示的取消回调的注册。
非内存资源的销毁
非内存资源不受JVM的控制,因此无法直接通过垃圾回收机制进行自动回收,需要程序员实现AutoCloseable接口的close()方法来手动或者自动(try-with-resource)关闭相关资源,或者通过finalize与cleaner方法自动触发。
Finalize方法
Finalize方法是Java中Object类的一个受保护方法,主要目的是提供一个在对象被垃圾回收器回收之前(或之后)执行一些非内存资源清理工作。这个方法通常是不可预测的,也就是垃圾回收器何时调用这个方法是不确定的。因此,在Java9开始,该方法已经不推荐使用,并在Java11后正式被废弃。
Cleaner方法
Cleaner类是Java9引入的一个高级资源管理工具,它提供了一种更加灵活和高效的方式来管理非内存资源的清理工作,如关闭文件、网络连接或解锁。相较于传统的 finalize()
方法,Cleaner
提供了更可靠的资源回收机制。但是Cleaner注册的清理方法调用时机依然是不确定的。
所以在文中,除了将其作为一个“安全网”,作者并不建议使用这两种方法(的确文中介绍了Cleaner方法在本地对等体中的使用,但是前提是本地对等体没有关键资源,并且性能也可以接受,个人认为如果符合这个假设那么其他的对象也同样适用)。
“安全网”机制
下面的案例介绍了什么是“安全网”机制:考虑一个Room类,一方面其引用了AutoCloseable接口实现了close()方法,同时注册了Cleaner的处理任务(State),作为close方法未按计划启动时的“兜底”方案。
import java.lang.ref.Cleaner;
public class room implements AutoCloseable{
private static final Cleaner cleaner = Cleaner.create();
private Cleaner.Cleanable cleanable;
private State state;
public room(int numJunkPiles){
this.state = new State(numJunkPiles);
this.cleanable = cleaner.register(this, state);
}
public static class State implements Runnable{
int numJunkPiles;
public State(int numJunkPiles){
this.numJunkPiles = numJunkPiles;
}
@Override
public void run(){
System.out.println("Cleaning room");
this.numJunkPiles = 0;
}
}
@Override
public void close() throws Exception {
cleanable.clean();
}
}
如果主程序按计划调用了close方法,则没有Cleaner处理任务什么事情:
public class roomApplication {
public static void main(String[] args) throws Exception {
try(room Room = new room(7)){
System.out.println("Bye");
}
}
}
输出:
(base) MacBook-Pro:Room $ java Room/roomApplication
Bye
Cleaning room
如果主程序没有按计划调用close方法,当外界不存在强引用的时候就会启用Cleaner的注册方法(但不一定会立即执行)
public class roomApplication {
public static void main(String[] args) throws Exception {
new room(7);
System.out.println("Bye");
}
}
try-with-resources方法
try-with-resource是Java建议的用于处理非内存资源回收的方法,使用的前提是相关的资源类要实现AutoCloseable接口,它有几项优势:
- 相比于finalize和cleaner:处理方法的调用时机可控,在资源任务执行完毕之后即自动调用close方法。
- 相比于直接调用close方法:无需显示声明,由程序自动调用。
- 相比于try-finally方法:一是无需显示声明close方法,二是当资源的执行和close方法同时报错的时候,try-finally方法的close异常会完全覆盖掉资源执行时的异常,而try-with-resource方法会自动禁止close异常,而让执行的异常显露出来,便于测试(而且close的异常并不会被抛弃,而是会在堆栈轨迹中被打印出来)。
以下是一个应用try-with-resource方法的案例,在out.write()方法执行完毕后,会自动将两个Stream给close掉。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class copier {
private static final int BUFFER_SIZE = 1024;
public static void copy(String src, String dst) throws FileNotFoundException, IOException{
try(
InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)
){
byte[] buffer = new byte[BUFFER_SIZE];
int n;
while((n=in.read(buffer))>=0){
out.write(buffer, 0, n);
}
}
}
}