踩坑经验:
对于IO及池化资源(文件、线程池、网络IO(HttpClient)、磁盘IO),使用之后一定要及时回收,好借好还,再借不难。
稳定关闭方式
- 完成I/O操作后,应该关闭流以释放系统资源。可以使用
finally
块确保流被关闭,或者使用try-with-resources
语句自动关闭。
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// 操作文件
} catch (IOException e) {
// 处理异常
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// 处理关闭时的异常
}
}
}
2. java 7及以上版本支持try-with-resources语句,它可以自动关闭实现了AutoCloseable接口的资源。
try-with-resources
模式不仅适用于文件操作,还可以用于数据库连接、网络连接等任何需要清理资源的场景。
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
// 操作文件
}
// reader在这里会自动关闭
节约资源
- 避免循环开启和关闭。在循环外部打开资源,然后在循环内部使用,这样可以避免在每次迭代中都打开和关闭资源。
- 使用连接池:
- 对于数据库连接、线程处理异步任务,使用连接池来管理,而不是每次操作都创建和关闭连接。
- 使用线程局部变量(
ThreadLocal
)时要小心:ThreadLocal
变量可能会在线程结束后仍然持有资源,导致资源泄露。确保在适当的时候移除或释放这些资源。mvc请求或者jsf请求可以在拦截器中统一进行设置上下文,执行结束后统一移除。
- 释放外部资源:
- 如果你的应用使用到了外部资源,如网络连接、文件句柄等,确保在不再需要时释放它们。
- 避免使用全局变量持有资源引用:
- 全局变量可能会长时间持有资源引用,导致资源无法被垃圾回收。
- 使用对象池:
- 对于频繁创建和销毁的对象,可以使用对象池来减少资源消耗。
- 注销监听器和其他回调:
- 当不再需要监听器或其他回调时,确保注销它们,以避免内存泄露。
- 使用适当的设计模式:
- 某些设计模式,如单例(Singleton)或静态持有(Static Hold),可能会隐含地持有资源引用。使用这些模式时要注意资源的生命周期。
- 定期检查资源使用情况:
- 通过泰山磁盘、内存监控定期检查资源使用情况,以便发现潜在的资源泄露。
- 编写单元测试:
- 编写单元测试来验证资源是否在适当的时候被释放。
- 避免在异常处理中创建资源:
- 在
catch
块或finally
块中创建资源可能会导致资源无法被正确关闭。
- 在
- 使用资源管理框架:
- 考虑使用资源管理框架,如Apache Commons IO的
IOUtils
,它们提供了方便的方法来关闭资源。
- 考虑使用资源管理框架,如Apache Commons IO的
- 理解垃圾回收机制:
- 了解Java的垃圾回收机制,确保资源对象的引用能够被垃圾回收器正确回收。
- 避免使用finalize方法:
finalize
方法在对象被垃圾回收时调用,它的执行时机不确定,不应该依赖它来释放资源。
通过遵循这些规范,可以有效地防止资源泄露,提高应用的稳定性和性能。
示例
- 线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
//线程池类未继承closeable接口,需要手动进行关闭
public class ThreadPoolShutdown {
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
try {
// 提交任务到线程池
executorService.submit(() -> {
// 模拟任务执行
System.out.println("Task is running.");
});
} catch (InterruptedException e) {
log.error("失败",e);
}finally{
//不能继续提交任务,现有任务可以执行完成
executorService.shutdown();
// 尝试立即停止所有正在执行的任务,并暂停处理等待的任务,并返回等待执行的任务列表
// 此方法不会等待正在执行的任务完成
executorService.shutdownNow();
}
}
}
2.io流
import java.io.FileInputStream;
import java.io.IOException;
public class ReadFile {
public static void main(String[] args) {
String filePath = "example.txt";
byte[] bytes = new byte[1024];
int bytesRead;
try (FileInputStream fis = new FileInputStream(filePath)) {
while ((bytesRead = fis.read(bytes)) != -1) {
// 将读取的字节转换为字符串
String readData = new String(bytes, 0, bytesRead);
System.out.println(readData);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void read() {
String content = "Hello, World!";
String filePath = "example.txt";
try (FileOutputStream fos = new FileOutputStream(filePath)) {
// 将字符串转换为字节并写入文件
fos.write(content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
//字符流方式读取
public void readByBuffer(){
String filePath = "example.txt";
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
//字符流方式写入
public void writeByBuffer() {
String content = "Hello, World!";
String filePath = "example.txt";
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
writer.write(content);
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.Apache CSV
public static void main(String[] args) {
//支持多个资源
try (Reader fileReader = new InputStreamReader(new FileInputStream("filename"), Charset.forName("utf-8"));
CSVParser records = CSVFormat.DEFAULT.withQuoteMode(QuoteMode.ALL_NON_NULL).withFirstRecordAsHeader()
.withAllowMissingColumnNames().parse(fileReader)) {
for (CSVRecord record : records) {
log.info("记录:{}", record.get("iemi"));
}
} catch (Exception e) {
log.error("错误", e);
}
}
一般fileReader和records是全局的
protected Reader fileReader;
protected CSVParser records;
@SneakyThrows
public Long export(File file, String[] properties, String[] headers) {
reset();
PrintWriter printWriter = new PrintWriter(file, StandardCharsets.UTF_8.name());
CSVPrinter csvPrinter = CSVFormat.EXCEL.withHeader(headers).print(printWriter);
Long rows = 0L;
try {
for (CSVRecord record : records) {
List<String> list = toList(record, properties);
if (list != null) {
csvPrinter.printRecord(escapeValues(list));
rows++;
}
}
} catch (Exception e) {
log.error("csv standard exception...{}", e.getMessage());
throw e;
} finally {
close();
csvPrinter.close(true);//包含刷新
printWriter.close();
}
return rows;
}
public void close() {
if (records != null) {
records.close();
}
if (fileReader != null) {
fileReader.close();
}
}
- 网络io流
Httpclient相关
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import java.io.IOException;
//httpclient连接池不能自动关闭,CloseableHttpClient 实现了Closeable
//RestTemplate 帮助使用者关闭了
public class HttpClientShutdown {
public static void main(String[] args) {
// 创建HttpClient连接池管理器
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// 创建CloseableHttpClient实例
try (CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(cm)
.build()) {
// 使用client执行HTTP请求
}finally{
// 关闭HttpClient连接池
// 这一步很重要,因为HttpClient默认不会自动关闭连接池
cm.shutdown();
}
}
}
其中,线程池ExecutorService、HttpClient连接池都需要进行资源回收,需调用shutdown方法
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NioSocketChannelCloseExample {
public static void main(String[] args) {
String host = "example.com"; // 替换为实际的服务器地址
int port = 12345; // 替换为实际的服务器端口
try (Socket socket = new Socket();
SocketChannel socketChannel = socket.getChannel()) {
// 连接到服务器
socketChannel.connect(new InetSocketAddress(host, port));
// 创建ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 从SocketChannel读取数据到ByteBuffer
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
// 达到对端流的末尾
return;
}
// 切换ByteBuffer到读模式
buffer.flip();
// 处理接收到的数据
// 例如,将字节转换为字符串
byte[] data = new byte[buffer.limit()];
buffer.get(data);
String receivedString = new String(data, 0, bytesRead);
System.out.println("Received: " + receivedString);
// 发送数据到SocketChannel
String message = "Hello, Server!";
buffer.clear();
buffer.put(message.getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
// 在这个try-with-resources块结束时,Socket和SocketChannel都会自动关闭
} catch (IOException e) {
e.printStackTrace();
}
// 所有资源都已在try-with-resources语句中自动关闭
}
}
- ThreadLocal
ThreadLocal<String> threadLocal = new ThreadLocal<>();
try {
threadLocal.set("some value");
// 执行一些操作
} finally {
//结束后remove
threadLocal.remove();
}
public class ThreadLocalWithCleanup extends ThreadLocal<String> {
@Override
protected String initialValue() {
return "default value";
}
@Override
protected void finalize() throws Throwable {
super.finalize();
this.remove(); // 确保在对象被垃圾回收前移除值
}
}
public class Task implements Runnable {
private final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public Task() {
// 初始化ThreadLocal
}
@Override
public void run() {
threadLocal.set("some value");
try {
// 执行任务
} finally {
threadLocal.remove(); // 任务执行完毕后移除值
}
}
}
某些场景下,子线程需要使用父线程的上下文
public class ParentTask implements Runnable {
public void run() {
InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("parent value");
// 创建并启动子线程
Thread childThread = new Thread(new ChildTask(threadLocal));
childThread.start();
}
}
public class ChildTask implements Runnable {
private final InheritableThreadLocal<String> threadLocal;
public ChildTask(InheritableThreadLocal<String> threadLocal) {
this.threadLocal = threadLocal;
}
@Override
public void run() {
try {
String value = threadLocal.get();
// 使用value
} finally {
threadLocal.remove(); // 子线程中移除值
}
}
}
ThreadLocal原理
ThreadLocal是Thread中的成员变量,内部维护了一个Map:ThreadLocalMap
|
用来保存线程私有变量
|
ThreadLocalMap中Entry的key使用WeakReference指向ThreadLocal,value为线程私有变量。
ThreadLocalMap和Thread的生命周期相同,在JVM中每个线程拥有一个方法调用栈,ThreadLocalMap并不会随着方法栈帧的进栈出栈而创建回收,
需要调用ThreadLocal::remove方法对变量进行清理,避免内存泄露:
|
|
WeakReference是为了兜底,如果没有显式的执行ThreadLocal::remove,那么在下一次GC时,ThreadLocal.Entry的key将被回收,设置为null,在下次调用ThreadLocal::get、ThreadLocal::set方法时,会进行null值的检查,如果为空,会进行value的回收,具体方法:
get:
set:
文件被锁原因分析
- 文件输入输出流(File InputStream/OutputStream)未关闭: 如果在Java程序中使用文件流进行读写操作,而在使用完毕后没有正确关闭流,那么即使文件在文件系统中被删除,Java程序中的文件流仍然持有对该文件的引用。这会导致操作系统无法回收文件所占用的空间。
FileInputStream fis =new FileInputStream("file.txt");// ... 进行文件操作 ...// 忘记关闭流
- 文件通道(FileChannel)未关闭: 类似地,如果使用
java.nio
包中的FileChannel
进行文件操作,也必须确保操作完成后关闭通道。
FileChannel channel = FileChannel.open(Paths.get("file.txt"));// ... 进行文件操作 ...// 忘记关闭通道
- RandomAccessFile未关闭:
RandomAccessFile
也用于文件的随机访问操作,同样需要在操作完成后关闭。本次不涉及
RandomAccessFile raf =new RandomAccessFile("file.txt","rw");// ... 进行文件操作 ...// 忘记关闭RandomAccessFile
- 文件锁(FileLock)未释放: 如果程序中使用了文件锁,那么即使文件被删除,持有锁的程序仍然可以访问文件内容。在删除文件前应该确保释放所有文件锁。
FileLock lock = channel.lock();// ... 进行文件操作 ...// 忘记释放锁
- 使用文件路径或文件对象引用: 即使文件被删除,只要程序中还保留有指向该文件的
File
对象或者文件路径的引用,就可能导致操作系统无法回收文件占用的空间。
File file =new File("file.txt");// ... 进行文件操作 ...// 忘记清理File对象引用