【问题标题】:splitting functions in parts is good for memory management?将功能分成几部分对内存管理有好处吗?
【发布时间】:2021-08-09 11:25:07
【问题描述】:

我一直在尝试学习垃圾收集器,以制作对内存更友好的 Java 应用程序。 我有一个关于垃圾收集器的问题,假设我们有这样的功能:

public void personWork(){
List<Person> firstPersonList = personService.getFirstPersonList();
List<Person> secondPersonList = personService.getSecondPersonList();

// Do something with firstPersonList
..
..
..
// Do something with secondPersonList
..
..
..

 finish the function
}

我也可以用这段代码做同样的事情

public void personWork(){
List<Person> firstPersonList = personService.getFirstPersonList();
List<Person> secondPersonList = personService.getSecondPersonList();

firstPersonListJob(firstPersonList);

secondPersonListJob(secondPersonList);

//finish the function
}

public void firstPersonListJob(List<Person> firstPersonList){
 // do someting with firstPersonList
}

public void secondPersonListJob(List<Person> secondPersonList){
 // do something with secondPersonList
}

终于我也能做到了:

public void personWork(){


firstPersonListJob();

secondPersonListJob();

//finish the function
}

public void firstPersonListJob(){
List<Person> firstPersonList = personService.getFirstPersonList();
// do someting with firstPersonList
}

public void secondPersonListJob(){
List<Person> secondPersonList = personService.getSecondPersonList();
 // do something with secondPersonList
}

据我从垃圾收集器中了解到,为了垃圾收集器清理堆,必须从堆栈中删除堆内存中值的引用。考虑到第一种和第二种方法不利于内存管理,因为 2 List 一直存在到完成,但在最后一种方法中,当函数 1 完成时,firstPersonList 也会被删除。

实际上我不确定哪种方法更适合内存管理,或者我是否关心这种事情?

【问题讨论】:

  • 我看不出有什么理由去在意用这些方法取悦 GC。无论如何,一旦它们超出范围,GC 就不会收集您的陈旧引用。运行 GC 本身就是一项昂贵的任务,因此它只会在最佳时间运行是有道理的。
  • 在您的第一个示例中,您早在需要之前就实现了secondPersonList,即在处理firstPersonList 时不需要它。因此,最后一个 sn-p 避免了两个列表同时在内存中,您也可以通过重新排序代码来使用早期的变体来实现这一点。但是您可能会注意到,最后一种方法如何自动导致更好的设计。所以使用更小的函数,不管垃圾回收。另请阅读Separation of concerns

标签: java garbage-collection


【解决方案1】:

为了了解您的原始方法与拆分为多个部分的方法的影响,您需要考虑多个方面:

  • 您使用的硬件限制是什么?运行应用程序的硬件上的 CPU 负载是多少?您有多少可用内存?
  • 您当前的堆和 GC 设置是什么?
  • 这些方法运行多长时间?它们运行的​​时间越长,您在实例被取消引用并有资格收集之前持有引用的时间就越长 - 几毫秒可能是微不足道的(但可能取决于您的应用程序做什么以及期望是什么,例如从结束用户或服务水平协议的观点),如果您正在查看一个长时间运行的循环或线程,它将运行数小时或数天,那么可能需要关注一个问题
  • 原始代码与建议替代代码的堆使用情况如何?
  • 关于垃圾收集的相同问题(您是否在启用 gc 日志的情况下运行应用程序以查看行为?)

一般来说,请考虑避免会显着影响 GC 行为的模式,例如紧密循环在短时间内实例化数百万个实例。否则,您需要分析您的代码和/或查看垃圾收集日志以确定您是否确实有问题需要关注。

【讨论】:

    【解决方案2】:

    据我从垃圾收集器了解到,为了垃圾收集器清理堆,必须从堆栈中删除堆内存中值的引用。

    这并不完全正确。

    实际的标准是堆中的对象是否可达。在这种情况下,它们是否可以通过堆栈变量访问。

    reachable(JLS 12.6.1)的实际定义如下:

    可达对象是可以在任何潜在的持续计算中从任何活动线程访问的任何对象。”

    现在,从线程的堆栈(和寄存器)中明确删除这些列表的引用就足以使它们无法访问。但是如果 JVM 可以确定当前线程不会再次使用该变量,则列表也可能无法访问。

    因此,在您的代码的第一个版本中,JVM 可以确定 firstPersonListfirstPersonListJob(...) 调用之后从未使用过,因此在 @ 987654323@ 已拨打电话!

    考虑到第一种和第二种方法不利于内存管理,因为 2 List 一直存在到完成,但在最后一种方法中,当函数 1 完成时,firstPersonList 也将被删除。

    正如我所解释的,这种推理的前提不一定正确。这取决于 GC + JIT + 的复杂程度。

    但假设它是真的。然后呢?

    解决方案包括。

    1. 您的最终版本(或次要版本)是一种解决方案。
    2. 第二种解决方案是在调用firstPersonListJob(...) 之后将null 分配给firstPersonList
    3. 最终的解决方案是不要管它。 (问:什么?答:继续往下看!)

    除非这些列表非常大,否则firstPersonList 是否立即被垃圾收集......或稍晚一点都不太可能产生任何影响。这些物品很可能在伊甸园空间中,现在与下次收集的成本会很小。

    唯一重要的情况是列表是否足够大以至于它们占据了总堆空间的很大一部分。但是,如果您处于这种情况,您还可以做其他事情:

    • 增加最大堆空间(假设您有足够的 RAM)
    • 修改应用程序逻辑,使列表不需要太大(例如更好的数据库查询),或者
    • 查找内存泄漏等导致老年代填满的问题。

    作为一般规则,Java 是非常需要内存的。如果垃圾收集器有足够的可用空间,它们的工作效果最好。 GC 的成本主要用于处理不是垃圾的对象。因此,自然地,如果垃圾与非垃圾的比例较小,则 GC 会为相同数量的回收空间付出更多的努力。如果您尝试挤压 Java 堆限制以“节省空间”,则会消耗 CPU 时间,您会获得更多的主要收集,并且您会面临更大的风险,即错过“暂停时间”目标。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-10-07
      • 2013-05-06
      • 2018-07-13
      • 2017-03-13
      • 2017-10-02
      • 2011-02-08
      • 2016-10-15
      • 1970-01-01
      相关资源
      最近更新 更多