Java GC基础知识

看到了一篇GC入门文章,感觉挺不错的,转载翻译一下。

wiseguy.jpg

References:

duang, duang.
谁啊?
…GC一段时间之后…
Java.
这是一个古老的笑话,还是在Java刚出来,比其他语言慢的时候。随着时间的流逝,巴拉巴拉~(懒得翻译了,反正就是Java天下第一就对了)。
废话少说,开始了。

Java附带了几个垃圾收集器。更具体地说,这些是在自己的线程中运行的不同算法。每个都有不同的工作方式,有利有弊。 最重要的是所有垃圾收集器都会stop the world。也就是说,你的应用程序会被被挂起或暂停,直到垃圾清理结束。算法的主要区别在于它们如何stop the world。有些算法完全处于空闲状态,直到必须需要执行垃圾收集,然后暂停你的应用程序很长一段时间,有些算法将主要的工作与你的应用程序同时运行,因此在stop the world时需要暂停的时间很短。最好的算法取决于你的目的。

为了增强垃圾收集过程,Java(HotSpot JVM,更准确地说)将堆内存分为两代:Young Generation(新生代)和Old Generation(也称为Tenured)(老年代)。 还有一个Permanent Generation(永久代),但我们不会在这篇文章中介绍它。
hotspot-heap.png

Young generation是新对象存活的地方。它进一步细分为以下几个区域:

  • Eden Space
  • Survivor Space 1
  • Survivor Space 2

默认情况下,Eden比两个survivor spaces的总和还要大。在我的带有64位HotSpot JVM的Mac OS X上,Eden占据了所有young generation空间的76%。所有的对象都是最初在这里创建。 当Eden区满时,会触发minor GC。所有新对象都会被快速检查一遍是否需要进行垃圾收集。那些已经死亡的,即未被引用(忽略此讨论的参考强度)的对象会被标记并回收。存活的对象会被移动到一个空的“survivor spaces”。那么是两个survivor spaces中的哪一个呢?要回答这个问题,让我们讨论一下survivor spaces。

两个Survivor Space也分别被称为From space和To space,拥有两个survivor spaces的原因是为了避免内存碎片。想象一下,如果只有一个survivor space。将survivor space想象成一个连续的内存数组。当young generation GC扫过数组时,它会识别待删除的对象。这些对象在删除后将会在内存中留下一段无用空间,因此需要压缩内存内容。为了避免压缩,HotSpot JVM只是将survivor space中所有存活的对象复制到另一个(空的)survivor space,这样就没有无用的空间。在我们讨论压缩时,请注意旧代垃圾收集器(CMS除外)在堆内存的旧代部分执行压缩以对其进行碎片整理。

注:关于两个survivor spaces有很多文章,可以自行参考,这里简单说明下。刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色(即From space和To space的角色也是不断交替的),如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。这种机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。

简言之, minor GC(当Eden区满时触发) 从Eden和其中一个survivor space(在日志中被称为‘from’ survivor space)复制存活的对象 到另一个(被称为‘to’ survivor space).

当以下之一发生时:

  • 对象达到了最大存活阈值,换句话说,因为复制了足够多次数,所以这些对象不再年轻了。
  • survivor space没有空间接收新建的对象。

对象被移到old generation.(可能还有其他条件,但我不知道。)让我们用一个真实的例子来理解。假设我们有以下应用程序在初始化期间创建一些“长期存活的对象”,并在其操作期间创建许多短期存活对象。(例如,web server为每个请求分配短期存活对象。)

private static void createFewLongLivedAndManyShortLivedObjects() {
        HashSet<Double> set = new HashSet<Double>();

        long l = 0;
        for (int i=0; i < 100; i++) {
            Double longLivedDouble = new Double(l++);
            set.add(longLivedDouble);  // add to Set so the objects continue living outside the scope
        }

        while(true) { // Keep creating short-lived objects. Extreme but illustrates the point
            Double shortLivedDouble = new Double(l++);
        }
}

让我们使用以下JVM命令行参数启用垃圾收集日志和其他设置:

-Xmx100m                     // Allow JVM 100 MB of heap memory
-XX:-PrintGC                 // Enable GC Logs
-XX:+PrintHeapAtGC           // Enable GC logs
-XX:MaxTenuringThreshold=15  // Allow objects to live in the young space longer
-XX:+UseConcMarkSweepGC      // Ignore for now; covered later
-XX:+UseParNewGC             // Ignore for now; covered later

应用日志显示垃圾收集之前和之后的状态的如下:

