本文简单介绍了垃圾收集的几种常见式,重点说明了G1回收的原理(毕竟JDK1.9 G1会是默认的GC回收器–--我们讨论的只针对采用HotSpot VM 的openJDK、Oracle
JDK)
如您已对几种常见的GC 回收原理有所了解,请您直接跳到最后一章 G1使用。
一、垃圾回收是什么
三种GC 回收方法
在我们了解G1之前,我们先回顾一下JVAV GC的技术特点。
自动内存管理,有效的避免了因为“忘记释放”,导致内存无法重复使用从而产生内存泄漏。
现在采用的方法
1. 标记-扫描
2. 标记-复制
3. 标记-整理
JVM中有一个非常具体和明确的对象集,称为垃圾收集根(Garbage Collection Roots)
注:所谓根(Root),就是判断对象是否可被引用的起始点,至于哪里才是根,不同的语言和编译器都有不同的规定,但基本是将变量和运行栈空间作为根
包含以下内容
1 局部变量
2 活动线程
3 静态字段
4 JNI引用
JVM用于跟踪所有可到达(实时)对象并确保不可访问对象声明的内存可以重用的方法称为标记和扫描算法。它包括两个步骤:
标记正在遍历所有可到达的对象,从GC根开始并在本机内存中保留有关所有此类对象的分类帐
扫描确保下一次分配可以重用未引用对象占用的内存地址。
JVM中的不同GC算法(例如Parallel Scavenge,Parallel Mark + Copy或CMS)正在以略微不同的方式实现这些阶段,但在概念层面,该过程仍然类似于上述两个步骤
关于这种方法的一个至关重要的事情是循环不再泄漏:
图1来源网络
但此种标记清理回收带来的问题是,需要暂停应用线程以便收集,因为如果程序一直在变化,便无法计算真正的引用,当应用程序暂停以便JVM进行内部引用整理,这种情况就是我们所说的停止世界“stop the wordpause”
注:标记和扫描算法在概念上使用最简单的垃圾处理方法,只需忽略这些对象。这意味着在标记阶段完成后,未访问对象占用的所有空间都被视为自由空间,因此可以重用以分配新对象。
这种方法需要使用所谓的自由列表记录每个自由区域及其大小。自由列表的管理增加了对象分配的开销。这种方法的另一个弱点是,可能存在大量的空闲区域,但是如果没有一个区域足够大以适应分配,那么分配仍然会失败(在Java中有OutOfMeMyRebug错误)。
当进行清理时,JVM必须确保可以重用填充了无法访问的对象的区域(图1中灰色部分)。这可能(并最终会)导致内存碎片,与磁盘碎片类似,会导致两个问题:
写操作变得更加耗时,因为找到足够大小的下一个空闲块不再是一个简单的操作。
在创建新对象时,JVM会在连续的块中分配内存。因此,如果碎片升级到没有单个空闲片段足以容纳新创建的对象的点,则会发生分配错误。
为避免此类问题,JVM确保碎片不会失控。因此,垃圾收集过程中也会发生“内存碎片整理”过程,而不仅仅是标记和扫描。此过程将所有可到达对象重新定位到彼此旁边,从而消除(或减少)碎片。这是一个例子:
注:Mark-Sweep-Compact 算法通过将所有标记对象移动到存储区域的开头来解决Mark和Sweep的缺点。这种方法的缺点是增加了GC暂停持续时间,因为我们需要将所有对象复制到新位置并更新对这些对象的所有引用。Mark和Sweep的好处也是可见的 -在这样的压缩操作之后,通过指针碰撞,新的对象分配再次非常便宜。使用这种方法,自由空间的位置始终是已知的,并且也不会触发碎片问题。
Jave heap
堆中的内存池的基本上是以下划分的。在不同的GC算法中,某些实现细节可能会有所不同,但概念仍然有效。
Eden
Eden是内存中通常在创建对象时分配对象的区域。由于通常有多个线程同时创建大量对象,因此Eden进一步划分为 驻留在Eden空间中的一个或多个Thread Local Allocation Buffer(简称TLAB)。这些缓冲区允许JVM直接在相应的TLAB中分配一个线程内的大多数对象,从而避免与其他线程的昂贵同步。
当无法在TLAB内部进行分配时(通常因为那里没有足够的空间),分配将移至共享的Eden空间。如果那里没有足够的空间,则会触发Young Generation中的垃圾收集过程以释放更多空间。如果垃圾收集也没有在Eden中产生足够的可用内存,那么该对象将在旧代中分配。
收集Eden时,GC会从根部遍历所有可到达的对象,并将它们标记为活着。
我们之前已经注意到,对象可以具有跨代链接,因此直接的方法必须检查从其他代到Eden的所有引用。不幸的是,这样做将首先击败几代人。JVM有一个技巧:卡片标记。从本质上讲,JVM只是标记了伊甸园中“脏”物体的粗略位置,这些物体可能与老一代有关。
标记阶段完成后,Eden中的所有活动对象都将复制到其中一个Survivor空间。整个伊甸园现在被认为是空的,可以重复使用来分配更多的物体。这种方法称为“标记和复制”:活动对象被标记,然后被复制(不移动)到幸存者空间。
注:标记和复制算法与标记和压缩非常相似,因为它们也会重新定位所有活动对象。重要的区别是,搬迁的目标是一个不同的记忆区域,作为幸存者的新空间。标记和复制方法有一些优点,因为在同一阶段复制可以与标记同时发生。缺点是还需要一个足够大的内存区域来容纳幸存的对象。
Survivor
在Eden空间旁边有两个叫做from和to的Survivor spaces(幸存者空间)。重要的是要注意两个幸存者空间中的一个总是空的。
空闲的幸存者空间将在下一次年轻一代被收集时开始让对象进驻。来自整个Young一代的所有存在引用连接对象(包括Eden空间和来自'Survivor空间的非空')被复制到'to'幸存者空间。完成此过程后,'to'现在包含对象,'from'则不包含对象。他们的角色此时已切换。
在两个幸存者空间之间复制活动对象的过程重复几次,直到一些对象被认为已经成熟并且“足够老”。基于代际假设(即对象存在年轻代与年老代),预期存活一段时间的物体将继续使用很长时间。
因此,这种“终身”对象可以被提升为 老年代。发生这种情况时,对象不会从一个幸存者空间移动到另一个幸存者空间,而是移动到旧空间,在那里它们将驻留直到它们变得无法访问。
为了确定对象是否“足够老”以便被认可可以转移到老年代空间,GC跟踪特定对象幸存的集合数。在使用GC完成每一代对象之后,那些仍然存活的对象的年龄会增加。每当年龄超过某个 期限阈值时, 该对象将被提升为旧空间。
实际的终身阈值由JVM动态调整,
-XX:+ MaxTenuringThreshold会设置它的上限。
设置-XX:+
MaxTenuringThreshold = 0会 导致立即升级,而不会在Survivor空间之间复制它。默认情况下,现代JVM上的此阈值设置为15个GC周期。这也是HotSpot中的最大值。
如果幸存者空间的大小不足以容纳Young一代中的所有活物,也可能过早地进行升级(Survivor空间复制老年代 )。
注:
-XX:MaxTenuringThreshold 垃圾最大年龄
如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率
该参数只有在串行GC时才有效.
Old Generation
老年代内存空间的实现要复杂得多。老一代通常要大得多,并且被不太可能是垃圾的物体所占据。
老一代的GC发生频率低于年轻一代。此外,由于预期大多数对象在旧一代中都存在,因此不会发生标记和复制。相反,移动对象以最小化碎片。清理旧空间的算法通常建立在不同的基础上。清理原理如下:
设置GC根可访问的所有对象旁边的标记位来标记可到达的对象
删除所有无法访问的对象
]通过将活动对象连续复制到旧空间的开头来压缩旧空间的内容
从描述中可以看出,老年代的GC必须处理显式压缩以避免过多的碎片
PermGen
Java 8之前,存在一个称为“Permanent Generation”永久代的特殊空间。 存放元数据(metadata),class信息,内部化的字符串(internalized strings),容易造成内存泄漏。因为很难预测所有这些都需要多少空间。这些失败预测的结果采用java.lang.OutOfMemoryError:Permgen空间的形式。除非此类OutOfMemoryError的原因是实际的内存泄漏,解决此问题的方法是简单地增加permgen大小,类似于以下示例将允许的最大permgen大小设置为256 MB:
java -XX:MaxPermSize = 256m
Metaspace
由于预测元数据的需求是一项复杂而不方便的工作,因此在Java 8中删除了永久代,以支持Metaspace。从Java 8开始,大多数各种各样的事情都转移到了Java堆中。
类定义现在被加载到名为Metaspace的东西中。它位于本机内存中,不会干扰java heap中的对象。默认情况下,Metaspace大小仅受Java进程可用的本机内存量的限制。这可以避免开发人员在向应用程序中再添加一个类而导致java.lang.OutOfMemoryError:
Permgen情况。注意,这种看似无限的空间并不意味着没有成本——让元空间无法控制地增长,会导致严重的swap交换,造成分配失败。
以通过设置参数MaxMetaspaceSize 控制Metaspace的增长。
java -XX:MaxMetaspaceSize = 256m
Minor GC vs Major GCvs Full GC
清除堆内存中不同部分的垃圾收集事件通常称为Minor,Major和Full GC事件。通过监视应用程序的延迟或吞吐量。将GC事件与结果相关联,监控否停止了应用程序以及多长时间。
但由于Minor,Major和Full GC这两个术语被广泛使用且没有正确的定义,下面我们分别进行说明。
Minor GC
从Young空间收集垃圾称为Minor GC。这个定义既清晰又统一。但在处理Minor
Garbage Collection事件时,Minor GC存在以下特点:
[if !supportLists]1. [endif]当JVM无法为新对象分配空间时,始终会触发Minor GC,例如Eden已满。因此,分配率越高,次要GC发生的频率就越高。
[if !supportLists]2. [endif]Mion GC事件期间,Tenured Generation(可认为老年代)被忽略。从Tenured Generation(老年代)到Young Generation的引用被认为是GC的根(GC roots)。在标记阶段,简单地忽略了从Young Generation到Tenured Generation(老年代)的引用。
[if !supportLists]3. [endif]Minor GC会触发暂停,暂停应用程序线程。对于大多数应用程序,如果Eden中的大多数对象可以被视为垃圾并且永远不会被复制到幸存者/旧空间,则暂停的长度可以忽略不计。如果情况相反并且大多数新生儿物品不符合收集条件,则轻微GC暂停开始花费相当多的时间。
因此Minor GC即清理Young Generation。
Major GC and Full GC
注意,这些术语没有正式的定义 - 无论是JVM规范还是垃圾收集研究论文。但是乍一看,关于Minor GC清理的年轻代空间之上说明这些定义应该很容易:
Major GC清理老年代空间。
Full GC正在清理整个jave heap - 包括Young和Old空间。
首先 - 许多Major GC由Minor GC触发,多数情况两个是串行发生的。另一方面 - 现代垃圾收集算法(如G1)执行部分垃圾清理,因此,再次使用术语“清洁”只是部分正确。
这使我们更加 关注GC是否被称为Major GC 或Full GC,而应该集中精力查明GC是否已停止所有应用程序线程,或者是否能够与应用程序线程同时进行。
二、GC算法:实现与选择
在JVM中找到的特定实现。首先要认识到的一个重要方面是,对于大多数JVM而言,需要两种不同的GC算法 - 一种用于清洁Young Generation,另一种用于清洁旧一代。
适用于Java 8,对于较旧的Java版本,可用的组合可能略有不同:
年轻代老年代JVM选项
增加的增加的-Xincgc
串行串行-XX:+ UseSerialGC
并行清除串行-XX:+ UseParallelGC -XX:-UseParallelOldGC
并行新串行N / A
串行平行的老N / A
并行清除平行的老-XX:+ UseParallelGC -XX:+ UseParallelOldGC
并行新平行的老N / A
串行CMS-XX:-UseParNewGC -XX:+ UseConcMarkSweepGC
并行清除CMSN / A
并行新CMS-XX:+ UseParNewGC -XX:+ UseConcMarkSweepGC
G1-XX:+ UseG1GC
下面我们分别介绍各自的工作原理(只有知道原理,我们才能知其所以然,才会真正使用)
适用于Young和Old代的串行GC
适用于Young和Old的并行GC
对于老一代的年轻+并发标记和扫描(CMS)的并行新功能
G1,包括Young和Old世代的收藏
Serial GC (串行GC)
串行GC 集合使用年轻一代的mark
copy和老年代的mark sweep compact。顾名思义,这两个收集器都是单线程收集器,无法并行处理任务。两个收集器应用暂停,停止所有应用程序线程。
因此,Serial GC算法不能利用现主流硬件中常见的多个CPU内核。与可用的核心数量无关,JVM在垃圾收集期间只使用一个。
JVM启动脚本中指定单个参数来为Young和Old
Generation启用此GC回收。
java -XX:+ UseSerialGC
Parallel GC(并行GC)
Parallel GC垃圾收集器的组合使用了年轻代的Mark Copy和老年代的Mark Sweep Compact。年轻代和老年代收集都会触发stop-the-world事件,停止所有应用程序线程以执行垃圾收集。两个收集器都使用多个线程运行标记和复制/压缩阶段,因此名称为“parallel”。使用这种方法,可以大大缩短收集时间。
垃圾收集过程中使用的线程数可以通过命令行参数-xx:parallelGCthreads=nnn进行配置。默认值等于计算机中的核心数。
并行GC的选择是通过JVM启动脚本中以下任何参数组合的规范来完成的
java -XX:+ UseParallelGC -XX:+ UseParallelOldGC
如果我们的应用优化的目标是提高吞吐量,并行垃圾收集器适用于多核计算机。由于更有效地使用系统资源,因此实现了更高的吞吐量:
在收集过程中,所有核心都在并行清理垃圾,从而缩短暂停时间
在垃圾收集周期之间,收集者都没有消耗任何资源
但另一方面,由于集合的所有阶段都必须在没有任何中断的情况下发生,因此这些收集器仍然容易受到长时间暂停的影响,在此期间应用程序线程将被停止。因此,如果延迟是您的主要目标,则不建议用些组合。
Concurrent Mark and
Sweep(CMS)
这个垃圾收集器集合的官方名称是“主要是并发标记和清理垃圾收集器”。它采用了年轻代并行stop-the-word标记复制算法和老年代并行标记扫描算法。
CMS收集器的设计是为了避免在旧一代收集时长时间停顿。它通过两种方式实现这一点。首先,它不压缩旧一代,而是使用空闲列表来管理回收的空间。其次,它与应用程序同时在标记和扫描阶段完成大部分工作。这意味着垃圾收集不会显式地停止应用程序线程来执行这些阶段。但是,应该注意的是,它仍然与应用程序线程竞争CPU时间。默认情况下,此GC算法使用的线程数等于计算机物理核心数的1/4。
命令行上指定以下选项来选择CMS垃圾收集器。
java -XX:+ UseConcMarkSweepGC
如果我们的主要目标是低延迟,这种组合在多核计算机上是一个不错的选择。减少单个GC暂停的持续时间,从而使他们感觉应用程序响应更快。由于大多数时候GC至少消耗了一些CPU资源而没有执行应用程序的代码,因此CMS通常比CPU绑定应用程序中的并行GC更差。
G1 – Garbage First
G1的一个关键设计目标是使由于垃圾收集而导致的stop-the-word暂停的持续时间和分布是可预测和可配置的,实际上Garbage-First是一个软实时垃圾收集器,这意味着可以为其设置特定的性能目标。可以在任何给定的y毫秒长时间范围内请求stop-the-word暂停不超过x毫秒,例如在任何给定秒内不超过5毫秒。Garbage-First
GC将尽最大努力以高概率实现这一目标(但不确定,这将是难以实时的)。
为实现这一目标,G1建立了许多见解。首先,堆不必分成连续的Young和Old代。相反,堆被分成多个(通常约2048个)较小的堆区域,可以容纳对象。每个区域可以是伊甸园区域,幸存者区域或旧区域。所有伊甸园和幸存者地区的逻辑联盟都是年轻一代,所有旧区域都是老一代:
这允许GC避免一次收集整个堆,而是逐步地处理问题:一次只考虑一个区域的子集,称为收集集。在每次暂停期间收集所有Young区域,但也可以包括一些旧区域:
G1的另一个新颖之处在于,在并发阶段,它估计每个区域包含的实时数据量。这用于构建集合集:首先收集包含最多垃圾的区域。因此名称:垃圾优先收集。
启用G1收集器的情况下运行JVM
java -XX:+ UseG1GC
Evacuation Pause:Fully Young
在应用程序生命周期的开始阶段,G1没有来自尚未执行的并发阶段的任何附加信息,因此它最初在完全年轻模式下运行。当Young
Generation填满时,应用程序线程被停止,Young区域内的实时数据被复制到Survivor区域,或任何由此成为Survivor的自由区域。
复制这些的过程称为Evacuation(疏散),它的工作方式与我们之前看到的其他年轻代收集器的工作方式非常相似。
Concurrent Marking(并行标记)
G1收集器建立在前一节中的许多CMS概念的基础上,尽管它在许多方面有所不同,但并发标记的目标是非常相似的。g1并发标记使用Snapshot-At-The-Beginning (初始快照)方法来标记标记循环开始时所有活动的对象,即使它们同时变成垃圾。关于哪些对象是活动的信息允许为每个区域建立活动状态,以便以后可以有效地选择收集集。
然后,这些信息用于在旧区域中执行垃圾收集。如果标记确定某个区域仅包含垃圾,或者在stop-the-word期间,对包含垃圾和活动对象的旧区域,则可以同时进行Evacuation(疏散)。
当堆的总占用量足够大时,就开始进行并发标记。默认情况下,它是45%,但这可以通过initiatingEapOccupancyPercent JVM选项更改。与CMS一样,G1中的并发标记包括许多阶段,其中一些阶段完全并发,而其中一些阶段要求停止应用程序线程
三、G1使用
HotSpot有这么多的垃圾回收器,,Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个GC选择:
[if !supportLists]1. [endif]如果你想要最小化地使用内存和并行开销,请选Serial GC;
[if !supportLists]2. [endif]如果你想要最大化应用程序的吞吐量,请选Parallel GC;
[if !supportLists]3. [endif]如果你想要最小化GC的中断或停顿时间,请选CMS GC。
G1 GC基本思想
G1 GC是一个压缩收集器,它基于回收最大量的垃圾原理进行设计。G1 GC利用递增、并行、独占暂停这些属性,通过拷贝方式完成压缩目标。此外,它也借助并行、多阶段并行标记这些方式来帮助减少标记、重标记、清除暂停的停顿时间,让停顿时间最小化是它的设计目标之一。
G1的第一论文发表于2004年,在2012年才在jdk1.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。G1回收器拥有独特的垃圾回收策略,这和之前提到的回收器截然不同。从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区,但从堆的结构上看,它并不要求整个Eden区、年轻代或者老年代在物理上都是连续。
综合来说,G1使用了全新的分区算法,其特点如下所示:
并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力;
并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况;
分代GC:G1依然是一个分代收集器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS只是简单地标记清理对象。在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片,进而提升内部循环速度。
可预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
随着G1 GC的出现,GC从传统的连续堆内存布局设计,逐渐走向不连续内存块,这是通过引入Region概念实现,也就是说,由一堆不连续的Region组成了堆内存。其实也不能说是不连续的,只是它从传统的物理连续逐渐改变为逻辑上的连续,这是通过Region的动态分配方式实现的,我们可以把一个Region分配给Eden、Survivor、老年代、大对象区间、空闲区间等的任意一个,而不是固定它的作用,因为越是固定,越是呆板。
G1 GC垃圾回收机制
首先,G1的设计原则就是简单可行的性能调优开发人员仅仅需要声明以下参数即可:
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。其次,G1将新生代,老年代的物理空间划分取消了。这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。
取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。 [if !vml]
[endif]在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
PS
:在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。对象分配策略说起大对象的分配,我们不得不谈谈对象的分配策略。它分为3个阶段:
TLAB(Thread Local Allocation Buffer)
线程本地分配缓冲区
Eden
区中分配
Humongous
区分配
TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。最后,G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。下面我们将分别介绍一下这2种模式。三,G1 Young GC
Young GC
主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
这时,需要考虑一个问题,如果仅仅GC 新生代对象,我们如何找到所有的根呢?老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。
[if !vml]
[endif]在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。
但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card
Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
Young GC
阶段:阶段1:根扫描静态和本地对象被扫描阶段2:更新RS处理dirty card队列更新RS
阶段3:处理RS检测从年轻代指向年老代的对象阶段4:对象拷贝拷贝存活的对象到survivor/old区域阶段5:处理引用队列软引用,弱引用,虚引用处理
四,G1 Mix GC
Mix GC
不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。它的GC步骤分2步:全局并发标记(global concurrent marking)拷贝存活对象(evacuation)
在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global
concurrent marking的执行过程是怎样的呢?在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:初始标记(initial mark,STW)在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。根区域扫描(root region scan)G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。并发标记(Concurrent Marking)G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断最终标记(Remark,STW)该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。清除垃圾(Cleanup,STW)在这个最后阶段,G1 GC 执行统计和 RSet 净化的STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
G1调优
CMS最主要解决了pause time,但是会占用CPU资源,牺牲吞吐量。CMS默认启动的回收线程数是(CPU数量+3)/ 4,当CPU<4个时,会影响用户线程的执行。另外一个缺点就是内存碎片的问题了,碎片会给大对象的内存分配造成麻烦,如果老年代的可用的连续空间也无法分配时,会触发full gc。并且full gc时如果发生young gc会被young gc打断,执行完young gc之后再继续执行full gc。
-XX:UseConcMarkSweepGC参数可以开启CMS,年轻代使用ParNew,老年代使用CMS,同时Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用。
MaxGCPauseMillis调优
G1GC
的最基本的参数:
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
前面2个参数都好理解,后面这个MaxGCPauseMillis参数该怎么配置呢?这个参数从字面的意思上看,就是允许的GC最大的暂停时间。G1尽量确保每次GC暂停的时间都在设置的MaxGCPauseMillis范围内。 那G1是如何做到最大暂停时间的呢?这涉及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域集合。
Young GC
:选定所有新生代里的region。通过控制新生代的region个数来控制young GC的开销。
Mixed GC
:选定所有新生代里的region,外加根据global
concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。
在理解了这些后,我们再设置最大暂停时间就好办了。首先,我们能容忍的最大暂停时间是有一个限度的,我们需要在这个限度范围内设置。但是应该设置的值是多少呢?我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。其他调优参数
-XX:G1HeapRegionSize=n
设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。
-XX:ParallelGCThreads=n
设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的5/8 左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中 n 的值可以是逻辑处理器数的 5/16 左右。
-XX:ConcGCThreads=n
设置并行标记的线程数。将 n 设置为并行垃圾回收线程数(ParallelGCThreads) 的 1/4 左右。
-XX:InitiatingHeapOccupancyPercent=45
设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个Java 堆的 45%。避免使用以下参数:避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。触发Full GC在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?并发模式失败
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:
a,
增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。
b,
通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
c,
也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。