[Understanding Java Garbage Collection]理解Java垃圾收集(一)

Understanding Java Garbage Collection

理解Java垃圾收集

原文链接:http://www.cubrid.org/blog/dev-platform/understanding-java-garbage-collection/


了解Java中GC的工作机制有什么好处?除了满足软件工程师的求知欲之外,了解GC工作机制还能帮助你写出更好的Java程序。

这是我自己非常个人和主观的看法,但是我相信一个精通GC的人可以成为更好的Java开发者。如果你对GC流程感兴趣,这意味着你在开发特定大小应用程序方面有经验。如果你仔细思考过选择正确的GC算法,这意味着你完全了解你开发的应用程序的特点。当然,这不是成为好开发者的通用标准。但是,当我说理解GC是成为伟大Java开发者所必需的时候,没有人会反对。

这是“成为Java GC专家”系列文章的第一篇。这次我将涵盖GC的介绍,下次我会讨论分析GC的状态和NHN公司的GC优化示例。

这篇文章的目的是以一种轻松的方式把GC介绍给你。我希望这篇文章会有帮助。事实上,我的同事已经发表了一些关于Java Internals的文章,它们再推特上很受欢迎。你也可以参考一下。

回到GC上来,你在学习GC之前,应该知道一个术语。这个术语是”stop-the-world“。无论你选择哪种GC算法,stop-the-world都会出现。stop-the-world意思是,JVM要停止应用程序来运行GC。当stop-the-world出现时,除了GC线程,其他所有的线程都会停止它们的任务。这些中断的任务将在GC任务完成之后继续运行。GC优化经常意味着减少stop-the-world的时间。

分代垃圾收集

Java在程序代码中并不显示分配内存和回收内存。有些人会设置引用的对象为null,然后使用System.gc()方法来显示回收内存。设置对象为null不是一个大问题,但是调用System.gc()方法会严重影响系统的性能,最好不要这么做。(幸好我还没有见到过NHN的哪个开发者调用过这个方法)。

在Java中,既然开发者不显示分配或回收内存,垃圾收集器发现不需要的(垃圾)对象时就会回收它们。垃圾收集器基于下面两个假设创建。(称它们为前提假设或者先决条件而非假设更合适。)

  • 大多数对象将不可达。
  • 从老对象指向新生对象的引用很少。

这些假设叫做weak generational hypothesis。因此,为了确保假设强化,在HotSpot VM中物理的分为新生代和老年代。

新生代:大多数新创建的对象被分配到此。因为大多数对象将不可达,许多对象在新生代区被创建,然后消失。当对象从这个区域回收时,我们称“minor GC”发生了。

老年代:不可达的对象以及从新生代区存活下来的对象将被拷贝到此。这个区域比新生代区一般要大。因为更大,所以在这里比在新生代区更少发生GC。当对象从老年代消失时,我们称“major GC”(或者“full GC”)发生了。

我们通过图来看一下。

java-gc-area-data-flow.png
图1 GC区域和数据流

上图中的永久代叫做“方法区”,他存储着类和字符串常量。所以这里不是从老年代存活下来的对象保持持久的地方。这里也可能发生GC。GC发生的GC也叫major GC。

有人可能存在疑问:

如果一个老年代的对象需要引用一个新生代的对象会怎么样?

为了解决这个问题,在老年代区有一个叫做“card table”的东西,它是一个512字节的块。当一个老年代的对象引用新生代的对象时,就会被记录到此。当新生代执行GC时,只有这个搜索这个card table来确定是否回收,而不用检查老年代所有对象的引用。这个card table由write barrier管理。write barrier是提高minor GC性能的一种方式。尽管这会带来一定的开销,但是整个GC的时间减少了。

card-table-structure.png
图2 Card Table结构

新生代的组成

为了理解GC,我们来看一下新生代,对象最先被创建在这里。新生代可以分为3个区域。

  • 一个Eden区域
  • 两个Survivor区域

总共有3块区域,其中两个是Survivor区域。每个区域的执行流程如下:

  1. 新创建的大部分对象都被分配在Eden区域。
  2. Eden区域执行一次GC之后,存活的对象会被移动到Survivor区域的一个中。
  3. Eden区域执行一次GC之后,对象被堆积到Survivor区域,无论其他存活对象是否已经存在。
  4. 一旦Survivor区域满了,存活的对象会被移动到另一个Survivor区域。然后,满的Survivor区域会变成没有数据的状态。
  5. 重复多次这些操作之后存活的对象会被移动到老年代区。

通过检查这些步骤,你可以看到,其中一个Survivor区域必须保持空的。如果两Survivor区域都有数据,或者使用率都是0,那么很遗憾你的系统一定出了什么问题。

数据通过minor GC转移到老年代的过程如下图所示:

before-and-after-java-gc.png
图3 GC前后

注意在HotSpot VM中,使用了两种可以更快分配内存的技术。一个叫做“bump-the-pointer”,另一个叫做“TLABs (Thread-Local Allocation Buffers)”。

Bump-the-pointer技术跟踪最后分配到Eden区域的对象。这个对象存在Eden区域的顶部。如果之后又有对象被创建,它会检查对象的大小是否适合Eden区域。如果看起来合适,就会被放到Eden区域,然后对象放到顶部。因此,当新对象被创建时,只有最后添加的对象需要被检查,这就会有更快的内存分配。但是,如果考虑多线程环境,这就不一样了。为了在Eden区域为线程安全保存多个线程的对象,必须要加锁,从而导致因锁竞争而引起性能的下降。TLABs是HotSpot中解决这个问题的方法。它要求每个线程都有在Eden区域都有一块独享的空间。因为每个线程只能访问自己的TLAB,就算是bump-the-pointer技术也允许不加锁的内存分配。

以上是GC在新生代的简介。你不需要记住我刚提到的那两个技术。也不需要刻意去记住它们。但是要记住对象最开始在Eden区域创建,长时间存活的对象通过Survivor区域移动到老年代。

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页