Heap before GC invocations=5 (full 0):
 par new (young) generation total 30720K, used 28680K
  eden space 27328K,   100% used
  from space 3392K,   39% used
  to   space 3392K,   0% used
 concurrent mark-sweep (old) generation total 68288K, used 0K 

Heap after GC invocations=6 (full 0):
 par new generation (young) total 30720K, used 1751K
  eden space 27328K,   0% used
  from space 3392K,   51% used
  to   space 3392K,   0% used
 concurrent mark-sweep (old) generation total 68288K, used 0K

从日志中,我们可以看到一些东西。 首先要注意的是,在此之前已经有5个minor GC(总共6个)。Eden100%时触发。其中一个survivor space占39%,因此有一些空间。垃圾收集结束后,我们可以看到Eden回到0%,survivor space增加到59%。 这意味着来自Eden和survivor space的存活对象被移动到第二个survivor space,而无用的对象被回收了。我们怎么知道回收了一些无用的对象?我们可以看到Eden比survivor space大得多(27328K vs 3392K),并且由于survivor space大小仅略微增加,因此大量无用的对象肯定是被回收了。在垃圾收集之前和之后,old generation space都是空(回想一下,存活阈值设置为15)。

我们来试试另一个实验。运行一个在多线程中只创建短期存活对象的应用程序。根据我们到目前为止所讨论的内容,这些对象都不应归于old generation; minor GC应该能够清理它们。

private static void createManyShortLivedObjects() {
        final int NUMBER_OF_THREADS = 100;
        final int NUMBER_OF_OBJECTS_EACH_TIME = 1000000;

        for (int i=0; i<NUMBER_OF_THREADS; i++) {
            new Thread(() -> {
                    while(true) {
                        for (int i=0; i<NUMBER_OF_OBJECTS_EACH_TIME; i++) {
                            Double shortLivedDouble = new Double(1.0d);
                        }
                        sleepMillis(1);
                    }
                }
            }).start();
        }
    }
}

这个例子中,我只给了JVM 10MB的内存。我们来看看GC日志。

Heap before GC invocations=0 (full 0):
 par new (young) generation total 3072K, used 2751K
  eden space 2752K,  99% used
  from space 320K,   0% used
  to   space 320K,   0% used
 concurrent mark-sweep (old) generation total 6848K, used 0K 

Heap after GC invocations=1 (full 0):
 par new generation  (young)  total 3072K, used 318K
  eden space 2752K,   0% used
  from space 320K,  99% used
  to   space 320K,   0% used
 concurrent mark-sweep (old) generation total 6848K, used 76K

和我们预测的不一样。我们可以看到,这一次,old generation在第一次minor GC后立即就有了对象。我们知道这些对象是短暂存活的,并且存活阈值设置为15,这是第一次回收。发生的事情如下:应用程序创建了大量填充Eden空间的对象。Minor GC试图收集垃圾。但是,大多数这些短期对象在GC期间是活动的,例如,从被一个活动的线程引用并被处理。young generation GC只能将这些对象放到old generation。这是不好的,因为被放到old generation的对象过早老化,只能通过old generation的major GC来清理,这通常需要更多的时间。使用我们稍后将介绍的特定GC算法,CMS,当old generation内存满70%时触发major GC。可以使用-XX:CMSInitiatingOccupancyFraction = 70参数更改此默认值。

如何防止短期存活的对象过早老化?一种方法是根据活跃的短期存活对象的数量适当的估计young generation的大小。我们进行以下的更改:

  • 默认情况下,Young Generation是堆总大小的1/3。我们使用-XX:NewRatio = 1修改,这会给young generation提供更多内存(与上一次3.0 MB相比,大约为3.4 MB)。
  • 同时使用-XX:SurvivorRatio = 1参数增加survivor space比率。(每个约1.6MB,而之前为0.3MB。)

问题解决了。 经过8次minor GC后,old generation空间仍然空着。

Heap before GC invocations=7 (full 0):
 par new generation   total 3456K, used 2352K
  eden space 1792K,  99% used
  from space 1664K,  33% used
  to   space 1664K,   0% used
 concurrent mark-sweep generation total 5120K, used 0K 

