【问题标题】:Java consumes too much memoryJava消耗太多内存
【发布时间】:2010-02-28 12:01:53
【问题描述】:

我的 Java 编写的应用程序消耗了太多内存。

程序如何工作:用户从日历 (GUI) 中选择一个日期,然后应用程序将数据加载到 JTable 组件中。每次加载数据时,都会创建并设置新的 TableModel。没有创建新的 JTable,只创建了模型。

有什么问题?:从日历中选择新的一天并加载到 JTable 会消耗大约 2-3 MB 的内存。 在启动应用程序时消耗 cca 50-60 MB RAM,在日历上“点击”几下(如 20 次)后,应用程序消耗全部堆大小 (128MB)。应用程序崩溃,当然...

我应该怎么做?:我很确定数据库查询没问题。我可能会以某种方式设置更大的堆大小(我用谷歌搜索过,但这只是我的计算机的解决方案,用户不会这样做)或者我应该以某种方式 删除带有数据库数据的旧 TableModel。 但不应该这样垃圾收集器的工作?我可以强制它(System.gc()),但这无济于事......

感谢您的建议!

编辑:处理日历事件的代码(我删除了 Javadoc,它是我的母语)

package timesheet.handlers;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import org.jdesktop.swingx.JXMonthView;
import org.jdesktop.swingx.event.DateSelectionEvent;
import org.jdesktop.swingx.event.DateSelectionListener;
import timesheet.database.WorkerOperations;
import timesheet.frames.WorkerFrame;
import timesheet.logictrier.*;


public class WorkerMonthViewHandler {
    private JXMonthView monthView;
    private WorkerFrame workerFrame;
    private WorkerOperations wops;
    private Date[] week = new Date[5];
    private WorkerTasksTableHandler wtth;

    public WorkerMonthViewHandler(WorkerFrame workerFrame) {
        this.workerFrame = workerFrame;
        this.monthView = workerFrame.getWorkerMonthView();
        wops = workerFrame.getWorkerOperations(); // for DB usage
    }

