java: web應用中不經意的記憶體洩露

weixin_33901926發表於2015-09-16

前面有一篇講解如何在spring mvc web應用中一啟動就執行某些邏輯,今天無意發現如果使用不當,很容易引起記憶體洩露,測試程式碼如下:

1、定義一個類App

package com.cnblogs.yjmyzz.web.controller;

import java.util.Date;


public class App {

    boolean isRun = false;

    public App() {
        isRun = true;
    }

    public void start() {
        while (isRun) {
            System.out.println("=======> I AM ALIVE =>" + new Date());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void stop() {
        isRun = false;
    }

}

程式碼裡面的內容不是重點,只是示意一下,我打算在spring mvc 應用一啟動時,就讓這個類例項化,執行其中的start方法,即:每隔一秒輸出一句話。

 

2、定義一個Listener

import com.cnblogs.yjmyzz.web.controller.App;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

@Component
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {

    App app;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent evt) {
        if (evt.getApplicationContext().getParent() == null) {

            new Thread(new Runnable() {
                @Override
                public void run() {
                    app = new App();
                    app.start();
                }
            }).start();
        }

    }
}

程式碼也很簡單,應用一啟動,就開一個執行緒,例項化App,然後呼叫app.start()方法,執行一下,也跟預期的一樣,每隔一秒輸出類似下面的內容:

 =======> I AM ALIVE =>Wed Sep 16 21:55:42 CST 2015

正式部署到jboss上以後,問題來了,在jboss管理控制檯上,把這個應用給disable甚至remove後,日誌裡仍然不斷有上面的類似輸出,即app的例項仍然活著,其start方法也始終在執行,換句話說,app並沒有被銷燬。

簡單分析一下:jboss的每個server啟動後,會伴隨啟動一個jvm例項,而部署在該server上的web應用,裡面建立的各種資源也在這個jvm例項中,就算把應用給停掉甚至刪除,由於程式碼中沒有任何清除app或停止start方法的處理,所以這個例項一直存在,不會被銷燬,除非server重啟。

 

另一個問題:如果把上面這段程式碼中,建立執行緒的部分去掉,改成直接 app = new App(); app.start(); 部署時會發現另一個現象,日誌裡仍然不斷有輸出,即程式碼在執行,但是該應用在jboss中的狀態始終是isdeploying,部署一直無法結束,始終處於『部署中』的狀態。

原因:start方法中的Thread.sleep()方法會阻塞執行緒,導致部署無法執行完畢。

 

解決辦法:

import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Date;

@Component
public class App {

    boolean isRun = false;


    @PostConstruct
    public void init() {
        System.out.println("init ==> " + new Date());
        isRun = true;
    }

    public void start() {
        while (isRun) {
            System.out.println("=======> I AM ALIVE =>" + new Date());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void stop() {
        isRun = false;
    }

    @PreDestroy
    public void destroy() {
        System.out.println("destroy ==> " + new Date());
        stop();
    }
}

這裡做了幾處改進:

a) 加上@Component後,App的例項將由Spring容器自動建立,即由容器來管理

b) 加上了@PreDestroy,Bean的生命週期由Spring容器來管理後,凡是Bean里加上該註解的方法,會在Bean銷燬前被執行,通常該方法用於清理資源

c) 將初始化的工作,移到了init方法中,並通過@PostConstruct註解告訴Spring,在呼叫完Bean的預設構造方法後,自動來呼叫該方法(當然這一步是可選的,並非必須)

@Component
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    App app;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent evt) {
        if (evt.getApplicationContext().getParent() == null) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    app.start();
                }
            }).start();
        }

    }


}

Listener中就簡單多了,直接@Autowired注入app例項就行了。

 

個人建議:

a) 如果要在web 應用一啟動時,就執行某些操作,特別是對資源類的長連線例項建立(比如:載入資料到快取中預熱、連線到Zookeeper監控節點變化、連線到Ftp準備取資料),最好交給Spring容器來自動建立,且務必記得在Destroy前,清理資源(即:斷開連線)

b) 在啟動的執行邏輯中,不要使用阻塞執行緒的操作(比如:Thread.sleep之類的方法),否則部署時,實際上程式碼已經在後臺執行了,jboss管理控制檯上,一直處於部署中的狀態,也沒有任何輸出,讓人一頭霧水,折騰半天才能定位錯誤,很浪費時間,如果是線上生產環境,是要粗事情的。

相關文章