Heap after GC invocations=8 (full 0):
 par new generation   total 3456K, used 560K
  eden space 1792K,   0% used
  from space 1664K,  33% used
  to   space 1664K,   0% used [
 concurrent mark-sweep generation total 5120K, used 0K

这绝不是调整GC的详尽方法。我只是想简单证明下相关的步骤。对于实际应用,可以通过不同设置的反复试验找到最佳设置。例如,我们也可以通过将总堆内存大小加倍来解决问题。

GC 算法
我们已经介绍了各种generations,现在让我们来看看GC算法。 HotSpot JVM为young generation和old generation提供了几种算法。 总的来看,有三种通用类型的GC算法,每种算法都有自己的工作特征

serial collector 使用单线程来执行所有垃圾收集工作,这使得它相对有效,因为没有线程间的通信开销。它最适合单处理器机器,尽管在多处理器上它对于具有小数据集(最大约100 MB)的应用程序的非常有用,但是它无法发挥多处理器硬件的优势。在特定硬件和操作系统配置下会默认选择serial collector,或者可以使用选项-XX:+UseSerialGC显式启用serial collector。

parallel collector (也称为throughput collector) 并行执行minor GC,这可以显着减少GC开销。它适用于在多处理器或多线程硬件上运行从中型到大型数据集的应用程序。默认情况下,在特定硬件和操作系统配置下会默认选择parallel collector,或者可以使用选项-XX:+UseParallelGC显式启用parallel collector。

concurrent collector 大多数concurrent collector并发执行大部分工作(例如,在应用程序仍在运行时)以保整GC时间够短。它适用于中型到大型数据集的应用程序,这些程序的响应时间比总吞吐量更重要,因为用于最小化暂停时间的技术会降低应用程序性能。 Java HotSpot VM提供两个主要concurrent collector之间的选择; 参见主要的concurrent collector。 使用选项-XX:+UseConcMarkSweepGC 启用CMS收集器或-XX:+UseG1GC启用G1收集器。

gc-compared.png

HotSpot JVM允许你为young generation和old generation配置单独的GC算法。但是你只能配对兼容的算法。例如,不能将young generation的Parallel Scavenge收集器与old generation的Concurrent Mark Sweep收集器配对,因为它们不兼容。为了方便,我打算制作一个信息图来显示哪些垃圾收集器兼容,但幸运的是,我先搜索并找到了一个由JVM工程师Jon Masamitsu创建的图。

gc-collectors-pairing.jpg

  1. “Serial”是个stop-the-world, 使用单个GC线程的复制收集器。
  2. “Parallel Scavenge”是个stop-the-world,使用多个GC线程的复制收集器。
  3. “ParNew”是个stop-the-world,使用多个GC线程的复制收集器。它不同于“Parallel Scavenge”在于它具有支持CMS的增强功能。例如,“ParNew”执行必要的同步,以便它可以在CMS的并发阶段运行。
  4. “Serial Old”是个stop-the-world,使用单个GC线程的mark-sweep-compact收集器。
  5. “CMS” (Concurrent Mark Sweep) 是个主要的并发,短暂停收集器.
  6. “Parallel Old”是个使用多个GC线程的压缩收集器。

Concurrent Mark Sweep(CMS)与ParNew配合使用,非常适合处理来自客户端实时请求的服务器端应用程序。 我一直在使用大约10GB的堆内存,它可以保持响应时间稳定,并且GC暂停时间也很短。我认识的一些开发人员使用Parallel collector(Parallel Scavenge + Parallel Old)对结果很满意。

关于CMS,已经有人要求弃用它,并且在Java 9中,G1被设置为默认垃圾收集器(JEP 248)。

G1收集器是一种服务器式垃圾收集器,适用于具有大容量内存的多处理器机器。它以高概率满足垃圾收集(GC)暂停时间的同时实现高吞吐量。

G1同时适用于young generation和old generation。它针对较大的堆大小(> 10 GB)进行了优化。我没有用过G1收集器,我团队中的开发人员也仍在使用CMS,所以我还不能比较两者。简单搜了下,结果显示CMS的表现优于G1。我看的很仔细,但G1应该也没多大问题。可以通过如下参数启用:

-XX:+UseG1GC

希望这篇文章对你有帮助,如果觉得不错,不妨去原文感谢下作者。

注:
用来说明“HotSpot Heap Structure”的图需要澄清:“Permanent Generation”被Java 8中的“Metaspace”取代。虽然我没有在帖子中讨论Permanent Generation,但它与Metaspace的主要区别在于Metaspace可以动态调整大小(增加和减少)。

关于G1 GC,可以参考:https://tech.meituan.com/2016/09/23/g1.htmlhttps://www.oracle.com/technetwork/tutorials/tutorials-1876574.html

标签: none

添加新评论

ali-01.gifali-58.gifali-09.gifali-23.gifali-04.gifali-46.gifali-57.gifali-22.gifali-38.gifali-13.gifali-10.gifali-34.gifali-06.gifali-37.gifali-42.gifali-35.gifali-12.gifali-30.gifali-16.gifali-54.gifali-55.gifali-59.gif

加载中……