Java 入门指南:JVM(Java虚拟机)垃圾回收机制 —— 内存分配和回收规则
文章目录
- 垃圾回收机制
- 堆空间的基本结构
- 内存分配和回收规则
- 对象优先在 Eden 区分配
- 分配担保机制
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 主要进行 GC 的区域
- 部分收集 (Partial GC):
- Minor GC
- Major/Old GC
- Mixed GC
- 整堆收集(Full GC)
- 空间分配担保
垃圾回收机制
垃圾回收(Garbage Collection,GC
),顾名思义就是释放垃圾占用的空间,当需要排查各种内存溢出问题、当垃圾收集成为系统达到更高并发的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
堆空间的基本结构
Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 堆 内存中对象的分配与回收。
Java 堆是垃圾收集器管理的主要区域,也被称作 GC 堆(Garbage Collected Heap)。
从垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法。
在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:
- 新生代内存(Young Generation)
- 老生代(Old Generation)
- 永久代(Permanent Generation)
下图所示的 Eden
区、两个 Survivor
区 S0
和 S1
都属于新生代,中间一层属于老年代,最下面一层属于永久代。
图片来源:JavaGuide
JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1
)。当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。
关于 Java 堆相关知识的详细讲解,请看:JVM(Java虚拟机)—— Java 内存运行时的数据区域
内存分配和回收规则
对象优先在 Eden 区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
当 Eden
区没有足够空间进行分配时,虚拟机将发起一次 Minor GC
(Minor Garbage Collection)。GC
期间虚拟机又发现 allocation1
无法存入 Survivor 空间,所以只好通过 JVM 内存管理技术的分配担保机制把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1
,所以不会出现 Full GC
。执行 Minor GC
后,后面分配的对象如果能够存在 Eden
区的话,还是会在 Eden
区分配内存。
分配担保机制
分配担保机制(Allocation Assurance Mechanism) 是Java虚拟机(JVM)中的一项内存管理技术,用于确保对象在分配内存时能够被成功分配到新生代的Eden空间中,以避免在发生垃圾收集时频繁进行老年代的扩容。
在新生代的内存管理中,JVM将堆空间划分为多个区域,其中包括一个 Eden
空间和两个 Survivor
空间(一般为 From(S0)
和 To(S1)
)。新创建的对象会被分配到 Eden
空间中。
在进行垃圾收集时,JVM会对 Eden
空间中的存活对象进行标记和复制操作。如果 Eden
空间的可用空间不足以容纳存活的对象,就会触发一次垃圾收集。
但是,如果Eden空间中的对象有一部分存活并且无法放入Survivor空间(To(S1)
、From(S0)
区),则通过分配担保机制将这些对象直接晋升到老年代。
分配担保机制的原理是,当发生一次新生代垃圾回收时,如果发现Eden空间中的存活对象占用的空间超过了 From(S0)
和 To(S1)
空间的可用空间之和,那么就会直接晋升这些存活对象到老年代。这样可以避免频繁进行老年代的扩容,减少了垃圾收集的次数,提高了性能。
需要注意的是,分配担保机制适用于大部分情况下,但在某些特殊情况下(如大对象或长期存活的对象),由于对象的特殊性,可能无法遵循分配担保机制的规则。这种情况下,可能会触发 Full GC
(Full Garbage Collection) 进行整体的堆内存回收。
分配担保机制,用于确保对象在分配内存时能够被成功分配到新生代的 Eden
空间中,避免在发生垃圾收集时频繁进行老年代的扩容。通过分配担保机制,可以减少垃圾收集的次数,提高垃圾收集的效率和性能。
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
大对象直接进入老年代的行为是由虚拟机动态决定的,它与具体使用的垃圾回收器和相关参数有关。大对象直接进入老年代是一种优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本。
-
G1垃圾回收器 会根据
-XX:G1HeapRegionSize
参数设置的堆区域大小和
-XX:G1MixedGCLiveThresholdPercent
参数设置的阈值,来决定哪些对象会直接进入老年代。 -
Parallel Scavenge垃圾回收器中,默认情况下,并没有一个固定的阈值
(-XX:ThresholdTolerance
是动态调整的)来决定何时直接在老年代分配大对象。而是由虚拟机根据当前的堆内存情况和历史数据动态决定
长期存活的对象进入老年代
虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
大部分情况,对象都会首先在 Eden
区域分配。如果对象在 Eden
出生并经过第一次 Minor GC
后仍然能够存活,并且能被 Survivor
容纳的话,将被移动到 Survivor
空间(s0 或者 s1)中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)
。
对象在 Survivor
中每熬过一次 MinorGC
,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁,可以通过参数 -XX:MaxTenuringThreshold
来设置),就会被晋升到老年代中。
Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor
区的一半(默认值是 50%,可以通过 -XX:TargetSurvivorRatio=percent
来设置)时,取这个年龄和 MaxTenuringThreshold
中更小的一个值,作为新的晋升年龄阈值。
主要进行 GC 的区域
来自知乎:RennaxelaFX 的回答
针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
部分收集 (Partial GC):
Partial GC(Partial Garbage Collection) 指在JVM中进行局部垃圾收集的一种策略。它是垃圾收集器对整个堆内存进行部分清理的过程。
传统的垃圾收集器通常会触发整体垃圾收集(Full GC
),即对整个堆内存进行完整的清理。这种方式可能会造成较长的停顿时间,影响应用程序的响应性能。为了减少这种停顿时间,一些现代的垃圾收集器引入了 Partial GC
的概念。
Partial GC
的思想是将堆内存划分为多个区域,并在每个区域中进行独立的垃圾回收。通过局部的垃圾收集,可以只清理一部分的垃圾对象,而不是整个堆内存。这样可以减少垃圾收集的时间,并且只影响到部分区域,从而减少对应用程序的影响
Minor GC
新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
在Java虚拟机中,内存分为不同的区域,其中新生代是对象被创建后分配的初始区域,大多数对象都在新生代中被分配。新生代又分为一个 Eden
区和两个 Survivor
区(通常是一个 From
区和一个 To
区)。
当新生代空间不足时,就会触发 Minor GC
,主要目的是清理新生代中的垃圾对象,保留尚且存活的对象,并为新的对象分配空间。
Minor GC的过程如下:
-
初始阶段,首先将
Eden
区和From
区中的存活对象复制到To
区,并根据对象的年龄(根据对象经历的Minor GC
次数)进行相应的处理。未达到晋升年龄阈值的对象会被复制到To
区,年龄达到晋升年龄阈值的对象则复制到老年代。 -
如果对象经过多次
Minor GC
后仍然存活,会晋升到老年代。当对象达到老年代晋升条件时,即老年代空间不足时,也会触发Full GC
(Major GC)。 -
清理完成后,将
Eden
区和From
区的对象内存清空,清理掉没有存活对象的空间,将To
区和From
区的角色互换,保持新生代的可用性。
Minor GC一般发生频率较高,且对整个堆内存的回收时间较短,因为新生代中的垃圾对象通常较少。因此,Minor GC对于程序的停顿时间影响较小。
Minor GC
的效果和行为会受到具体的垃圾回收器和虚拟机配置参数的影响,不同的垃圾回收器对于新生代的管理方式和策略可能有所不同。
Major/Old GC
老年代收集 old GC 是指在 JVM 中针对老年代进行的垃圾收集操作。
Major GC 在有的语境中也用于指代整堆收集 Full GC
老年代是堆内存中存放长时间存活的对象的区域,通常包含已经经历过多次新生代垃圾收集的对象。
old GC
是为了回收不再使用的对象,释放内存空间以供新的对象使用。由于老年代中存储的对象通常比较大且存活时间较长,old GC 可能对系统性能产生较大影响。不同的垃圾收集器有不同的策略和算法来进行老年代的垃圾收集。
一些常见的老年代垃圾收集器包括:
-
CMS
(Concurrent Mark Sweep) 收集器:CMS 收集器通过使用标记-清除算法来进行垃圾收集,并且在标记和清理阶段尽可能地与应用程序线程并发执行,以减少停顿时间。 -
G1
(Garbage First) 收集器:G1 收集器是一种全局并发的垃圾收集器,它将整个堆内存划分为多个大小相等的区域,采用分代和并发的方式进行垃圾收集,以减小停顿时间。 -
Serial Old
(串行老年代) 收集器:Serial Old 收集器是一种单线程的老年代收集器,它使用标记-整理算法进行垃圾收集。
具体选择哪种老年代垃圾收集器取决于应用程序的需求和系统环境。通常,性能较高的收集器可能会对系统产生更大的停顿时间,而并发的收集器可能会减小停顿时间但牺牲一些系统性能。
在选择老年代垃圾收集器时需要权衡性能需求、停顿时间和可用系统资源。
Mixed GC
Mixed GC(Mixed Garbage Collection) 是一种混合型的垃圾收集方式,在JVM中用于处理全部新生代和部分老年代的垃圾回收。它结合了部分清理和整体清理的特性,旨在提高垃圾收集器的性能和效率。
传统的垃圾收集器通常会针对不同代的垃圾进行独立的收集。例如,年轻代会使用年轻代的垃圾收集器,老年代会使用老年代的垃圾收集器,它们独立运作。
Mixed GC
则通过组合新生代和老年代的部分垃圾收集,以获得更好的性能。它的基本思想是在一次垃圾收集中同时处理新生代和老年代的垃圾,而不是分开进行。
Mixed GC的过程:
-
进行新生代的垃圾回收,清理出生代(Eden空间和Survivor空间)中不再存活的对象。
-
将存活的对象晋升(Promotion)到老年代。
-
对老年代进行部分清理,回收一部分老年代中不再存活的对象。
这种混合的收集方式可以减少全局停顿时间,更充分地利用了整个堆内存的空间。
Mixed GC
通常是由支持分代收集的垃圾收集器 (如G1(Garbage First)收集器 实现的。它在收集时会根据各个分区中的垃圾数量、自适应的触发条件等因素来确定执行 Mixed GC
的时机和频率。通过动态地调整收集策略,Mixed GC
可以在保证垃圾回收效果的同时,减小垃圾收集的停顿时间。
但 Mixed GC
并不适用于所有的垃圾收集器,它主要应用于一些具有分代收集特性的垃圾收集器。在使用 Mixed GC
时,可以根据应用程序的需求和性能要求进行垃圾收集器的选择和配置。
整堆收集(Full GC)
Full GC(Full Garbage Collection),也称为Major GC
,是指对整个Java堆(包括新生代和老年代)进行的完整垃圾回收操作。
与 Minor GC
只针对新生代进行部分回收不同,Full GC会同时清理新生代和老年代中的垃圾对象。
选择何时触发 Major GC
(Full GC)以及使用哪种垃圾收集器来执行 Major GC 是取决于特定 JVM 配置和垃圾收集器的决策。Full GC的触发条件通常包括以下几个方面:
-
当新生代无法容纳要分配的对象,并且新生代中垃圾对象的占比较高时,可能会触发
Full GC
来尝试扩大新生代的空间。 -
对象在新生代经历了多次
Minor GC
后仍然存活,并且无法晋升到老年代时,会触发Full GC
来释放空间。 -
当老年代空间不足以容纳大对象时,也会触发
Full GC
来进行整理和释放。 -
当调用
System.gc()
显式请求进行垃圾收集时,可能会触发Major GC
Full GC的过程相对较为复杂和耗时,主要包括以下几个阶段:
-
标记阶段(Marking Phase):从根对象开始,对整个堆内存中的所有对象进行可达性分析,标记出存活的对象。
-
清理阶段(Sweeping Phase):清理所有未标记的对象,即被认为是垃圾对象的对象。
-
压缩阶段(Compaction Phase):对堆内存进行压缩和整理,将存活对象向一端移动,以便留出连续的空间。
Full GC
会导致整个应用程序在清理和整理内存期间暂停,造成较长的停顿时间,
应尽量避免频繁触发 Full GC
。高效地调整堆内存大小、合理设置垃圾收集器以及优化应用程序的内存使用方式等手段可以降低 Full GC
的频率和影响。
需要注意的是,Full GC
的性能和行为会受到具体的垃圾回收器、堆内存配置参数和应用程序的内存使用情况等因素的影响,不同的情况下 Full GC
的触发和执行策略可能会有所不同。
空间分配担保
空间分配担保是为了确保在 Minor GC
之前老年代本身还有容纳新生代所有对象的剩余空间
JDK 6 Update 24 之前,在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次 Minor GC 可以确保是安全的。如果不成立,则虚拟机会先查看 -XX:HandlePromotionFailure
参数的设置值是否允许担保失败(Handle Promotion Failure)
如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 -XX: HandlePromotionFailure
设置不允许冒险,那这时就要改为进行一次 Full GC。
JDK 6 Update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC