为什么短时间和长寿命对象在垃圾收集方面有所不同?

我经常读到,在Sun JVM中,短期对象(“相对较新的对象”)可以比长寿命对象(“相对较旧的对象”)更有效地进行垃圾收集。

  • 为什么?
  • 这是特定于Sun JVM还是来自一般的垃圾收集原则?

大多数Java应用程序创建Java对象,然后相当快地丢弃它们,例如。 你在方法中创建一些对象,然后一旦退出方法,所有对象都会死亡。 大多数应用都采用这种方式,大多数人倾向于以这种方式编写应用。 Java堆大致分为3个部分,永久,旧(长寿)代和年轻(短期)代。 年轻人进一步分为S1,S2和伊甸园。 这些只是堆积物。

大多数物体是在年轻人中创造的。 这里的想法是,由于物体的死亡率很高,我们很快创造它们,使用它们然后丢弃它们。 速度至关重要。 当您创建对象时,年轻的gen会填满,直到发生次要GC。 在次要GC中,所有活着的对象都从eden复制并说S2到S1。 然后,’指针’在eden和S2上rest。

每个副本都会使对象老化。 默认情况下,如果一个对象存活32个副本即。 32个小型GC,然后GC表明它将会存在更长时间。 所以,它的作用是通过将它移到老一代来保持它。 老一代只是一个很大的空间。 当旧的gen填满时,旧的GC会发生完整的GC或主要的GC。 由于没有其他空间可供复制,因此GC必须紧凑。 这比次要GC慢很多,这就是为什么我们要避免更频繁地这样做。

您可以使用调整tenuring参数

java -XX:MaxTenuringThreshold=16 

如果你知道你有很多长寿的物体。 您可以打印应用程序的各个年龄段

 java -XX:-PrintTenuringDistribution 

(参见上面对更一般的GC的解释..这个答案为什么新的GC比旧的便宜)。

可以更快地清除eden的原因很简单:算法与在伊甸园空间中存活GC的对象数量成正比,与整个堆中的活动对象数量不成比例。 即:如果您在伊甸园中的平均物体死亡率为99%(即:99%的物体不能在GC中存活,这不是exception),您只需要查看并复制1%。 对于“旧”GC,需要标记/扫描整个堆中的所有活动对象。 这要贵得多。

这是世代垃圾收集 。 它如今被广泛使用。 在这里查看更多: (wiki) 。

从本质上讲,GC假设新对象比旧对象更容易无法访问。

在这种现象中,“大多数物体都会年轻”。 许多对象在方法内创建,并且从不存储在字段中。 因此,一旦该方法退出这些对象“死亡”,因此是下一个收集周期的候选集合。

这是一个例子:

 public String concatenate(int[] arr) { StringBuilder sb = new StringBuilder(); for(int i = 0; i < arr.length; ++i) sb.append(i > 0 ? "," : "").append(arr[i]); return sb.toString(); } 

方法返回后, sb对象将变为垃圾。

通过将对象空间分成两个(或更多)基于年龄的区域,GC可以更有效:GC不经常扫描整个堆,而是经常只扫描托儿所(年轻物体区域) – 这显然需要很多完整堆扫描的时间更少。 较旧的对象区域扫描频率较低。

更有效地管理年轻对象(不仅收集;对年轻对象的访问也更快),因为它们被分配在特殊区域(“年轻一代”)。 这个特殊区域效率更高,因为它是“一次性”收集的(所有线程都停止了),收集器和应用程序代码都不必处理来自另一个的并发访问。

这里的权衡是在收集“有效区域”时停止“世界”。 这可能会引起明显的停顿。 JVM通过保持有效区域足够小来保持较短的暂停时间。 换句话说,如果有一个有效管理的区域,那么该区域必须很小。

适用于许多程序和编程语言的一种非常常见的启发式方法是许多对象非常短暂,并且大多数写访问都发生在年轻对象(最近创建的对象)中。 编写不能以这种方式工作的应用程序代码是可能的,但这些启发式算法在“大多数应用程序”中“大部分都是真的”。 因此,将年轻物体存储在有效管理的区域中是有意义的。 这就是JVM GC的作用,这就是为什么这个有效区域被称为“年轻一代”的原因。

请注意,有些系统可以“高效”处理整个内存。 当GC必须运行时,应用程序将“冻结”几秒钟。 这对于长期计算是无害的,但对交互性是有害的,这就是为什么大多数现代GC启用的编程环境使用具有有限大小的年轻代的代际GC。

这是基于观察到物体的寿命随着年龄增长而上升的观察。 因此,一旦达到特定年龄,将对象移动到频率较低的池中是有意义的。

这不是程序使用内存的基本属性。 您可以编写一个病毒程序,将所有对象保持很长时间(并且所有对象的时间长度相同),但这往往不会偶然发生。

JVM(通常)使用分代垃圾收集器。 这种收集器根据其中对象的年龄将堆内存分成几个池。 这里的推理是基于观察到大多数对象都是短暂的,所以如果你在一个带有“年轻”对象的内存区域上进行垃圾收集,你可以回收相对更多的内存,而不是你在“旧”垃圾收集时进行垃圾收集。 “对象。

在Hotspot JVM中,新对象在所谓的Eden区域中分配。 当这个区域填满时,JVM将扫描伊甸园区域(这不会占用太多时间,因为它不是那么大)。 仍然活着的物体被移动到幸存者区域,剩下的物体被丢弃,为下一代腾出了伊甸园。 当Eden集合不足时,垃圾收集器会转移到老一代(需要更多工作)。

所有的GC都是这样的。 基本思想是您尝试减少每次运行GC时需要检查的对象数量,因为这是一项相当昂贵的操作。 因此,如果您有数百万个对象,但只需要检查一些,这比检查所有对象要好。 此外,GC的一个function在您的手中播放:临时对象(任何人都无法访问),在GC运行期间没有成本(好吧,我们暂时忽略finalize()方法)。 只有幸存的对象需要花费CPU时间。 接下来,观察到许多物体是短暂的。

因此,对象是在一个小空间中创建的(称为“Eden”或“young gen”)。 过了一会儿,所有可以到达的对象都被复制(=昂贵)在这个空间之外,然后空间被宣告为空(因此Java有效地忘记了所有无法到达的对象,因此它们没有成本,因为它们没有必须被复制)。 随着时间的推移,长寿命对象被移动到“较旧”的空间,较旧的空间不经常扫描以减少GC开销(例如,每运行N次,GC将运行旧空间而不是伊甸园空间)。

只是为了比较:如果你在C / C ++中分配一个对象,你需要为每个对象调用free()和析构函数。 这就是为什么GC比传统的手动内存管理更快的原因之一。

当然,这是一个相当简化的外观。 今天,GC的工作处于编译器设计的水平(即由极少数人完成)。 GCs采用各种技巧使整个过程高效且不引人注意。 有关指示,请参阅维基百科文章 。