- PR -

JSTLのメモリリーク?

投稿者投稿内容
ねこたまご
会議室デビュー日: 2006/10/06
投稿数: 6
投稿日時: 2006-10-06 16:46
Strutsを利用したWebアプリを開発しているのですが、
Actionクラス内で作成したListオブジェクトを
HttpRequest.setAttribute()でrequestオブジェクトに格納して
表示用JSPにforwardし、JSP内でJSTLの<forEach>タグを使用して
Listオブジェクトの中身を表示するという処理を行っていました。
このActionとJSPはお互いにforwardし合うのですが、Listオブジェクトの
サイズが大きいと、かならず二回目のAction -> JSPのforwardで
OutOfMemoryErrorが発生しました。
色々と試してみて、JSTLの<forEach>タグではなくてStrutsの
<logic:iterate>タグを使うとOutOfMemoryErrorは発生しなくなったのですが
これはJSTLの<forEach>タグの実装がメモリリークを起こすような
コードになっているんでしょうか?

このような現象を経験された方いらっしゃいますでしょうか?
nagise
ぬし
会議室デビュー日: 2006/05/19
投稿数: 1141
投稿日時: 2006-10-06 17:24
引用:

ねこたまごさんの書き込み (2006-10-06 16:46) より:
このActionとJSPはお互いにforwardし合うのですが、



forwardし合うとはどういう意味ですか?
Servlet側(StrutsのAction含む)からJSP側へのRequestDispatcher.forward()は解るとして。
JSPからServletにforwardというのはちょっとピンときません。
<jsp:forward>?いやいやそんな筈は…
nagise
ぬし
会議室デビュー日: 2006/05/19
投稿数: 1141
投稿日時: 2006-10-06 17:42
とりあえず、以下のような検証を行いました。

1.java.lang.Object.finalize()をオーバーライドして
  ガーベッジコレクションに回収される際にSystem.outに
  ログを書き出すHogeクラスを作成
2.ListにHogeを格納してJSPにforward
3.JSPで該当Listを<forEach>でループさせる

JSPの表示後にHogeクラスのfinalize()が
呼び出されることが確認できました。

通常のServletにての確認なので前提条件は異なるかもしれませんが
JSTLの<forEach>が単純にオブジェクトをリークしていることは否定されたと思います。

なお、当方の環境はWindowsXP上でJDK 5。
Tomcat5.5 なので JSTL 2.0 の筈。
別バージョンは未検証です。
ねこたまご
会議室デビュー日: 2006/10/06
投稿数: 6
投稿日時: 2006-10-06 17:44
nagise様

すみません、書き方が悪くて混乱させてしまいましたが、
単純にJSPのformからrequestを送信するだけです。
struts-config.xmlのAction-mappingではこのように定義しています。
<action-mappings>
  <action path="/memorytest" name="memorytestform"
      type="memorytest.action.MemoryTestAction">
    <forward name="forward1" path="/pages/memorytest.jsp"/>
  </action>
</action-mappings>


参考までに検証用に作ったActionとJSPのソースを載せておきます。
JVMのヒープサイズは256MBです。
=========== JSP =======================
<body>
 <html:form method="GET" action="/memorytest">
  <input type="submit" /><br>
  <%
  if (null != request.getAttribute("resultList")) {
  %>
    <c:forEach var="listOfRows" varStatus="rowStatus" items="${resultList}">
      ${listOfRows}<br>
    </c:forEach>
  <%
  }
  %>
  </html:form>
</body>

====== Action ================
public class MemoryTestAction extends Action {
public ActionForward execute(ActionMapping map, ActionForm form,
  HttpServletRequest req, HttpServletResponse res) throws Exception {

  String var = "AAAAAAAAAAAAAAAAAAAAAAAAAAAA";
  List<String> list = new ArrayList<String>();
  try {
    for(int i = 0; i < 1300000; i ++) {
      String temp = new String(var.getBytes()) + i;
      list.add(temp);
    }
  } catch (OutOfMemoryError oome) {
    System.out.println("*********** OutOfMemoryの発生 ************");
    oome.printStackTrace();
    req.removeAttribute("resultList");
    return map.findForward("forward1");
  }
  req.setAttribute("resultList", list.subList(0, 300));
  return map.findForward("forward1");
}
}
小僧
ぬし
会議室デビュー日: 2002/08/14
投稿数: 526
投稿日時: 2006-10-06 18:54
コード:

    for(int i = 0; i < 1300000; i ++) {



ちょっとListに乗っけるインスタンスの量が多すぎませんかね?。
一時的に消費されるメモリ量を計算してみました。
JRE 1.4.2での場合ですが、Stringクラスのインスタンスのメモリ消費量は、
 2 * (Length) + 38 ± 2
だそうなので、ループ後のlistインスタンスのサイズは
 2 * (28) + 38 + 2 = 124800000byte   ※+2で計算
124800000byte / 1024 / 1024 ≒ 119Mbyte
凄いサイズになってしまいました・・・。
あと、ArrayListの初期化なんですが、
コード:

  List<String> list = new ArrayList<String>();


要素数を指定しないと、必要になる度に領域を拡張して行くと思うので、
そういった内部処理でもメモリ消費が進んでいるかもしれませんね。





[ メッセージ編集済み 編集者: 小僧 編集日時 2006-10-06 18:58 ]
ねこたまご
会議室デビュー日: 2006/10/06
投稿数: 6
投稿日時: 2006-10-06 19:11
引用:

nagiseさんの書き込み (2006-10-06 17:42) より:

JSPの表示後にHogeクラスのfinalize()が
呼び出されることが確認できました。




nagise様
検証ありがとうございます。
こちらの環境でも同様のMemTestListクラスを作成し、さらに
Actionクラス内のfor文の前後にprint文を追加してListオブジェクトの
生成開始と完了を見てみました。
すると、<logic:iterate>タグを使用した場合は、
生成開始 -> finalize -> 生成完了 という順番でprint文が出力されていたのに対し、
<forEach>タグでは
生成開始のprint文が出力された後でfinalizeのprint文は出力されずに
OutOfMemoryErrorが発生しました。

このことからやはり生成したListオブジェクトがメモリ上に残っているのだと
思うので、来週にでもStrutsを使わないケースでの検証などしてみようと思います。

P.S
 finalize()をオーバーライドしてオブジェクトが破棄されることを確認する
 方法は気づきませんでした。大変勉強になりました。ありがとうございます。
ねこたまご
会議室デビュー日: 2006/10/06
投稿数: 6
投稿日時: 2006-10-06 19:25
小僧様

ご意見ありがとうございます。
今回の検証ではListオブジェクト作成処理を2回繰り返すとJVMのヒープサイズを
オーバーするという状況にしたかったのでListオブジェクトのサイズが大きくなるように
しました
(JVMのヒープサイズを小さくすれば良かったんですが、他の作業の
関係でJVMのヒープサイズを変えたくなかったもので(汗))
ArrayListの初期サイズの指定についても今後の検証で色々試してみたいと思います。
nagise
ぬし
会議室デビュー日: 2006/05/19
投稿数: 1141
投稿日時: 2006-10-06 19:36
引用:

ねこたまごさんの書き込み (2006-10-06 19:11) より:
P.S
 finalize()をオーバーライドしてオブジェクトが破棄されることを確認する
 方法は気づきませんでした。大変勉強になりました。ありがとうございます。



でもコレ、確実ではないんですよ。
finalize()が呼ばれたことでガーベッジコレクションに回収されたとは
言えると思いますが、呼ばれないことでリークしているとはいえません
通常、スコープをはずれて参照されなくなった時点で直ちにガーベッジコレクションが
回収するわけではないので、この間にタイムラグがあります。
finalize()がされるタイミングはVMのガーベッジコレクションの実装に依存しますから。
そして、必ず呼ばれるという保証もない(System.exit()とかすると呼ばれずに終了します)。

今回のサンプルのように単に1発どでかい参照を用意するというケースでは
リークしていようがいまいが、その時点でヒープ領域が埋まればOutOfMemoryですからね。
「リークしている」と言うためには「本来の役目が終わった後にも参照を保持しつづけている」
ということを証明しないといけません。

ほどほどのメモリを食うオブジェクトをforEachするコードを用意して
それを何度か繰り返し呼び出した際にOutOfMemoryが発生するのであれば
forEachタグがオブジェクトをリークしている疑いがでてきますが、
その検証コードでは、検証コードそのものが誤っている疑いが強いと思いますよ。

スキルアップ/キャリアアップ(JOB@IT)