    public void initMonthView() {
        List<Task> tasks = wops.getWorkerTasks(workerFrame.getWorker()); // db select
        for (Task task : tasks) {
            if (!monthView.getSelection().contains(task.getPlannedStart())) { 
                monthView.addFlaggedDates(task.getPlannedStart());
                monthView.addFlaggedDates(task.gePlannedEnd()); // not really important
            }
        }
        monthView.setSelectionDate(new Date());
        monthView.getSelectionModel().addDateSelectionListener(new DateSelectionListener() {
            public void valueChanged(DateSelectionEvent dse) {
                Date d = monthView.getSelectionDate();
                for (int i=0; i<week.length; i++) {
                    if (d.equals(week[i])) {     
                        return;
                    }
                }
                Calendar cal = new GregorianCalendar();
                cal.setTime(d);
                long dayMs = 24 * 60 * 60 * 1000;
                switch (cal.get(Calendar.DAY_OF_WEEK)) {
                    case(Calendar.MONDAY) : {
                        week[0] = new Date(cal.getTimeInMillis());
                        week[1] = new Date(cal.getTimeInMillis()+dayMs);
                        week[2] = new Date(cal.getTimeInMillis()+2*dayMs);
                        week[3] = new Date(cal.getTimeInMillis()+3*dayMs);
                        week[4] = new Date(cal.getTimeInMillis()+4*dayMs);
                    } break;
                    case (Calendar.TUESDAY) : {
                        week[0] = new Date(cal.getTimeInMillis()-dayMs);
                        week[1] = new Date(cal.getTimeInMillis());
                        week[2] = new Date(cal.getTimeInMillis()+1*dayMs);
                        week[3] = new Date(cal.getTimeInMillis()+2*dayMs);
                        week[4] = new Date(cal.getTimeInMillis()+3*dayMs);
                    } break;
                    case (Calendar.WEDNESDAY) : {
                        week[0] = new Date(cal.getTimeInMillis()-2*dayMs);
                        week[1] = new Date(cal.getTimeInMillis()-dayMs);
                        week[2] = new Date(cal.getTimeInMillis());
                        week[3] = new Date(cal.getTimeInMillis()+1*dayMs);
                        week[4] = new Date(cal.getTimeInMillis()+2*dayMs);
                    } break;
                    case (Calendar.THURSDAY) : {
                        week[0] = new Date(cal.getTimeInMillis()-3*dayMs);
                        week[1] = new Date(cal.getTimeInMillis()-2*dayMs);
                        week[2] = new Date(cal.getTimeInMillis()-1*dayMs);
                        week[3] = new Date(cal.getTimeInMillis());
                        week[4] = new Date(cal.getTimeInMillis()+1*dayMs);
                    } break;
                    case (Calendar.FRIDAY) : {
                        week[0] = new Date(cal.getTimeInMillis()-4*dayMs);
                        week[1] = new Date(cal.getTimeInMillis()-3*dayMs);
                        week[2] = new Date(cal.getTimeInMillis()-2*dayMs);
                        week[3] = new Date(cal.getTimeInMillis()-dayMs);
                        week[4] = new Date(cal.getTimeInMillis());
                    } break;
                    case (Calendar.SATURDAY) : {
                        week[0] = new Date(cal.getTimeInMillis()-5*dayMs);
                        week[1] = new Date(cal.getTimeInMillis()-4*dayMs);
                        week[2] = new Date(cal.getTimeInMillis()-3*dayMs);
                        week[3] = new Date(cal.getTimeInMillis()-2*dayMs);
                        week[4] = new Date(cal.getTimeInMillis()-dayMs);
                    } break;
                    case (Calendar.SUNDAY) : {
                        week[0] = new Date(cal.getTimeInMillis()-6*dayMs);
                        week[1] = new Date(cal.getTimeInMillis()-5*dayMs);
                        week[2] = new Date(cal.getTimeInMillis()-4*dayMs);
                        week[3] = new Date(cal.getTimeInMillis()-3*dayMs);
                        week[4] = new Date(cal.getTimeInMillis()-2*dayMs);
                    } break;
                }
                wtth = new WorkerTasksTableHandler(workerFrame,week);
                wtth.createTable(); // sets model on JTable
            }
        });
    }

    public void reportTask() {
        wtth.reportTasks(); // simple DB insert
    }
}

使用 NetBeans 分析器: 拍摄日期:2010 年 2 月 28 日星期日 14:25:16 CET 文件:C:...\private\profiler\java_pid4708.hprof 文件大小:72.2 MB

Total bytes: 62 323 264
Total classes: 3 304
Total instances: 1 344 586
Classloaders: 18
GC roots: 2 860
Number of objects pending for finalization: 0

【问题讨论】:

  • 你提到了一个数据库......你能告诉我们更多关于它在做什么,你正在使用什么数据库,如果你正在关闭你的结果集等等......似乎更有可能是罪魁祸首。另外,您是否保留对旧模型的任何引用?您将多少数据加载到模型中?你的表有自定义渲染器吗?
  • 注意:过去我已经将包含数千行的 JTable TableModels 换掉了......并且从未增加默认的堆大小。所以这里还有别的东西在起作用。
  • 更有可能是您的代码而不是 JVM 出现问题。 Java 肯定有错误,但自 Java 1.0 以来就已经找到并修复了一个如此基础的类中的内存“泄漏”。
  • 好吧,您可以看到代码,但恐怕您不想:-(很多数据库选择等,我将它们作为查询进行了测试,以查看它们带来正确的结果我使用 MySQL 服务器我不关闭 ResultSets,我应该吗?:) 我不保留对旧模型的引用,如果您的意思是将它们保存到数组或列表中,或者我有 JSpinner 的自定义表格渲染器
  • 哦,我不想看代码。那是你的工作。不要关闭结果集?天啊。我希望微笑是讽刺的。如果没有,你应该更仔细地检查你的东西。

