Resource management is a critical yet often overlooked aspect of Java development. This article explores how to properly manage resources in Java to prevent leaks, using a real example from the SeaTunnel project.
A Fix in SeaTunnel
In Apache SeaTunnel, the HiveSink
component once had a typical resource leak issue. The code comparison before and after the fix is shown below:
Before the fix:
@Override
public List<FileAggregatedCommitInfo> commit(...) throws IOException {
HiveMetaStoreProxy hiveMetaStore = HiveMetaStoreProxy.getInstance(pluginConfig);
List<FileAggregatedCommitInfo> errorCommitInfos = super.commit(aggregatedCommitInfos);
if (errorCommitInfos.isEmpty()) {
// Handle partition logic...
}
hiveMetaStore.close(); // Will not execute if an exception occurs above
return errorCommitInfos;
}
After the fix:
@Override
public List<FileAggregatedCommitInfo> commit(...) throws IOException {
List<FileAggregatedCommitInfo> errorCommitInfos = super.commit(aggregatedCommitInfos);
HiveMetaStoreProxy hiveMetaStore = HiveMetaStoreProxy.getInstance(pluginConfig);
try {
if (errorCommitInfos.isEmpty()) {
// Handle partition logic...
}
} finally {
hiveMetaStore.close(); // Ensures the resource is always released
}
return errorCommitInfos;
}
Though the change appears small, it effectively prevents resource leaks and ensures system stability. Let’s now explore general strategies for Java resource management.
What Is a Resource Leak?
A resource leak occurs when a program acquires a resource but fails to properly release it, resulting in long-term resource occupation. Common resources that require proper management include:
- 📁 File handles
- 📊 Database connections
- 🌐 Network connections
- 🧵 Thread resources
- 🔒 Lock resources
- 💾 Memory resources
Improper resource management may lead to:
- Performance degradation
- Out of memory errors
- Application crashes
- Service unavailability
Two Approaches to Resource Management
(1) Traditional Approach: try-catch-finally
Connection conn = null;
try {
conn = DriverManager.getConnection(url, user, password);
// Use connection
} catch (SQLException e) {
// Exception handling
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// Handle close exception
}
}
}
Drawbacks:
- Verbose code
- Complex nesting
- Easy to forget closing resources
- Harder to maintain when multiple resources are involved
(2) Modern Approach: try-with-resources (Java 7+)
try (Connection conn = DriverManager.getConnection(url, user, password)) {
// Use connection
} catch (SQLException e) {
// Exception handling
}
Advantages:
- Cleaner and more concise
- Automatically closes resources
- Handles exceptions gracefully
- Maintains readability even with multiple resources
Custom Resource Classes
To manage custom resources, implement the AutoCloseable
interface:
public class MyResource implements AutoCloseable {
private final ExpensiveResource resource;
public MyResource() {
this.resource = acquireExpensiveResource();
}
@Override
public void close() {
if (resource != null) {
try {
resource.release();
} catch (Exception e) {
logger.error("Error while closing resource", e);
}
}
}
}
Key principles:
- The
close()
method should be idempotent - Handle internal exceptions within
close()
- Log failures during resource release
Common Pitfalls and Solutions
(1) Resource management inside loops
Incorrect:
public void processFiles(List<String> filePaths) throws IOException {
for (String path : filePaths) {
FileInputStream fis = new FileInputStream(path); // Potential leak
// Process file
fis.close(); // Won’t run if an exception is thrown
}
}
Correct:
public void processFiles(List<String> filePaths) throws IOException {
for (String path : filePaths) {
try (FileInputStream fis = new FileInputStream(path)) {
// Process file
} // Automatically closed
}
}
(2) Nested resource handling
// Recommended approach
public void nestedResources() throws Exception {
try (
OutputStream os = new FileOutputStream("file.txt");
BufferedOutputStream bos = new BufferedOutputStream(os)
) {
// Use bos
} // Automatically closed in reverse order
}
Real-World Examples
(1) Database Connection
public void processData() throws SQLException {
try (
Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users")
) {
while (rs.next()) {
// Process data
}
} // All resources closed automatically
}
(2) File Copy
public void copyFile(String source, String target) throws IOException {
try (
FileInputStream in = new FileInputStream(source);
FileOutputStream out = new FileOutputStream(target)
) {
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
}
}
(3) HTTP Request
public String fetchData(String urlString) throws IOException {
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream()))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
} finally {
connection.disconnect();
}
}
Summary & Best Practices
- Prefer try-with-resources for managing resources
- If try-with-resources is not applicable, use
finally
blocks to release resources - Custom resource classes should implement
AutoCloseable
- Release resources in reverse order of acquisition
- Handle exceptions in
close()
gracefully - Be extra cautious with resource creation inside loops