#31.Understanding Java Garbage Collection
理解GC(Garbage Collection)的工作原理对Java编程有什么益处呢?满足软件工程师的求知欲或许是一个不错的原因,但与此同时,也可以帮助你编写更加优秀的Java应用程序。
这是我的个人的主观意见,但是我相信那些深谙GC的人往往更容易成为一个优秀的Java工程师。如果你对GC感兴趣,那么意味着你有不错的开发经验。如果你有过仔细选择合适的GC算法经验,这意味着你完全了解你开发应用程序的功能特点。当然,这也许只是优秀开发者的普遍衡量标准,然而我要说的是,要想成为一名优秀的开发者,理解GC是一门必修的课程。
这篇文章的主要目的是以尽量简洁的方式向你讲解GC。我希望这篇文章能切切实实地对你有所帮助。回到正题,在GC中有个词汇stop-the-word,stop-the-word这个过程总会发生,无论你选择何种GC算法。stop-the-world意味着在执行GC的过程中,JVM会中断所有的应用程序线程( 除了GC需要的线程外)。被中断的线程会在GC完成后恢复。我们所关注的GC调优就在于如何减少stop-the-world的运行时间。
Java代码并不能显式地对内存进行分配和移除。有些人会将对象设置为null或者调用System.gc()方法来显式地移除内存空间。将对象设置为null没有大不了的,当调用System.gc()方法却会大大地响应系统的性能(我们并不需要这样做)。
在Java中,开发者并不需要显式地在代码中释放内存,垃圾收集器会帮助我们找到不需要的对象并讲它们移除。垃圾收集器只所以被引入使用是基于以下两个假定前提:
- 大多数对象很快成为不可达状态;
- 老对象引用新对象这种情况总是控制在很小的数量内。
这两个假定前提被成为弱世代假说(Weak generational hypothesis),基于这个假设,在HotSpot虚拟机中,内存(切确地说是Java Heap)被分为两种:新生代(Young Generation)与老年代(Old Generation)。
新生代:绝大部分的新创建的对象都被分配到这里。由于大部分的对象很快会成为不可达状态,很多新创建的对象都分配到新生代,然后很快从这个区域被释放。对象从新生代被释放,我们称这个过程为Minor GC。
老年代:当在新生代的对象没有成为不可达状态,并且从新生代存活下来后,我们会将这些对象复制到老年代。老年代的储存空间会比新生代的要大,所以在老年代发生GC的频率要远远低于在新生代的GC频率。对象从老年代被释放,我们称这个过程为major GC或full GC。
我们看下以下两个图表:
以上图中永久代(Permanent Generation)被称为方法区,它用于存储class文件和运行时常量池。所以,这里的存储空间并不用于“收留”从老年代存活下来的对象。当GC可能会在这个区域发生,我们也把在这个区域发生的GC算作full GC。
有些人会有疑问:当老年代的对象需要引用新生代的对象,这时候会发生什么情况?
为了处理这些情况,在老年代中会有个叫做卡表(card table)的东西,它是一个512字节的数据块。在老年代的对象需要引用新生代的对象时,会被记录到这里。然后,当新生代的GC执行时,这个card table会被检查以确定对象是否应该被GC处理,这样做可以防止对老年代的所有对象进行遍历。这个卡表使用一个被称为写屏障的装置进行管理,它可以让minor GC的性能更加高效,虽然它本身也需要一定的开销,但是整体的开销却是减少的。
为了深入理解GC,我们来看一下新生代。新生代被划分为3个区域空间:
- 一个伊甸园(One Eden Space)
- 两个幸存区 (Two Survivor Spaces)
这三个区域空间中,有两个是幸存区(Survivor Spaces)。每个区域空间的执行过程如下:
- 绝大多数新创建的对象都首先被分配到伊甸园(Eden Space)。
- 当伊甸园的GC执行以后,存活下来的对象会被移动到其中一个幸存区(这个幸存区存放着之前存活下来的对象)。
- 一旦幸存区满了以后,该幸存区存活下来的对象会移动到另外一个幸存区,然后该幸存区会重置为空状态。
- 在多次幸存区的GC执行后而存活下来的对象会被移动到老年代。
在这个过程中,其中一个幸存区必须要保持为空状态。如果两个幸存区都是空状态或者都同时存在数据,你的系统一定出现了什么错误。
数据通过minor GC并堆砌进入老年代的过程如下图所示:
在HotSpot虚拟机中,有两项被用于快速分配内存的技术。一种被成为bump-the-pointer,而另一种是所谓的线程局部缓冲器TLABs (Thread-Local Allocation Buffers)。
Bump-the-pointer technique tracks the last object allocated to the Eden space. That object will be located on top of the Eden space. And if there is an object created afterwards, it checks only if the size of the object is suitable for the Eden space. If the said object seems right, it will be placed in the Eden space, and the new object goes on top. So, when new objects are created, only the lastly added object needs to be checked, which allows much faster memory allocations. However, it is a different story if we consider a multithreaded environment. To save objects used by multiple threads in the Eden space for Thread-Safe, an inevitable lock will occur and the performance will drop due to the lock-contention. TLABs is the solution to this problem in HotSpot VM. This allows each thread to have a small portion of its Eden space that corresponds to its own share. As each thread can only access to their own TLAB, even the bump-the-pointer technique will allow memory allocations without a lock.
你并不需要技术以上提到的两种技术。你需要记住的是:当对象创建之后会首先分配到伊甸园空间,然后通过在幸存区的长时间存活被晋升到老年代空间。
##老年代的GC(GC for the Old Generation)
老年代基本在空间被沾满时才执行GC操作。GC的执行过程根据GC的类型不同而有所差异,如果你对不同的GC类型有所了解,则会明白其中的差异所在。
根据JDK7,共有5中GC类型:
- Serial GC
- Parallel GC
- Parallel Old GC (Parrallel Compacting GC)
- Concurrent Mark & Sweep GC (or CMS)
- Garbage First GC (G1)
其中,串行GC不能使用的操作的服务器上。这种类型的GC创建时有在台式计算机上只有一个CPU核心。使用该系列GC将显著删除应用程序的性能。 使用这种将很明显地降低应用程序的性能。
现在让我们来了解每种GC的类型:
###串行GC( Serial GC (-XX:+UseSerialGC))
(The GC in the young generation uses the type we explained in the previous paragraph. ?) 在新生代中我们使用一种称为**标记-清除-紧凑(mark-sweep-compact)**的算法。
这种算法的第一步就是对新生代的幸存对象进行标记。然后,它从堆的从前往后逐个清理不需要的对象。最后对幸存的对象进行紧凑,使它们在位于连续的内存空间。这个过程会将堆分为两部分:一部分有数据,一部分没数据。Serial GC适用于小的内存空间和少量的CPU核心的机器。
###并行GC (Parallel GC (-XX:+UseParallelGC))
看上图,你可以清楚看到Serial GC与Parallel之间的差异。Serial GC仅使用一个线程去执行GC过程,而Parallel GC会使用多个线程去执行GC过程,因此,可得到更加优秀的性能。当机器拥有很大的内存和较多的CPU核心时,Paraller GC会表现得非常不错。Parallel GC也被称为throughput GC。
###Parallel Old GC
Parallel Old GC从JDK 5 update版本开始得到支持。相比Parallel GC,唯一的区别在于:Parallel Old GC只工作于老年代。它通过三个步骤进行工作:标记-总结-紧凑。The summary step identifies the surviving objects separately for the areas that the GC have previously performed, and thus different from the sweep step of the mark-sweep-compact algorithm. It goes through a little more complicated steps.
###Concurrent Mark & Sweep GC (or CMS)
如你上图所看的,Concurrent Mark-Sweep GC比之前介绍的几种GC都要复杂得多。早期的初始标记阶段很简单,它的主要功能是最接近根类加载器的对象进行标记,这个阶段的停顿时间十分短暂。在并发标记阶段,对刚刚幸存下来的对象的引用进行跟踪和检查,这个过程中,其他的JVM线程不会被中止(也就是没有stop-the-world)。在重新标记阶段,会对并发标记阶段新添加或停止的引用进行确认。最后,在并发清除阶段,对不可达对象进行清理工作(也就GC动作),这个过程,其他的JVM线程也不会被中止。由于这种GC工作方式,GC的停顿时间非常短暂。CMS GC也被称为低延迟GC,这对那些对响应时间有严格要求的应用程序是至关重要的。
虽然这种GC垃圾收集的停顿时间非常短暂,但是他对内存大小和CPU内核数量与性能有着更高的要求。 虽然这种GC类型具有极其短暂的停顿时间,但它也有以下缺点:
- 对内存和CPU的要求更加高。
- 不提供默认的内存紧凑步骤
在使用这种GC之前,你需要仔细地review。此外,在内存紧凑阶段,如果存在大量的内存碎片,那么这种GC需要停顿时间可能会比其他的GC类型的要长。你需要仔细检查内存紧凑发生的频率和时间。
###Garbage First GC (G1)
最后,让我们来看下Garbage First GC(G1)
如果你想要了解G1 GC,首先你要忘记关于新生代和老年代的一切知识点。Java堆(新生代&老年代)被划分为一个个大小固定的区域,对象被分派到这些区域中,如果一个区域被占满,则继续分配另外的区域,同时在后台维护一个优先列表,每次在允许的GC时间内,优先回收占用内存多的对象,这是就G1的来源。
待续...





