了解Java中的垃圾回收(GC)的工作原理有什么好处?满足软件工程师的求知欲是一个正当的理由,不仅如此,了解GC的工作原理也可以帮助您编写更好的Java应用程序。
这是我个人的非常主观的看法,但是我相信精通GC的人往往是更好的Java开发人员。如果您对GC流程感兴趣,则意味着您具有开发特定大小的应用程序的经验。如果您仔细地考虑过选择正确的GC算法,则意味着您完全了解所开发应用程序的功能。当然,对于优秀的开发人员来说,这可能不是通用的标准。但是,当我说理解GC是成为一名出色的Java开发人员的要求时,很少有人会反对。
这是“ 成为Java GC专家 ”系列文章中的第一篇。这次我将介绍GC,在下一篇文章中,我将讨论分析特定的GC状态和调优示例。
在学习GC之前,您应该了解一个术语。术语是STW( Stop The World )。意味着JVM需要停止运行应用程序以执行GC。这意味着除GC所需的线程以外的所有线程都将停止其任务,无法处理外部响应。。被中断的任务将仅在GC任务完成后才能恢复。无论您选择哪种GC算法,STW都会发生。GC调优通常意味着减少STW时间。
分代垃圾收集
Java没有显式释放内存的操作。有人将相关对象设置为null或使用System.gc()方法显式释放内存,这是不合适的。调用System.gc()方法将触发垃圾回收,极大地影响系统性能。将对象设为null其实在大多种场景下没有任何意义。
在Java中,由于开发人员未明确删除程序代码中的内存,因此垃圾收集器会找到不再需要的(垃圾)对象并将其删除。该垃圾收集器是基于以下两个假设创建的。
1、大多数新创建的对象生命周期都很短
2、从老对象到新对象的引用数量很少。
这些假设称为分代假设。基于此,在HotSpot VM实现中将其从物理上分为年轻代和老年代。
年轻代(Yong Gen):大多数新创建的对象都位于此处。由于大多数对象很快变得无法访问,因此许多对象是在年轻一代中创建的,然后死亡了。当回收年轻代对象时,我们称之为“ Minor GC ”。
年老代(Old Gen):从年轻代幸存下来的对象复制到此处。它通常比年轻一代大。同时一些大对象也在此分配。由于它的大小较大,因此与年轻一代相比,GC发生的频率会降低,耗时会更久。当回收老年代对象时,我们说发生了“ Major GC ”(或“ Full GC ”)。
见下图:
图1:GC区域和数据流。
上表中的永久代也称为“ 方法区域 ”,它存储类或长量字符串。在该区域可能会发生GC,此处发生的GC仍被视为Major GC(Full GC)。
有人可能会怀疑:
如果老一代的对象需要引用年轻一代的对象怎么办?
为了处理这些情况,在年老代中有一个称为“ 卡片表 ”的东西,它是一个512字节的块。每当年老代中的对象引用年轻一代中的对象时,该对象都会记录在此表中。当为年轻代执行GC时,仅搜索此卡表以确定它是否适用于GC,而不是检查年老代中所有对象的引用。
图2:卡片表结构。
年轻代的组成
年轻代是第一次创建对象的地方。年轻一代分为3个空间。
一个 Eden区
两个 Survivor(From,To)区
总共有3个区,其中两个是幸存者区。每个区的执行过程顺序如下:
1、大多数新创建的对象位于Eden区中。
2、在Eden区中进行一次GC之后,将幸存的对象移动到其中一个Survivor区。
3、在Eden区进行GC之后,将这些对象堆积到Survivor区中,该区中已经存在其他幸存的物体。
4、一旦Survivor区已满,就将幸存对象移动到另一个Survivor区。然后,已满的Survivor区将更改为完全没有数据的状态。
在这些步骤中幸存下来的对象(已重复多次)被移到了年老代。
通过检查这些步骤可以看到,Survivor区之一必须保持为空。
下表显示了通过次要GC收集到旧数据中的数据的过程:
图3:GC前和后。
在HotSpot VM中,使用两种技术来加快内存分配。一个称为“ 碰撞指针 ”,另一个称为“ TLAB(线程本地分配缓冲区)”。
指针碰撞技术跟踪分配给Eden区的最后一个对象。该物体将位于Eden区的顶部。并且如果之后创建了一个对象,则仅检查该对象的大小是否适合Eden空间。如果上述对象看起来正确,它将被放置在Eden空间中,新对象将位于顶部。因此,在创建新对象时,仅需要检查最后添加的对象,从而可以更快地分配内存。但是,如果我们考虑多线程环境,则情况就不同了。为了将多个线程使用的对象保存在Eden空间中以确保线程安全,将发生不可避免的锁定,并且由于锁定竞争而导致性能下降。TLAB 是HotSpot VM中解决此问题的方法。这允许每个线程在其Eden空间中有一小部分与其对应的份额相对应。由于每个线程只能访问自己的TLAB,因此即使是“撞球指针”技术也可以在没有锁的情况下分配内存。
年老代GC
当堆空间不够用时会发生年老代GC。年老代GC的执行过程根据GC策略类型的不同区别很大。对于JDK 7,8,有5种GC类型。
1、Serial GC - 串行GC
2、Parallel GC - 并行GC
3、Parallel Old GC (Parallel Compacting GC)
4、Concurrent Mark & Sweep GC (or "CMS")
5、Garbage First (G1) GC
其中,Serial GC不得在运行服务器上使用。当台式计算机上只有一个CPU内核时,就会创建此GC类型。使用此串行GC将大大降低应用程序性能。
现在让我们了解每种GC类型。
串行GC(-XX:+ UseSerialGC)
年轻一代中的GC使用我们在上一段中介绍的类型。老一代的GC使用一种称为“ mark-sweep-compact ” 的算法。
标记-清除-整理算法会将清理后的堆的整理成连续的内存空间。
串行GC适用于较小的内存和少量的CPU内核。
并行GC(-XX:+ UseParallelGC)
图4:串行GC和并行GC之间的区别。
从图片中,您可以轻松地看到串行GC和并行GC之间的区别。串行GC仅使用一个线程来处理GC,而并行GC使用多个线程来处理GC,因此速度更快。当有足够的内存和大量内核时,此GC很有用。也称为“ 吞吐量GC”。
并行旧GC(-XX:+ UseParallelOldGC)
从JDK5开始支持。与并行GC相比,唯一的区别是年老代的GC算法相对复杂。它经历了三个步骤:标记–摘要–整理。
CMS GC(-XX:+ UseConcMarkSweepGC)
图5:串行GC和CMS GC
从图片中可以看到,CMS比到目前为止我所解释的任何其他GC类型都要复杂得多。早期的初始标记步骤很简单。在最接近类加载器的对象中搜索尚存的对象。因此,暂停时间很短。在并发标记步骤中,跟踪并检查了刚被确认的存活对象所引用的对象。此步骤的不同之处在于,它是在同时处理其他线程的同时进行的。在重新标记步骤中,将检查并发标记步骤中新添加或停止引用的对象。最后,在并发收集中步骤,将执行垃圾收集过程。在仍在处理其他线程时执行垃圾回收。由于以这种方式执行此GC类型,因此GC的暂停时间非常短。CMS GC也称为低延迟GC,在所有应用程序的响应时间至关重要时使用。
CMS的一大有点是缩短了STW时间,但它也具有以下缺点:
1、它比其他GC类型使用更多的内存和CPU。
2、默认情况下不提供压缩步骤,容易导致内存碎片
使用此类型之前,您需要仔细检查。另外,如果由于内存碎片过多而需要执行压缩任务,那么STW时间可能比任何其他GC类型都要长。您需要检查压缩任务执行的频率和时间。
G1 GC
最后,让我们了解G1 GC。
图6:G1 GC的布局。
如果您想了解G1 GC,请忘记所有有关年轻一代和老一代的知识。如您在图片中看到的,将一个对象分配给每个Region,然后执行GC。G1 GC是基于停顿时间为目标的回收算法,以最大程度的减少STW