标签: java heap-memory


【解决方案1】:

您是否针对此运行过像 YourKit 这样的分析器?我怀疑它会显示一些内存泄漏,因为引用在应该被释放时被持有。请注意,System.gc() 是对 JVM 的提示,不会强制执行 GC 循环。

或者,您的应用程序可能只是需要比 JVM 允许分配更多的内存。 JVM 最多只能分配一个默认最大值(取决于您的平台)。尝试通过以下方式增加:

java -Xmx256m {classname}

等等。看看这是否能永久解决问题。如果没有,则表明存在内存泄漏。

【讨论】:

  • 尝试使用捆绑在JDK_1.6.10的bin目录下的免费JVisualVM
  • 您可以下载评估 YourKit,这可能足以诊断您的问题(15 天)
  • 我使用了 JVisualVM(顺便说一句,好东西),它说我只使用 20-30 mb 的堆,Windows 任务管理器显示 80-90 ...
  • @Miso:堆并不是 Java 占用的唯一内存。另外:这种差异对您的问题无关紧要:使用内存分析器。
  • 我确实请看一下我在下面发布的SS
【解决方案2】:

阅读 Veijko Krunic 的精彩论文 How to Fix Memory Leaks in Java。他建议了类似问题的诊断路径。

【讨论】:

    【解决方案3】:

    显然,日历上的每次“点击”都会创建一些对象,这些对象没有被垃圾收集,因此内存使用量增加并最终导致崩溃。在没有实际运行代码的情况下,通过查看您的代码示例,我想说一个可能的罪魁祸首是这里创建的匿名内部类:

    monthView.getSelectionModel().addDateSelectionListener(new DateSelectionListener() {
      ...
    }
    

    您创建的新 DateSelectionListener 将引用它(WorkerMonthViewHandler),如果不了解更多有关如何使用 initMonthView 的信息,我无法确切了解这将如何导致问题,但我发现重构匿名内部类已创建作为摆动对象的侦听器,过去曾帮助识别并最终解决了许多内存泄漏。只要他们正在侦听的 swing 对象存在,侦听器就会存在,因此即使在您创建新的 WorkerMonthViewHandler 之后,假设原始的 swing JTable 仍然相同,它也会一直存在。

    如果您想进一步阅读此内容,请尝试http://www.javalobby.org/java/forums/t19468.html

    希望这会有所帮助。

    【讨论】:

    • 如果我理解正确,我应该只创建一个 DateSelectionListener 实例(例如在构造函数中)?不过你可能是对的,我 99% 肯定它的 Swing
    • 将侦听器分解为它自己的类,这将使它持有的引用更清楚,并可能启发您了解内存泄漏的来源。只创建一个然后不断将其添加到组件中可能无法解决您的问题。将其重构为自己的类后,您可能仍需要进行其他更改,但希望它会更清楚发生了什么。
    • @Miso,不是真的...真正的问题是,如果您一遍又一遍地添加侦听器,那么您还需要再次删除它们。我在基于 Swing 的应用程序中发现的最大内存泄漏源是代码忘记清理其侦听器。匿名内部类加剧了这个问题,因为它们持有对其外部实例的引用。
    • ...您从分析中发现。 :) 记住仍然有用。
    • 是的:) 但我实际上并没有删除它们,我只是没有创建这么多......
    【解决方案4】:

    这里有一个疯狂的猜测,但正如我在 C# 中看到的那样,您的日历/控件事件处理程序是否持有对未正确清理的数据的引用?确保在不再需要句柄时将其清空,因为循环依赖会导致大量泄漏。

    【讨论】:

    • 嗯,我有 DateSelectionListener for DateSelectionEvent 每次更改日期时,都会创建新的 TableMoel(Listener 会这样做,并且还从 DB 加载数据)
    • 非常仔细地查看所有这些处理程序中发生的事情。如果任何引用保存在处理程序中并且处理程序由对话框保存,则不会释放该内存。这就是使用 GC 语言的弊端,不能只删除指针,看看哪里弹出空指针异常。
    • 我添加了一些代码,请看一下,也许你会发现一些错误
    【解决方案5】:

    这听起来完全像是 Swing 组件的内存泄漏。有一些组件被多次实例化,并附加到其他东西(通常作为侦听器),因此它不能被垃圾收集,因为仍然有对它的有效引用。正如其他人指出的那样,任何分析器都会帮助您找到源头。

    在应用程序开始时拍摄堆快照。然后在您按下按钮大约十次后,再拍摄一个堆快照并进行比较。应该有一组你知道不应该仍然在内存中的对象,但它们是。然后你可以找出是什么持有对它的引用,并修复它们。

    【讨论】:

    • 所以我一直在调查看一下图片,我像疯子一样点击将 CellEditor 设置为我的自定义 SpinnerEditor 的单元格(换句话说,双击单元格来处理 JSpinner)。 ...查看已使用/分配的堆,实际上它只会上升....i49.tinypic.com/160wx0g.jpg
    • 对,这更能证明某些对象没有被释放。 (过去,当我从外部引用 HashMap 中的对话框时,这发生在我身上,所以即使我处理了对话框,仍然有对它的引用,Java 不会 GC 它。)使用类似的东西YourKit 或 JProfiler 并对两个不同的堆转储进行比较。 (JProfiler 会告诉您堆之间有哪些新对象,因此您可以快速忽略那些属于应用程序一部分的实例,而不是您正在做的事情。)
    【解决方案6】:

    很难从代码中看出,但你有没有机会不断添加 FlaggedDates ?

    public void initMonthView() {
        List<Task> tasks = wops.getWorkerTasks(workerFrame.getWorker()); // db select
        for (Task task : tasks) {
            if (!monthView.getSelection().contains(task.getPlannedStart())) { 
                monthView.addFlaggedDates(task.getPlannedStart());
                monthView.addFlaggedDates(task.gePlannedEnd()); // not really important
            }
        }
    

    【讨论】:

    • 不,只有第一次初始化 JXmonthView (以及在 RESET JButton 上),所以不经常 :)
    【解决方案7】:

    您不需要每次都精确到毫秒的日期。在我看来,一周中的同一天就足够了。

    就我个人而言,我会想办法预填充此日历并对其进行缓存。无需每次都重新创建。这值得一试。如果您每天都重复使用它,为什么每次都重新创建它?让定时器在每天午夜重新填充它。将其设为只读并允许所有用户共享。

    也不需要每次都使用“新”日历。我会这样做:

    Calendar cal = Calendar.getInstance();
    

    让工厂发放。

    我还建议您花时间查看 JODA 之类的库。它肯定比你在这里做的更有效率。

    更新:也许this 可以帮助您找出内存泄漏。至少它是从哪里开始寻找的清单。

    【讨论】:

    • 你的意思是像创建类属性并在构造函数中声明它?
    【解决方案8】:

    先生们,谢谢大家的回答。 感谢每一个回复

    我添加了我自己的更明显,不能真正选择一个正确的。

    因此,当您更仔细地查看我在原始问题中发布的代码时,您会发现这两行

            wtth = new WorkerTasksTableHandler(workerFrame,week);
            wtth.createTable(); // sets model on JTable
    

    结果是,正如你们中的一些人所注意到的,每次创建 NEW TableModel 时都会使用它自己的 Listener。 所以现在我只重新加载数据(不是整个模型)并使用原始监听器。

    看一下图片,现在它消耗的 RAM 太少了,而且 GC 确实有效 :)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-04-16
      • 2011-09-23
      • 1970-01-01
      • 1970-01-01
      • 2019-01-16
      • 2016-06-29
      相关资源
      最近更新 更多