对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块 内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”。堆的大小可以通过参数 –Xms、-Xmx 来指定。
从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代。默认的,新生代与老年代的比例的值为 1:2 (该值可以通过参数 –XX:NewRatio 来指定 )。其中新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 – XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
在堆里存放着几乎所有的Java对象实例,在GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象(有用对象),哪些是死亡对象(垃圾对象)。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣告为已经死亡。判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
引用计数算法(Reference Counting)比较简单,对每个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况。它的优点是:
但是缺点也很明显:
相较于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数法中循环引用的问题,防止内存泄漏的发生。
可达性分析是以根对象集合(GC Roots)为起始点,按从上至下的方式搜索被跟对象集合所连接的目标对象是否可达。使用可达性分析算法后,内存中存活的对象都会被根对象集合直接或间接连接着,搜索所过的路 径称为引用链(Reference Chain)。如果目标对象没有和任何引用链相连,则是不可达的,就判定对象已经死亡,可以标记为垃圾对象。
GC Roots 对象有以下几类:
分析工作必须在一个能保障一致性的快照中进行,这点不满足的话,分析结果的准确性就无法保证。这点也是导致GC进行时必须“Stop The World”的一个重要原因。即使是号称几乎不会停顿的CMS垃圾回收器 中,枚举根节点时也是必须要停顿的。
标记-清除(Mark-Sweep)算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:
它的优点是:
但是缺点也比较明显:
复制算法主要是将内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的对象,交换两个内存的角色,最后完成垃圾回收。
对于这种算法来说,如果存活的对象过多的话则要执行较多的复制操作,效率会变低,因此它适合存活率较低的情况。在年轻代中就是使用的复制算法。
它的优点是:
缺点是:
标记-整理分为“标记”和“整理”两个阶段:
标记-整理算法的最终效果等同于标记-清除算法执行后,再进行一次内存碎片整理,因此也可以把它称为标记-清除-压缩算法。 它的优点是消除了标记-清除算法中内存碎片问题。但是缺点也比较明显:
按照所清除垃圾的位置来区分,垃圾清除可要分为 Minor GC 和 Full GC 两种。
Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。新生代几乎是所有 Java 对象出生的地 方,即 Java 对象申请的内存以及存放都是在这个地方。当一个对象被判定为 "死亡" 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳,则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域, 并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
需要注意的是,老年代对象不仅仅是由新生代晋升过来的,有些大对象(即需要分配一块较大的连续内存空间 ) 在创建时是直接进入到老年代。
Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
现在收集器基本都是采用的分代收集算法,其中 Serial 收集器、ParNew 收集器、Parallel Scavenge 收集器主要负责新生代的回收,Serial old 收集器、Parallel Old 收集器、CMS 收集器主要负责老年代的回收,G1 收集器则同时负责两个区域的回收。
Serial 收集器用于新生代的垃圾回收中,采用简单的复制算法,以串行的方式执行,单线程、简单高效(限定单个CPU的环境来说),Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(即STW)。
Serial Old 收集器是Serial 收集器的老年代版本,不同的是它采用标记-整理算法。
ParNew 收集器其实就是 Serial 收集器的多线程版本。除了使用多线程外其余行为均和Serial收集器一模 一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。ParNew 收集器默认开启的收集线程数与 CPU 的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。
Parallel Scavenge 收集器与吞吐量关系密切,故也称为吞吐量优先收集器(吞吐量 = 用户线程执行时间/总时间 * 100%)。 它是属于新生代的、采用复制算法的多线程收集器,目标是达到一个可控制的吞吐量,同时它还有GC自适应调节策略,这是与 ParNew 收集器最重要的区别。
Parallel Scavenge收集器可设置 -XX:+UseAdptiveSizePolicy 参数,当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
Parallel Scavenge收集器使用两个参数控制吞吐量:XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间; XX:GCRatio 直接设置吞吐量的大小。
Parallel Scavenge的设计目标是通过并行执行来实现高吞吐量。它更注重整体系统的工作效率而不是单次垃圾回收的停顿时间,适用于对实时性要求相对较低的应用。
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,是多线程的,采用标记-整理算法实现。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS 收集器主要用于要求低延迟(即提高响应速度)的互联网项目。设置使用 CMS 收集器参数是:- XX:+UseConcMarkSweepGC。CMS 收集器采用的算法是标记-清除算法。
CMS 垃圾收集器特点:
CMS垃圾回收过程主要分为初始标记、并发标记、并发预处理、可终止预处理、重新标记、并发清除、并 发重置七个步骤。
初始标记主要是标记存活的对象,存活对象包含两部分:
在初始标记的基础上,进行并发标记。这一步骤主要是 tracinng 的过程,用于标记所有可达的对象。这个过程会持续比较长的时间,但却可以和用户线程并行。在这个阶段的执行过程中,可能会产生很多变化:比如对象从新生代晋升到了老年代、对象被分配到老年代、老年代或者新生代的对象引用发生了变化。在这个阶段受到影响的老年代对象所对应的卡页,会被标记为 dirty,用于后续重新标记阶段的扫描。
在并发标记阶段,由于标记期间与应用程序并行,对象间的引用关系可能发生变化,因此采用三色标记的方式对对象进行标记,标记过程分为三种颜色:白色、灰色、黑色。
标记过程的具体步骤:
因为在标记过程中可能存在引用对象的变化,所以三色标级存在多标和漏标问题。(漏标问题:未被访问到的对象,被【黑色集合】中的对象引用了,这时候该对象就被漏标了。这种情况使用增量更新方法解决:将新增的引用维护到一个集合中,将引用的源头变为灰色,等待重新标记阶段再重新进行一次扫描。 如:当D的引用指向了C,则会将C变为灰色,并将C放到一个新增引用的集合中,在重新标记阶段会将C作为根节开始继续向下扫描。多标问题:已被扫描过的对象,后来被清空了,这时候不会清除它。这种问题不会产生bug,等待下次回收即可。)
重新扫描前一个阶段标记的 Dirty 对象,并标记被 Dirty 对象直接或间接引用的对象,然后清除 Card 标识。
可终止预处理阶段与并发预处理节点一样,主要是处理并发阶段因引用关系发生变更而未标记到的存活对象(即:扫描所有标记为 Dirty 的 Card)。为什么需要这个阶段?因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。进入可终止预处理阶段的是有条件的,通过配置 CMSScheduleRemarkEdenSizeThreshold (默认值 2M)参数来控制,当新生代Eden 区对象超过 2M时才会进入,如果新生代的对象太少就没有必要执行该阶段,直接执行重新标记阶段。在该阶段,主要循环的做两件事:
这个逻辑不会一直循环下去,打断这个循环的条件有三个:
如果在循环退出之前,发生了一次YGC,对于后面的Remark阶段来说,大大减轻了扫描年轻代的负担,但是发生YGC并非人为控制,所以只能祈祷这5s内可以来一次YGC。
最后CMS还提供了参数 CMSScavengeBeforeRemark,表示进入重新标记前是否强行执行一次 Minor GC(默认关闭,建议开启,开启方式:-XX:+CMSScavengeBeforeRemark)。
预清理阶段也是并发执行的,并不一定是所有存活对象都会被标记,因为在并发标记的过程中对象及其引用关系还在不断变化中。因此,需要有一个 STW 的阶段来完成最后的标记工作,这就是重新标记阶段(CMS标记阶段的最后一个阶段),其主要目的是重新扫描之前并发处理阶段的所有残留更新对象。
并发清理阶段主要工作是清理所有的死亡对象,回收被占用的空间。由于是并发阶段,此时仍然会产生一些不可达对象,称为浮动垃圾。这些只能在下个回收周期才能被回收。
并发重置阶段,将清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构,为下一个垃圾收集周期做好准备。
由于CMS采用标记-清除算法,回收过程会产生内存碎片。如果内存碎片过多时,会给大对象分配带来影 响(如:老年代剩余空间足够,却没有足够的连续内存空间分配给大对象,从而触发Full GC)。针对这种情况,CMS提供了两个参数进行优化:
注意:CMSFullGCsBeforeCompaction参数虽然会降低Full GC压缩频率,减少停顿时长,但是会加剧内存碎片的产生,增加Full GC触发频率,因此,设置时需要在Full GC停顿时长和内存碎片数量之间做权衡。
G1 收集器(Garbage-First)从 JDK7 开始被引入,在 JDK9 被设为默认垃圾收集器,目标就是彻底替换掉 CMS 收集器。G1 收集器在逻辑上还是划分 Eden、survivor 和 old,但是物理上他们不是连续的。G1 收集器将内存分成一个个的 Region,且不要求各部分是连续的。每个 Region 的大小在 JVM 启动时就确定,JVM 通常生成 2048个 左右的 Heap 区,根据堆内存的总大小,区的 size 范围为 1-32 Mb(2的n次方),一般 4M。这样的划分使得 GC 不必每次都去收集整个堆空间,而是以增量的方式来处理:每次只处理一部分小堆区,称为此次的回收集(collection set)。 每次暂停都会收集所有年轻代的小堆区, 但可能只包含一部分老年代小堆区。G1 的另一项创新是在并发阶段估算每个小堆区存活对象的总数。用来构建回收集的原则是垃圾最多的小堆区会被优先收集,这也是 garbage-first 名称的由来。G1 的内存模型如下图所示:
G1 收集器是物理分区,逻辑分代的,图中红色区域是年轻代,包含 Eden 区(红色不带S)和 Survivor 区(红色带S);蓝色区域是老年代,包含 Old 区(蓝色不带H)和 Humongous 区(跨多个区域组成的大对象区域,蓝色带H);灰色区域表示空闲区(Free 区)。H 区保存比标准 region 区大50%及以上的对象,存储在一组连续的区中,需要注意的是每一个H 区最多只能保存一个巨型对象,剩余空间得不到利用,会有内存碎片。同时由于转移巨型对象会影响 GC 效率,所以在标记阶段发现巨型对象不再存活时,会被直接回收。划分成 Region 的好处在于,G1 能够根据需要动态调整不同代的内存大小。例如,如果新生代空间不足,G1 可以从 Free 类型的 Region 中划分一块成为 Eden 类型的 Region。
G1 位了避免STW 的整堆扫描,在每个 Region 都维护了一个已记忆集合(RSet),其内部类似一个反向指针,用于记录不同 Region 之间的跨 Region 引用关系。例如,有两个对象 A 和 B,且 A 在 RegionA 上,B 在 RegionB 上,对象 A 的某个属性是 B,那就意味着 A 引用着 B。此时,RegionB 对应的 RSet 上就会记录着 A 引用着 B,即 RSet 上记录的是别的区域对本区域对象的引用。RSet 的数据结构类型可分为以下三种:
收集集合(CSet)代表每次 GC 暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中。候选老年代分区的 CSet 准入条件,可以通过活跃度阈值 -XX:G1MixedGCLiveThresholdPercent (默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据 CSet 对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。由上述可知,G1的收集都是根据CSet进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的触发条件。
当需要对新的内存区域进行划分时,了解 LAB 的概念非常重要。由于 Region 通常较大,内存申请和划分往往需要更小的粒度。因此,引入了 LAB,它允许更细粒度的内存分配。
为了更有效地管理内存,G1 将每个 Region 进一步划分为多个 Card,通常大小为 512B。这样,一个 Region 就包含多个 Card,Card 是 G1 进行内存管理和垃圾回收的最小单位。一个对象可能跨越多个 Card,或者一个 Card 内存储多个对象。
为了全局管理 Card,引入了 CardTable。CardTable 用于记录 Card 内对象的引用情况,是 G1 垃圾回收过程中的关键数据结构。CardTable 可以被理解为一个字节数组,其中 Card 的首地址就是数组的下标,下标对应的值表示该 Card 上的对象是否发生了引用修改。
LAB 是每个线程的私有内存分配区域,减少线程间的竞争,用于加速对象的分配过程。这里的私有内存是堆内存里Eden区域中的内存,只不过对应的线程管理自己负责部分的内存区域,而且如果使用完可以重新申请LAB。
G1 垃圾回收算法整体是使用“标记-整理”算法,Region 之间基于“复制”算法。G1 的运行过程与CMS 大体相似,分为以下四个步骤:
SATB算法是一种垃圾回收算法,它通过在并发标记过程中采用快照技术,
确保在标记过程中应用线程不会干扰标记线程的工作,从而提高了垃圾回收的效率和可靠性。
SATB算法的基本思想可以概括为以下几点:
1、并发标记前快照:在并发标记开始之前,对内存中的对象进行快照,这个快照记录了标记开始时的对象状态。
2、独立标记:基于这个快照,标记线程独立进行标记,而应用线程不会直接修改这个快照中的对象,
从而保证了标记过程的准确性。
3、新分配对象的处理:应用线程新分配的对象都被认为是活跃对象,这些对象在下一个并发标记周期进行标记。
4、引用关系变更处理:在并发标记过程中,如果对象的引用关系发生变化(例如引用被删除),
这些变化在后续的标记周期中被单独处理,确保了垃圾回收的正确性。
G1 有两个 TAMS 指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象的分配。并发回收时新分配的对象地址都必须在这两个指针之上,G1 收集器默认在这个地址上的对象是存活的,不纳入回收范围。
G1的GC类型可分为以下两种:
在分配一般对象(非巨型对象)时,当所有 Eden 的 region 使用达到最大阀值并且无法申请足够内存时会出发 YGC。YGC 会回收所有 Eden 以及 Survivor 区,并且将存活对象复制到 Old 区以及另一部分的 Survivor 区。因为 YoungGC 会进行根扫描,所以会 STW。YoungGC 的回收过程如下:
一次 YoungGC 之后,老年代占据堆内存的百占比超过 InitiatingHeapOccupancyPercent(默认45%)时,就会触发MixedGC。混合回收都是基于复制算法进行的,把要回收的 Region 区存活的对象放入其他 Region,然后这个 Region 全部清理掉,这样就会不断空出来新的 Region。混合回收停止条件可以由参数 -XX:G1HeapWastePercent 控制,默认值5%,表示空出来的区域大于整个堆的5%,就会立即停止混合回收了。如正常默认回收次数是8次,但是可能到了4次,空闲 Region 大于整个堆的 5%,就不会再进行后续回收了。MixGC 过程如下:
G1 在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发 FullGC。开始版本FullGC 使用的是 stop the world 的单线程的 Serial Old 模式。JDK10 以后, Full GC 已经是并行运行,在很多场景下,其表现还略优于 Parallel GC 的并行 Full GC 实现。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。