Saturday, June 18, 2005

ปัญหา Memory Leak ใน Web Application

หลายคนคงจะเคยเจอปัญหา
OutOfMemoryError
หลังจากที่ทำ hot restart
Web Application ไปซักระยะหนึ่ง

วันนี้อ่านพบสาเหตุของปัญหาใน blog THE ART OF .WAR
เขาอธิบายและทำ reference ไว้ดีมาก

ส่วนคนที่ขี้เกียจอ่านโดยละเอียด
ผมจะสรุปให้ฟังคร่าวๆดังนี้

สาเหตุหลักของปัญหานี้เกิดที่ตัว Application ClassLoader
(war แต่ละตัวมี Application ClassLoader ของใครของมัน)
เมื่อใดก็ตามที่ AppServer ทำ hot restart
เจ้า AppServer ก็จะทำการโยน ClassLoader ตัวเก่าทิ้งไป
และทำการ new ClassLoader ตัวใหม่ขึ้นมา
ปัญหาจะเกิดขึ้นเมื่อยังมี Reference ที่ยังคงชี้ไปยัง ClassLoader ตัวเก่าอยู่
ทำให้ Garbage Collection ไม่สามารถเก็บกวาด ClassLoader ตัวเก่าได้

ตัว object ที่สามารถทำให้เกิดสาเหตุแบบนี้ได้
สามารถแบ่งออกเป็น 2 ประเภทคือ
  • JVM level Singletons
  • ThreadLocal object


ประเด็นของ JVM Singleton มีตัวที่เป็นสาเหตุหลักๆอยู่ 2 ตัวก็คือ
  • java.sql.DriverManager
  • java.beans.Introspector


ในส่วนของ DriverManager จะเกิดปัญหาเมื่อ
jdbc library ของเราวางอยู่ใน /WEB-INF/lib
เนื่องจาก DriverManager อยู่ใน System ClassLoader
ตัว jdbc อยู่ใน Application ClassLoader
ดังนั้นจะเกิด reference เชื่อมระหว่าง
System ClassLoader ไปถึง Application ClassLoader
ทำให้ GC ไม่สามารถเก็บกวาดได้

ทางแก้ไขก็คือ implements ServletContextListener
ใน Web App เพื่อที่จะทำการ deregister driver
ออกจาก DriverManager
    public void contextDestroyed(ServletContextEvent event) {
log.info("Shutdown");
Enumeration drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
Driver o = (Driver) drivers.nextElement();
if(doesClassLoaderMatch(o)){
log.info("The current driver '" + o + "' is being deregistered.");
try {
DriverManager.deregisterDriver(o);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}else{
log.info("Driver '" + o + "' wasn't loaded by this webapp, so no touching it.");
}
}
}


สำหรับเจ้า Introspector (ตัวนี้จะใช้เยอะใน Spring Framework)
ก็เป็นปัญหาแบบเดียวกับ DriverManager
นั่นคือเกิด reference เชื่อมระหว่าง SystemClassLoader <--> Introspector
<--> cached Class <--> Application ClassLoader

วิธีแก้ไขก็ใช้วิธีเดียวกับ DriverManager
public void contextDestroyed(ServletContextEvent event) {
// Driver Clean up stuff omitted

// Flushes the cache of classes
java.beans.Introspector.flushCaches();
}


สำหรับประเด็นที่ 2 ที่เป็นปัญหาก็คือการใช้ ThreadLocal
ปัจจุบันนิยมใช้กันมาก
เช่นพวกกลุ่ม lightweight container, Web App. Framework
dom4j (ใช้เก็บ cache)
แม้แต่เจ้า Axis เองก็ใช้ (พวกนี้ยังไม่เห็นด้วยตานะ)

ผมชอบที่ THE ART OF .WAR เขาตั้งหัวข้อประเด็นนี้ว่า
ThreadLocals - With Great Power, comes Great Responsibility.


ประเด็นเรื่อง ThreadLocal นี้ก็เหมือนๆกับ เจ้า 2 ตัวบน
ก็คือมี reference ชี้ไปถึง Application ClassLoader
วิธีแก้ไขก็เช่นเดียวกัน ก็คือต้องมีการเรียกใช้ ThreadLocal.set(null)
หลังจากเลิกใช้แล้ว

ในส่วนของ Spring ก็มีพูดถึงใน mailing list ว่า
For Introspector-related problems in third-party libraries, Spring ships an
IntrospectorCleanupListener for web applications, to be registered as listener in web.xml. This
simply clears the entire JVM-level cache of the JavaBeans Introspector on web app shutdown.

For ThreadLocals, there's nothing we can do to clean up after third-party libraries, I'm afraid. So the
only way to move forward there is to make the developers aware of those issues in their products.

Related link from Roti

No comments: