文章目录
三、HotSpot VM垃圾收集器
1、分代垃圾收集
- 新生代:大多数新创建的对象被分配在新生代中(见图3-2),与整个Java堆相比,通常新生代的空间比较小而且收集频繁。新生代中大部分对象的存活时间很短,所以通常来说,新生代收集(也称为次要垃圾收集,以后记作Minor GC)之后存活的对象很少。因为MinorGC关注小并且有大量垃圾对象的空间,所以通常垃圾收集的效率很高。
- 老年代:新生代中长期存活的对象最后会被提升(Promote)或晋升(Tenure )到老年代(见图3-2),。通常来说,老年代的空间比新生代大,而空间占用的增长速度比新生代慢。因此,相比Minor GC而言,老年代收集(也称为主要垃圾收集或完全垃圾收集,以后记作Full GC)的执行频率比较低,但是一旦发生,执行时间就会很长。
- 永久代:这是HotSpot VM内存中的第3块区域(见图3-2)。虽然称为代,但实际上不应该把它看作分代层次的一部分(也就是说,用户程序创建的对象最终并不会从老年代移送到永久代)。相反,HotSpot VM只是用它来存储元数据,例如类的数据结构、保留字符串Interned String)等。
分代垃圾收集的一大优点是,每个分代都可以依据其特性使用最适的垃圾收集算法。新生代通常使用速度快的垃圾收集器,因为Minor GC频繁。这种垃圾收集器会浪费一点空间,但新生代通常只是Java堆中的一小部分,所以不是什么大问题。另外一方面,老年代通常使用空间效率高的垃圾收集器,因为老年代要占用大部分Java堆。这种垃圾收集器不会很快,不过Full GC不会很频繁,所以对性能也不会有很大影响。
2、新生代
HotSpot VM新生代的布局参见图3-4 (空间没有按比例表示),分为3个独立区域(或空间)。
- Eden:大多数新对象分配在这里(不是所有,因为大对象可能直接分配到老年代)。 MinorGC后Eden几乎总是空的。不为空的例子参见第7章。
- Survivor (一对) :这里存放的对象至少经历了一次Minor GC,它们在提升到老年代之前还有一次被收集的机会。图3-4演示的Survivor,只有一块持有对象,另一块基本上是空的。
图3-5演示了Minor GC的操作。灰色X标记的是需要被收集的对象。图3-5a中, Minor GC后,Eden中的存活对象被复制到未使用的Survivor,被占用Survivor里不够老(即还有在新生代中被收集的机会)的存活对象也被复制到未使用的Survivor,最后,被占用Survivor里“足够老”的存活对象被提升到老年代。
Minor GC之后,两个Survivor交换角色(见图3-5b ),Eden完全为空,仍然只使用一个Survivor;老年代的占用略微增长。因为收集过程中复制存活对象,所以这种垃圾收集器称为复制垃圾收集器(Copying Garbage Collector)。也就是利用的复制算法。
需要指出的是,在Minor GC过程中, Survivor可能不足以容纳Eden和另一个Survivor中的存活对象。如果Survivor中的存活对象溢出,多余的对象将被移到老年代。这称为过早提升( Premature Promotion),这会导致老年代中短期存活对象的增长,可能会引发严重的性能问题。再进一步说,在Minor GC过程中,如果老年代满了而无法容纳更多的对象, Minor GC之后通常就会进行Full GC,这将导致遍历整个Java堆。这称为提升失败(Promotion Failure)。
3、快速内存分配
垃圾收集器以复制方式回收HotSpot VM新生代,其好处在于回收以后Eden总为空,在Eden中运用被称为指针碰撞(Bump-the-Pointer)的技术就可以有效地分配空间。这种技术追踪最后一个分配的对象(常称为top),当有新的分配请求时,分配器只需要检查top和Eden末端之间的空间是否能容纳。如果能容纳, top则跳到新近分配对象的末端。
重要的Java应用大多是多线程的,因此内存分配的操作需要考虑多线程安全。如果只用全局锁,在Eden中的分配操作就会成为瓶颈因而降低性能。HotSpot VM没有采用这种方式,而是以一种称为线程本地分配缓冲区(Thread-Local Allocation Buffer,TLAB )的技术,为每个线程设置各自的缓冲区(即Eden的一小块),以此改善多线程分配的吞吐量。因为每个TLAB都只有一个线程从中分配对象,所以可以使用指针碰撞技术快速分配而不需要任何锁。然而,当程的TLAB填满需要获取新的空间时(不常见),它就需要采用多线程安全的方式了。大部分时候,HotSpotVM的new Object()操作只需要大约十条指令。垃圾收集器清空Eden区域,然后就可以支持快速内存分配了。
4、垃圾收集器
HotSpot VM垃圾收集器主要包括以下7种:
5、Serial垃圾收集器
新生代采用Serial,是利用复制算法;老年代使用Serial Old,采用滑动压缩标记-清除(Sliding Compacting Mark-Sweep)算法,也称为标记-压缩(Mark-Compact)垃圾收集器。
它的Minor GC和Full GC都是以Stop-The-World方式(即收集时应用程序停止运行)运行,只有等垃圾收集结束后,应用程序才会继续执行如下图:
标记一压缩收集器首先找出老年代中有哪些依然存活的对象,然后将它们滑向堆的头部,从而将所有的空闲空间留在堆尾部的连续块中。这使得将来任何在老年代中的分配操作(大多数是从新生代提升到老年代)都可以使用快速的指针碰撞技术。下图演示了这样的垃圾收集操作。假定标记为灰色x的是将被收集的对象,而压缩之后末端的阴影区域是已回收的(例如空闲的)空间。
Serial收集器适合大多数对停顿时间要求不高和在客户端运行的应用。同一台机器上运行大量JVM实例(某些情况下JVM的实例数超过了可用的处理器数)时,也常用Serial收集器。当JVM进行垃圾收集时,最好只用一个处理器,虽然会使垃圾收集的时间有所延长,但对其他JVM的干扰最小,这方面Serial收集器处理得很好。
6、Parallel收集器
-
ParNew:用于新生代收集,是利用复制算法。Serial收集器的多线程版本,默认开启的收集线程数和cpu数量一样,运行数量可以通过修改
-XX:ParallelGCThreads=<n>
设定。使用-XX:+UseParNewGC
和Serial Old收集器组合进行内存回收。如下图所示。
-
Parallel Scavenge:用于新生代收集,采用复制算法。关注吞吐量,吞吐量优先,采用多线程,也就是高效率利用cpu时间。通过
-XX:+UseParallelGC
参数,Server模式下默认提供了其和SerialOld进行搭配的分代收集方式。
如果设置了-XX:+UseAdaptiveSizePolicy
参数,则随着Minor GC会动态调整新生代的大小、Eden、Survivor空间比例等,以提供最合适的停顿时间或者最大的吞吐量。 -
Parllel Old:Parallel Scavenge的老年代版本,采用标记一压缩算法(同Serial Old)。JDK 1.6开始提供的。通过
-XX:+UseParallelOldGC
参数使用Parallel Scavenge + Parallel Old器组合进行内存回收,如下图所示。
与Serial收集器相比, Parallel收集器改善了垃圾收集的整体效率,从而也改善了应用的吞吐量。以下应用可以从Parallel收集器获益:
- 需要高吞吐量的应用
- 运行在多处理器系统之上的应用
- 批处理引擎、科学计算等
7、CMS(Concurrent Mark-Sweep)收集器
并发标记清除收集器(Concurrent Mark Sweep,CMS收集器)是一种以获得最短回收停顿时间为目标的收集器。它管理新生代的方式与Parallel收集器和Serial收集器相同,而它在老年代则是尽可能并发执行,每个垃圾收集周期只有2次短的停顿。
但是它比一般的标记-清除算法要复杂一些,分为以下几个阶段:
- 初始标记(Initial Mark):它标记那些从外部(即GC Roots)直接可达的老年代对象,会“Stop The World”。
- 并发标记(Concurrent Marking):它标记所有从这些对象(初始标记的对象)可达的存活对象,可以和用户线程并发执行。
- 重新标记(Remark):重新遍历所有在并发标记期间有变动的对象并进行最后的标记,会“Stop The World”。
- 预清除( Pre-Cleaning ):为了进一步减少重新标记时的工作量, CMS收集器引入了并发预清除阶段。如图,预清除在并发标记之后和重新标记之前,完成一些原本要在重新标记阶段完成的工作,即重新遍历那些在标记期间因并发而被改掉的对象。虽然标记结束前仍然需要重新标记(因为程序在预清除阶段仍有可能改变对象),但预清除依然可以减少在重新标记时需要遍历的对象,有时甚至能非常有效地减少重新标记导致的停顿。
- 并发清理(Concurrent sweeping):清除整个Java堆,释放没有迁移的垃圾对象。
- 并发重置(Concurrent reset)
既然预清除和重新标记阶段的重新遍历对象会增加垃圾收集器的工作量(相比而言, Parallel收集器只在标记期间遍历一次),CMS整体的开销相应增加了。对于大多数垃圾收集器来说,这是典型的为了力图减少停顿时间而做的权衡。
从名字就能知道CMS是采用标记-清除算法的,如下图。
标记为灰色x的将被收集,而清除之后的阴影区域是空闲区域。在这个例子中,空闲区域不连续 ,垃圾收集器需要使用一个数据结构(HotSpot VM中使用空闲列表)记录哪部分堆有空闲空间。因此在老年代分配的代价更昂贵,因为空闲列表的分配不如指针碰撞方法有效。这使Minor GC会产生额外的开销,因为当Minor GC过程中对象提升时,会在老年代中造成大量的分配。
CMS与前两个垃圾收集器相比还有一个缺点,就是需要更大的Java堆。原因如下:
- CMS的周期时间长于Stop-The-World垃圾收集所用的时间。同时,只有在清除阶段,空间才会真的回收。假使允许应用在标记时继续运行,也就允许它继续分配内存,因而在标记阶段老年代的占用可能会有所增加,而只有到清除阶段才会减少。
- 尽管垃圾收集器确保在标记阶段标识所有存活的对象,但实际上它无法保证找出所有的垃圾对象。标记阶段成为垃圾的对象在周期内可能被收集也可能不被收集。如果没有,则它将在下一周期被收集。垃圾收集期间没有找出的垃圾对象通常称为浮动垃圾(Floating Garbage)。
- 缺乏压缩会形成空间碎片化(Fragmentation) ,这将导致垃圾收集器无法最大程度地利用所有可用的空闲空间。在回收周期中,如果尚未回收到足够多空间之前,老年代满了,CMS就会退而求其次,使用代价昂贵的Stop-The-World进行空间压缩,就像Parallel收集器和Serial收集器那样。
应该注意到,CMS的并发(标记和清除)阶段是与用户线程并行的。在高度并行的硬件上运行时(这会变得越来越普遍),这种方式会很有用。否则,单个并发CMS线程将无法应对许多的应用线程。
与Parallel收集器相比,CMS老年代停顿变短了(有时相当可观),但代价是新生代停顿略微拉长、吞吐量有所降低,堆的大小有所增长,并且由于并发,垃圾收集还会占用应用的CPU周期。需要快速响应(例如数据追踪服务器, web服务器等)的应用可以从中受益,像这样的应用非常多。
8、G1(Garbage-First)收集器
Garbage-First收集器(缩写为G1 )是一个并行、并发和增量式压缩低停顿的垃圾收集器,长远来看是为了替代CMS。
G1的Java堆布局和HotSpot VM中其他垃圾收集器有着极大的不同,它将Java堆分成相同尺寸的块(称为区域, Region),虽然G1也是分代,但整体上没有划分成新生代和老年代。相反,每代是一组(可能不连续)区域,这使得它可以灵活地调整新生代。
G1的垃圾收集是将区域中的存活对象转移到另外一些区域,然后收集前者(通常是更大)。大部分时候只收集新生区域(这些形成G1的新生代),它们相当于MinorGC。 G1也定期执行并发标记,以标识那些空或几乎空的非新生区域。这些是收集效率最高的区域(即G1以最少的代价回收最空的区域),它们定期被回收。这是G1名称的由来:它优先回收垃圾对象最多的区域。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。G1收集器之所以能建立可预测的停顿时间模型,也是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这样就意味者它空间整合做的比较好,因为不会产生空间碎片。
G1收集器的运作大致可以分为以下步骤:
- 初始标记(Initial Marking):初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Set)的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这个阶段需要Stop-The-World,但耗时很短。
- 并发标记(Concurrent Marking):并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找到存活的对象,这阶段耗时较长,但是可以和用户线程并发运行。
- 最终标记(Final Marking):最终标记阶段是为了修正在并发标记期间因用户程序继续运行而导致标记产生变化的那一部分标记记录,虚拟机将这段时间对象变化记录在线程
Remembered Set Logs
里面,最终标记需要把Remembered Set Logs
的数据合并到Remembered Sets
中,这阶段需要暂停线程,但是可并行执行。 - 筛选回收(Live Data Counting and Evacuation):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
G1的堆区在分代的基础上,引入分区的概念。如下图简单画了下G1分区模型。
- Eden regions(年轻代-Eden区)
- Survivor regions(年轻代-Survivor区)
- Old regions(老年代)
- Humongous regions(巨型对象区域)
- Free regions(未分配区域,也会叫做可用分区)-上图中空白的区域
G1中的巨型对象是指,占用了Region容量的50%以上的一个对象。Humongous区,就专门用来存储巨型对象。如果一个H区装不下一个巨型对象,则会通过连续的若干H分区来存储。因为巨型对象的转移会影响GC效率,所以并发标记阶段发现巨型对象不再存活时,会将其直接回收。ygc也会在某些情况下对巨型对象进行回收。
分区可以有效利用内存空间,因为收集整体是使用“标记-整理”,Region之间基于“复制”算法,GC后会将存活对象复制到可用分区(未分配的分区),所以不会产生空间碎片。
此处引用:https://blog.csdn.net/iva_brother/article/details/87886525
9、垃圾收集器比较
10、应用程序对垃圾收集器的影响
本节概要介绍了应用程序如何影响垃圾收集器。通常来说,包括以下3个方面。
- 内存分配。应用的内存分配速率越高,垃圾收集的触发就越频繁。
- 存活数据的多少。Java堆中的存活对象越多,收集器需要做的工作越多。
- 老年代中的引用更新。如果老年代中的引用发生了更新,就会创建一个Old-To-Young的引用,这也可能导致在预清除或重新标记阶段就产生一个需要遍历的对象(如果在CMS标记周期中)。