美文网首页Java学习笔记程序员Java 杂谈
[翻译]Garbage Collection in Java

[翻译]Garbage Collection in Java

作者: 徐士林 | 来源:发表于2018-03-10 16:22 被阅读51次

    原文、侵删
    关于标记清除算法的介绍大多都是理论性的。当理论算法付诸实践的时候需要做大量调整来满足现实世界的情景和要求。

    碎片和压缩碎片

    无论垃圾清理的过程何时出现,JVM都必须确保这块充满不可达对象的区域是可以重新使用的。这可能(最终确实会发生)会导致内存碎片,与磁盘碎片类似,内存碎片会导致下列两个问题

    • 写操作会更加耗时,因为查找下一个足够大小空间的区域更加耗时。
    • 当创建对象时,JVM会在连续的块中分配内存。所以当内存碎片达到一个点时,没有一块空间足够容纳新建对象的时候,则会发生分配错误

    为了避免这样的问题,JVM需要保证内存碎片不会失控。所以JVM中不只有标记和清理阶段,内存的整理也是GC的一部分。此过程会将所有可达对象重新分配使其彼此相邻,及减少内存的碎片化


    fragmented-vs-compacted-heap.png

    分代理论

    正如我们之前提到的,GC会导致所有的应用程序线程停止运行。同样的,需要收集的对象越多,收集时间就会越长。有没有可能使用更小的内存空间呢?基于研究发现,应用程序的中的大部分内存分配可分为两类

    • 大多数对象都会很快的变为不可达对象
    • 那些对象通常不会存活很长时间
      这个结论促成了分代理论的诞生。基于分代理论,VM中的内存分为年轻代老年代Old Generation 也被成为Tenured
      object-age-based-on-GC-generation-generational-hypothesis.png

    拥有这样独立且单独收集的区域可以实现很多不同的垃圾收集算法,这些算法在改善GC性能方面已经取得长足的进步。

    同时分代收集的方法也会面临一些问题。如,不同代的对象实际上可能有相互之间的引用,而当收集一个代时,这些引用可能被视为跟对象。
    但是更重要的是,世代假设实际上可能不适合某些应用。因为某些GC算法是针对“存活时间非常短”或者“存活时间非常久”做出的优化。所以JVM针对那些存活时间中等的对象的表现不是很好。

    内存池

    下图是关于堆中内存各部分的划分。What is not so commonly understood is how Garbage Collection performs its duties within the different memory pools. 请注意,在不同的GC算法中,各部分的实现也有一些差别。但是我们在这里仍然只是保持概念不变。


    java-heap-eden-survivor-old.png

    Eden

    Eden区域是程序创建对象的首选区域。由于多线程通常同时创建大量对象,为了加快线程中对象的分配,Eden进一步分为多个驻留在Eden区域中的线程本地分配缓冲区(TLAB)。这些缓冲区允许JVM直接在相应的TLAB中分配对象,从而避免了昂贵的线程同步的代价。

    当TLAB中不能分配对象时(一般是因为缓冲区空间不够),对象就会分配在共享的Eden区域。如果Eden区空间不够,年轻代GC就会被触发。如果GC没有有效的回收Eden区域内存,对象就会在老年代分配。

    当Eden区域被收集时,GC会从跟节点遍历所有可达的对象并标记它们为存活对象。

    我们之前已经提到,对象可能会跨代引用,所以遍历时必须检查所有从其他代到Eden区域的引用。但是这样就破坏了分代的意义。JVM实现了一个技巧:card-marking。实际上,JVM会把老年代引用到Eden区的对象标记为“脏对象”。这篇文章介绍了更多关于card-marking的细节。

    TLAB-in-Eden-memory.png

    当标记阶段完成后,所有存活的对象会被移动到Survivor区域。这是,整个Eden区域都视为可用的。这个过程被称为标记复制算法。所有存活的对象都被标记,并且被复制到survivor区域。

    Survivor区域
    在Eden旁边分了两个Survivor区域,成为from区域和to区域。注意这两个Survivor区域总有一个是空的。

    空的Survivor区域会保存下一次GC时存活的对象。所有在年轻代存活的对象(包括Eden和其中一个Survivor区中所有的对象)都被拷贝到另一个Survivor区域。


    how-java-garbage-collection-works.png

    对象在两个Survivor区域中来回复制几次,每复制一次对象的年龄就会增加一岁,当对象足够“老“,就被提升到老年代。基于分代理论,存活一段时间的对象很可能会存活很长时间。

    实际上,提升到老年代的年龄有JVM通过-XX:+MaxTenuringThreshold参数指定。如果设置此参数值为0,对象会立即提升到老年代而不会拷贝到Survivor区域。默认情况下,JVM设置该参数的值为15,这个值也是HotSpot支持的最大值

    如果Survivor空间不足够容纳年轻代所有存活的对象,对象提升可能提前发生。

    老年代

    老年代的内存空间实现的要更加复杂。因为我们认为老年代的对象是可以长期存活的,所以这里不会发生标记和拷贝的事情。但是对象也会移动以减少内存碎片化。老年代的空间清理算法实现上通常有些微差别。但是原则上,都会采取如下步骤。

    • 标记所有可达对象
    • 删除不可达对象
    • 将存活的对象复制到连续的空间以便压缩老年代
      从上边的描述来看,老年代GC必须明确的处理碎片问题。

    永久代

    Java 8之前存在一个特殊的空间就是永久代。这块空间在Java 8改为metadata空间。一些其他的事情例如底层的字符串保存在永久代。但是这块内存却被认为给程序开发者带来很多的麻烦,因为很难预测这块区域需要多少内存。这里如果出现了OOM,除非能够确定问题的原因是内存泄漏,否则只能增加这块内存。

    元数据

    由于预测元数据的需求是一项复杂而且困难的工作,所以永久代被移除并且推出了metadata区域。而随着永久代的移除,很多对内存消耗的事情都被移到常规的Java堆中。

    类的定义现在被加载到metadata中,它位于本机内存,不会干扰到常规Java堆的大小。而且Metadata的大小也只限于Java进程使用本机内存量的限制。请注意这样看似无限的空间并不是没有成本,让Metadata区域不受控制的增长可能会使用大量的交换区空间或者本地的内存分配失败问题。

    Minor GC vs Major GC vs Full GC

    根据GC清理内存空间的不同,GC事件也被分为Minor、Major和Full GC。我们在本节介绍他们的区别。

    What typically is relevant is whether the application meets its SLAs, and to see that you monitor your application for latency or throughput. And only then are GC events linked to the results. What is important about these events is whether they stopped the application and how long it took.

    But as the terms Minor, Major and Full GC are widely used and without a proper definition, let us look into the topic in a bit more detail.

    Minor GC

    Minor GC收集年轻代相关垃圾。这个定义非常清楚。但是处理Minor GC事件时,还有一点有趣的事情应该注意

    • 当JVM无法为新对象分配空间时会触发Minor GC。例如,Eden区域没有可分配空间。所以内存分配更频繁,Minor GC就出现的更频繁。
    • 在Minor GC事件中,将不涉及老年代空间。那些被老年代对象引用的Eden对象被是为GC root。从Eden区到老年代的引用在标记阶段被忽视。
    • 与常规的观念相反,Minor GC确实会触发stop-the-world阶段,即暂停所有应用程序的线程。如果Eden区域的对象大部分都被认为垃圾,那么就没有很多的对象被复制到Survivor/Old空间,所以暂停事件可以被忽略的。如果情况恰好相反,大部分对象都不是垃圾,那么Minor GC就会花费相当多的时间。

    因此对于Minor GC的定义很简单,就是对年轻代的收集。

    Major GC vs Full GC

    应该指出的是,对于这两个术语并没有官方的解释。但乍看之下,对于这两个术语的定义也应该像Minor GC的定义一样简单

    • Major GC清理老年代
    • Full GC清理真个内存空间,包括年轻代和老年代

    但是仍然有一点复杂和迷惑性。许多Major GC都是由Minor GC引发的。所以在很多情况下分离这两个算法都是不可能的。换句话说,G1等现代垃圾收集算法执行部分垃圾清理,所以再次使用术语”清理“可能并不完全正确。

    其实我们关心的并不是这里应该叫Major GC 或者叫Full GC ,我们关心的应该是GC是否暂停了应用程序的执行,或者是否能够与应用程序线程同时执行。

    相关文章

      网友评论

        本文标题:[翻译]Garbage Collection in Java

        本文链接:https://www.haomeiwen.com/subject/nenvfftx.html