在建模时,通常需要优化模型的性能。要么通过提高内存利用率要么提高CPU的利用率。
然而,仅通过代码审查或手动试错更改,几乎是不可能完成的。幸运的是,Java应用程序有一个特定的工具可以帮助您,让工作变得更轻松。
因此,对于如何使用分析器分析模型性能这一主题,我发布了由 2 篇文章组成的系列文,本文是其中的第1篇,主要介绍了如何改善内存消耗和如何检测内存泄漏。
下面让我们看一个检测内存泄漏和内存消耗的示例。
一、示例
让我们看看下面这个超级简单的仿真模型。我们有一个超基础的仿真模型系统,其中包含智能体进入队列,接受服务,然后退出系统。如果智能体在系统中等待太长时间,它会过早离开队列,无法获得服务,然后退出系统。
我们想测量得到服务的智能体在系统中花费的时间,因此我们在服务块前后为过早退出系统的对象添加了时间测量开始和时间测量结束块。
这种基本的服务流程仿真可以代表许多现实生活中的流程,例如,呼叫中心,呼叫者只愿意为服务等待一定的时间,或者洗车中心,潜在的顾客只会在队列中等待几分钟,然后决定跳过洗车日。
附言:如果您认为自己是AnyLogic高级用户,您应该能够发现上述模型中的问题以及它将对您的模型性能产生的影响。 花点时间做一个预测,并在下面的评论部分写下你的预测结果。
该模型非常简单,我们计划对其进行大的改进,添加大量细节和功能。但在此之前,让我们快速运行一整年,并检查结果。
这就是。。。
一切看起来都很好,模型在不到9秒的时间内完成了全年,结果看起来不错。。。但是等等,看看消耗500MB内存。我们甚至还没有开始为模型添加重要的细节!在我们将智能体的数量增加了一倍,并添加了大量的统计数据和额外的逻辑之后,我们究竟将如何运行该模型。。。。
二、查找内存泄漏
幸运的是,优化java应用程序性能已经存在很长时间了。我们不需要依靠自己的直觉或反复试验来尝试和优化我们的仿真模型。
有许多商业工具可以用于Java应用程序分析,我更喜欢eJ Technologies的一个叫做Jprofiler的工具。
然而,如果您是初学者,您可能不想在一个您不知道如何使用的工具上花钱。幸运的是,像Visual VM这样的免费应用程序可以在这里下载到Mac和Windows上。
提示仅供参考:使用VisualVM Monitor视图监视应用程序
CPU使用率:此图表绘制了一段时间内的CPU使用率,用于指示模型需求何时使CPU过载
堆:堆图显示总堆大小和当前使用的堆数量。堆类似于内存
类:类图显示加载类和共享类总数的概述。
线程:线程图显示了应用程序JVM中活动线程和守护进程线程数量的概述。如果您想在特定时间点捕获和查看应用程序线程上的精确数据,可以使用VisualVM进行线程转储。
什么是可视虚拟机
使用Visual VM检测内存泄漏
使用以下步骤对应用程序进行内存分析,或观看此处的视频以获取逐步说明
1) 运行模型,直到内存消耗出现问题,然后暂停模型。在我们的例子中,这是在模型端
2) 打开Visual VM应用程序。
3) 在左侧窗格中,您将看到两个应用程序链接到您的模型。AnyLogic数据库以及模型本身。
4) 双击与模型关联的应用程序,然后选择右侧窗格中的“monitor”选项卡
从概述统计数据中可以看出,我们使用的是零CPU,这在模型完成运行时是有意义的。然而,我们几乎消耗了所有可用的500 MB内存。
提示:使用VisualVM视图监视应用程序
CPU使用率:此图表绘制了一段时间内的CPU使用率,用于指示模型需求何时使CPU过载
堆:堆图显示总堆大小和当前使用的堆数量。堆类似于内存
类:类图显示加载类和共享类总数的概述。
线程:线程图显示了应用程序JVM中活动线程和守护进程线程数量的概述。如果您想在特定时间点捕获和查看应用程序线程上的精确数据,可以使用VisualVM进行线程转储。
5) 导航到Sampler页面,在sample部分选择memory,暂停分析器并选择堆转储
提示:内存分析
在内存分析模式下,VisualVM显示由应用程序加载的每个类分配的对象总数。对于Java虚拟机(JVM)中加载的每的大小和数量。在分配新对象和加载新类时,结果会自动更新。分配的字节也显示为一个图形,表示字节的百分比以及每个类分配的字节总数
6) 在堆转储页面上,导航到对象视图以分析堆转储
现在,这是一个有点棘手的地方,您需要获得一些快速查找泄漏的经验。最好的建议是从dump的顶部开始,按照自己的方式工作,直到找到有意义的东西,然后可以更改模型。鉴于AnyLogic包含许多您无法控制的内部功能和特性,您可能经常会遇到无法进行任何更改的可能问题。继续,直到找到所创建模型的部分。
为了更快地解决问题,您需要计算对象的保留大小,并对其进行相应排序。默认情况下不会计算保留大小,但按此列排序将为您计算保留大小。
这可能需要一段时间。。。因此,在屏幕底部的状态栏中跟踪进度。。。
完成后,您将看到以下内容。
有98k个LinkedHashMap对象,占500MB大小的大部分
有一个TimeMeasureStart对象,它也有大约500MB的内存
还有98k LinkedHashMap$Entryfields,也有大约500MB的内存
系统中有97k辆汽车,也占了大约500MB的内存
有几个迹象表明,这些事情是相关的,内存大小的保留。它必须是相关的,不能相互排斥;否则总内存消耗将远远超过2 GB。实例数量也令人怀疑。
奇怪的是,为什么有这么多关于Car对象的引用。即使所有的车都在水槽里被毁了。。。当模型结束时,系统中只有四辆车。。。。(第一个红灯)
让我们从占据最大尺寸的对象开始,在本例中是LinkedHashMap。如果我们深入研究,我们会尝试查看哪些对象保留了对此对象的引用。
正如所怀疑的那样,TimeMeasureStart保留了对这个LinkedHashMap的引用。
让我们看看TimeMeasureStart。由于这个对象只有一个实例,我们将查看它的字段,看看是什么导致这个对象消耗了这么多内存。。。再往下看。。它似乎与我们在上一个屏幕截图中看到的消耗了最多内存的hashMap(#1551)相同。
现在我们知道TimeMeasureStart对象保留了太多对某个对象的引用。。。因为我们知道这是衡量汽车进入时间的,我们在系统中有97976辆汽车,而预期只有4辆,我们可以假设它保留了对汽车的引用,即使它们在水槽中被摧毁了。。。
当你意识到所有通过队列超时退出的汽车仍然被TimeMeasureStart作为参考。。。因为他们从来没有通过过TimeMeasurerEnd。
模型内存中的汽车总数等于系统中当前的汽车数量+通过队列超时退出的汽车数量。
三、修复泄漏
现在,我们确切地知道问题在哪里,修复泄漏非常容易。我们只需在队列超时出口连接器中添加一个timemeasurend。
现在运行该模型,我们看到内存消耗已从500 MB下降到约250MB
哪一个更好,但不是很好。。。那现在怎么办?
关于Java,需要记住的最后一件事是垃圾收集器(GC)仅在需要时运行。垃圾回收是指清除内存中没有活动引用的所有对象。因此,所有正在使用的东西都被保留,那些不需要的东西被废置。由于我们只有50%的内存使用率,GC可能就是这种情况。
在Visual VM内进行分析之前,可以强制垃圾收集。
这样做内存消耗只有4%,这很可能是运行模型所需的最低内存消耗。在我们模型的早期版本上强制垃圾收集会导致内存减少,因为所有汽车对象在TimeMeasureStart对象内都有活动引用。
您可以在此处下载示例模型。