Java ngầm thu hồi lại bộ nhớ thông qua từng chu kỳ kiểm tra của GC - Garbage Collector. GC phát hiện và thu hồi các unreachable objects - tức các object không được tham chiếu tới.
Như vậy, nếu object không được sử dụng đến trong application (hay còn gọi là unused) nhưng bằng một cách nào đó, vẫn tồn tại tham chiếu tới object đó thì đây chính là nguy cơ xảy ra memory leaks.
Oái oăm thay, bộ Garbage Collector chỉ nhận biết được "unreachable" object chứ không nhận biết được unused object. Sự tồn tại của unused object lệ thuộc vào logic của chương trình. Do đó, lập trình viên phải hết sức thận trọng khi code. Nếu không, những lỗi ngớ ngẩn nhất có thể biến thành tai họa khôn lường trong tương lai.
Chúng ta cùng xem xét 6 nguy cơ xảy ra memory leaks trong Java.
package com.example.memoryleak;
public class Adder {
public long addIncremental(long l)
{
Long sum=0L;
sum =sum+l;
return sum;
}
public static void main(String[] args) {
Adder adder = new Adder();
for(long ;i<1000;i++)
{
adder.addIncremental(i);
}
}
}
Bạn có phát hiện được memory leaks trong trường hợp này?
Sai lầm của tôi nằm ở dòng code thứ 5. Thay vì sử dụng kiểu dữ liệu nguyên thủy long cho biến sum, tôi lại dùng Wrapper class Long, từ đó, theo cơ chế autoboxing, vòng lặp này sẽ tạo ra 1000 object sau 1000 bước lặp.
Bài học: chúng ta cần phân biệt rạch ròi các trường hợp sử dụng primitive type và wrapper class. Hãy cố gắng sử dụng primitive type nhiều nhất có thể.
package com.example.memoryleak;
import java.util.HashMap;
import java.util.Map;
public class Cache {
private Map<String,String> map= new HashMap<String,String>();
public void initCache()
{
map.put("Anil", "Work as Engineer");
map.put("Shamik", "Work as Java Engineer");
map.put("Ram", "Work as Doctor");
}
public Map<String,String> getCache()
{
return map;
}
public void forEachDisplay()
{
for(String key : map.keySet())
{
String val = map.get(key);
System.out.println(key + " :: "+ val);
}
}
public static void main(String[] args) {
Cache cache = new Cache();
cache.initCache();
cache.forEachDisplay();
}
}
Tại ví dụ này, memory leaks xảy ra do cấu trúc dữ liệu map.
Trên thực tế, nhiệm vụ của class Cache là hiển thị giá trị của employee từ cache. Một khi giá trị đã được hiển thị, ta không cần lưu chúng trong cache nữa. Vấn đề ở đây là ta "quên" clear cache. Mặc dù các object kiểu HashMap trong cache không được application dùng đến nhưng chúng vẫn tồn tại và được "lưu trữ trong map" (tức map vẫn chứa tham chiếu tới các object này), do đó bộ GC không thể "dọn dẹp" các object "thừa". Vậy là... memory leaks.
Từ ví dụ này, có 2 giải pháp tránh memory leaks khi làm việc với cache:
try
{
Connection con = DriverManager.getConnection();
…………………..
con.close();
}
Catch(exception ex)
{
}
Qúa rõ ràng, chúng ta close connection trong khối try, như vậy nếu có exception, connection sẽ không bị close(), và đây là lỗi memory leaks ngớ ngẩn nhất mà bạn có thể mắc phải.
Hãy nhớ, chỉ nên gọi hàm close() trong khối finally.
package com.example.memoryleak;
import java.util.HashMap;
import java.util.Map;
public class CustomKey {
public CustomKey(String name)
{
this.name=name;
}
private String name;
public static void main(String[] args) {
Map<CustomKey,String> map = new HashMap<CustomKey,String>();
map.put(new CustomKey("Shamik"), "Shamik Mitra");
String val = map.get(new CustomKey("Shamik"));
System.out.println("Missing equals and hascode so value is not accessible from Map " + val);
}
}
Trong class CustomKey này, chúng ta quên không implement 2 method là equals() và hashCode(). Nói thêm một chút, để truy cập tới một object của map, method get() sẽ check hashCode() và equals() cho từng object. Như vậy, hậu qủa của việc không implement 2 method kia là key và value lưu trong map không thể lấy ra được. Việc trong map tồn tại các cặp <key,value> nhưng application không thể "dùng" được chúng chắc chắn được liệt vào danh sách memory leaks.
Như vậy, khi tạo CustomKey, đừng quên equals() và hashCode().
package com.example.memoryleak;
import java.util.HashMap;
import java.util.Map;
public class MutableCustomKey {
public MutableCustomKey(String name)
{
this.name=name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MutableCustomKey other = (MutableCustomKey) obj;
if (name == null) {
if (other.name != null)
return false;
} elseif (!name.equals(other.name))
return false;
return true;
}
public static void main(String[] args) {
MutableCustomKey key = new MutableCustomKey("Shamik");
Map<MutableCustomKey,String> map = new HashMap<MutableCustomKey,String>();
map.put(key, "Shamik Mitra");
MutableCustomKey refKey = new MutableCustomKey("Shamik");
String val = map.get(refKey);
System.out.println("Value Found " + val);
key.setName("Bubun");
String val1 = map.get(refKey);
System.out.println("Due to MutableKey value not found " + val1);
}
}
Trường hợp này có equals() và hashCode() cho CustomKey nhưng vô tình chúng ta đã biến các object CustomKey thành mutable sau khi lưu nó vào map. Nếu thuộc tính của chúng thay đổi thì application sẽ không thể "tìm" được chúng. Tuy nhiên map vẫn giữ tham chiếu tới các object đó >> Memory leaks.
Bài học: CustomKey luôn đi kèm với tính Immutable
package com.example.memoryleak;
public class Stack {
private int maxSize;
private int[] stackArray;
private int pointer;
public Stack(int s) {
maxSize = s;
stackArray = newint[maxSize];
pointer = -1;
}
public void push(int j) {
stackArray[++pointer] = j;
}
public int pop() {
return stackArray[pointer--];
}
public int peek() {
return stackArray[pointer];
}
public boolean isEmpty() {
return (pointer == -1);
}
public boolean isFull() {
return (pointer == maxSize - 1);
}
public static void main(String[] args) {
Stack stack = new Stack(1000);
for(int ;i<1000;i++)
{
stack.push(i);
}
for(int ;i<1000;i++)
{
int element = stack.pop();
System.out.println("Poped element is "+ element);
}
}
}
Vấn đề này hơi oái oăm - stack của chúng ta bị "co-giãn".
Mọi đau khổ bắt nguồn từ qúa trình implementation một stack của lập trình viên. Dưới góc nhìn của con người, Stack giống một mảng (với quy tắc ghi-xóa phần tử đặc biệt), tuy nhiên từ góc nhìn của application, bộ phận khả dụng của Stack là nơi con trỏ trỏ đến.
Vì thế, xét trong ví dụ này, khi Stack tăng lên 1000 phần tử, các ô trống của nó sẽ được lấp đầy. Tuy nhiên sau khi chúng ta pop() tất cả phần tử ra, con trỏ trỏ về 0. Application căn cứ vào gía trị con trỏ trỏ đến, và sẽ xem xét stack này là stack rỗng. Tuy vậy, stack này vẫn chữa tham chiếu tới các phần tử bị pop(). Trong Java, các tham chiếu kiểu này được gọi là obsolete reference - tham chiếu thừa. Tham chiếu thừa là những tham chiếu không thể "gỡ" đi được - tức không thể "dọn dẹp" chúng bằng GC.
Khắc phục: gán giá trị null cho mỗi object sau khi chúng bị pop().
Tổng hợp lại, ta có 5 giải pháp phòng ngừa memory leaks trong Java:
Bài viết được dịch từ dzone.com
IT Software Engineer (Long Thanh IZ)
Công ty TNHH framas Korea Vina
Địa điểm: Đồng Nai
Lương: Cạnh Tranh
Sales Manager/ Business Development Manager (Japanese Language/ Digital Solutions)
CÔNG TY TNHH TRANSCOSMOS VIỆT NAM
Địa điểm: Hồ Chí Minh
Lương: 40 Tr - 90 Tr VND
Middle/ Senior NodeJS Developer
CÔNG TY CỔ PHẦN HASAKI BEAUTY & CLINIC
Địa điểm: Hồ Chí Minh
Lương: Cạnh Tranh
FrontEnd Developer (ReactJS, VueJS, HTML)
Địa điểm: Hồ Chí Minh
Lương: 10 Tr - 16 Tr VND
Địa điểm: Hồ Chí Minh
Lương: Cạnh Tranh
Quản lý dự án (Scrum Master/Game Producer)
CÔNG TY CỔ PHẦN CÔNG NGHỆ SKYBULL VIỆT NAM
Địa điểm: Hà Nội
Lương: 10 Tr - 12 Tr VND
Địa điểm: Hồ Chí Minh
Lương: 25 Tr - 40 Tr VND
CÔNG TY TNHH BẢO HIỂM NHÂN THỌ MB AGEAS
Địa điểm: Hà Nội
Lương: Cạnh Tranh
Tổng công ty Xây dựng số 1 – CTCP
Địa điểm: Hồ Chí Minh
Lương: Cạnh Tranh
Mobile Developer Junior (Mạnh Flutter)
Công Ty Cổ Phần Phương Tiện Điện Thông Minh Selex
Địa điểm: Hà Nội
Lương: 800 - 1,500 USD