JVM内存分配与回收概览

JVM内存管理是Java程序性能优化的关键环节,其核心流程包括内存分配与回收。本文将详细解析这一流程的四个主要步骤:何时分配、怎样分配、何时回收、以及怎样回收。

怎样分配

  • JVM内存分配策略

对象分配对象通常首先在新生代的Eden区分配。如果启用了本地线程分配缓冲(TLAB),则会优先在TLAB上分配。在某些情况下,对象可能直接在老年代分配,或者在栈上以标量类型存在,这通常由JIT优化决定。分配的具体规则受垃圾收集器的选择和虚拟机内存参数的影响。

优先在Eden区分配大多数情况下,对象在Eden区分配。如果Eden区空间不足,将触发一次Minor GC,将存活的对象从Eden区和Survivor区复制到另一块Survivor区。如果Survivor区空间不足,对象将通过空间分配担保机制提前进入老年代。

大对象直接进入老年代使用Serial和ParNew收集器时,可以通过设置-XX:PretenureSizeThreshold参数,使得大于该参数值的大对象直接在老年代分配,避免在新生代间复制时产生大量内存复制。

对象晋升对象在Eden区出生,并在Survivor区中“存活”一定次数后,根据年龄阈值晋升到老年代。对象的年龄由JVM内部计数器记录,每次Minor GC后,若对象存活,则年龄加一,直到达到-XX:MaxTenuringThreshold设定的阈值。

提前晋升:动态年龄判定JVM在Survivor区中相同年龄的对象总大小超过Survivor区一半时,会将该年龄或更高年龄的对象直接晋升到老年代,而无需达到最大年龄阈值。

何时回收

  • 对象生死判定

可达性分析算法Java等语言的主流实现使用可达性分析算法来判断对象是否存活。算法以GC Roots为起点向下搜索,形成引用链。如果对象到GC Roots没有任何引用链相连,则该对象不可达,即判定为可回收。

GC Roots可作为GC Roots的对象包括:1. 方法区中的类静态属性引用的对象2. 方法区中的常量引用的对象3. 虚拟机栈中的本地变量表引用的对象4. 本地方法栈中JNI引用的对象

即使对象在可达性分析中不可达,也不一定立即回收。对象需要经过两次标记过程,包括在F-Queue中的小规模标记,对象才真正被宣告死亡。 以上是对JVM内存分配与回收策略的概述,涵盖了对象如何在新生代分配、大对象为何直接进入老年代、对象如何根据年龄晋升,以及如何通过可达性分析判定对象的生死。
图片

Java中的GC Roots

在Java虚拟机(JVM)中,GC Roots是垃圾收集器进行可达性分析的起点。以下是可作为GC Roots的对象:

  1. 方法区:类静态属性引用的对象;
  2. 方法区:常量引用的对象;
  3. 虚拟机栈:本地变量表中引用的对象;
  4. 本地方法栈JNI:Native方法中引用的对象。 即使对象在可达性分析中不可达,JVM也不会立即回收它们。对象需要经过两次标记过程才能真正被宣告死亡:第一次是发现没有与GC Roots相连的引用链;第二次是在F-Queue执行队列中进行的小规模标记,这通常发生在对象覆盖了finalize()方法且未被调用过的情况下。

GC原理

  • 垃圾收集算法

分代收集算法与分区收集算法

分代收集

JVM的垃圾收集大多采用分代收集(Generational Collection)算法,它根据对象的存活周期将内存划分为不同的区域,例如新生代、老年代和永久代。这种划分允许JVM根据每个区域的特点采用最合适的GC算法:

  • 新生代:采用复制算法,因为每次垃圾收集都能发现大量对象已死亡,只有少数存活,复制存活对象的成本较低。
  • 老年代:由于对象存活率高,且没有额外空间进行分配担保,通常采用标记—清理标记—整理算法进行回收。

分区收集

与分代收集不同,分区收集算法将整个堆空间划分为多个连续的小区间,每个区间独立使用和回收。这种方法的优势在于可以控制单次回收的区间数量,从而更好地控制GC产生的停顿时间。

新生代

  • 复制算法 复制算法的工作原理是将可用内存分为两个相等容量的部分,每次只使用其中一部分。当这部分内存用尽时,将存活的对象复制到另一部分,然后清理已使用的内存空间。 这种算法适用于新生代,因为新生代的对象死亡率高,复制少量存活对象的成本相对较低。
    图片
    在现代商用虚拟机(VM)中,内存管理是提高程序运行效率的关键。针对新生代内存的回收,采用了复制算法,这种算法通过简化内存分配和回收过程,提高了运行效率。由于新生代中大多数对象的生命周期较短,因此,新生代内存并不是按照1:1的比例划分,而是采用了一个较大的Eden区和两个较小的Survivor区的布局。HotSpot虚拟机默认的Eden区与Survivor区的大小比例为8:1。在进行Minor GC时,Eden区和一块Survivor区中的存活对象会被复制到另一块Survivor区,然后清理掉Eden区和之前使用的Survivor区。如果Survivor区的空间不足以保存所有存活的对象,就会触发老年代的空间分配担保机制,将这部分对象直接转移到老年代。 老年代的内存回收则采用了标记清除算法。这个算法分为两个阶段:标记和清除。在标记阶段,通过可达性分析找出所有需要回收的对象;在清除阶段,统一清理掉这些被标记的对象。这种算法虽然简单,但在实际应用中可能会遇到内存碎片问题,需要进一步优化。
    图片
    在垃圾回收算法中,老年代的内存管理尤为重要。针对老年代的特点,开发了标记整理算法,以解决效率和空间问题。以下是对标记整理算法的详细解析:

效率问题

  • 标记和清除过程:在传统的垃圾回收算法中,标记和清除过程的效率并不高,这会导致程序运行时出现延迟。

空间问题

  • 内存碎片:标记清除算法在清理内存后,会留下大量不连续的内存碎片。过多的空间碎片可能导致在需要分配较大对象时,系统无法找到足够的连续内存,从而不得不提前触发垃圾收集。

老年代-标记整理算法

  • 标记过程:标记整理算法的标记阶段与标记清除算法相同,首先识别出存活的对象。
  • 整理过程:与标记清除算法不同,标记整理算法在标记后,不是直接清理可回收对象,而是将所有存活的对象移动到内存的一端。
  • 清理内存:移动完成后,系统会清理掉端边界以外的内存区域,从而避免内存碎片的产生。 通过这种方式,标记整理算法有效地解决了效率低下和内存碎片过多的问题,提高了老年代内存管理的效率和效果。
    图片

永久代与方法区回收

永久代和方法区的垃圾回收通常性价比较低,主要回收对象包括废弃常量和无用类。以下是无用类的判断条件:

  1. 实例回收:该类的所有实例都已被回收,Java堆中不存在该类的任何实例。
  2. 引用检查:对应的Class对象在任何地方都没有被引用,即无法通过反射访问该类的方法。
  3. 类加载器回收:加载该类的ClassLoader已经被回收。 即便满足上述条件,类也可能不会被回收,因为Hotspot VM提供了-Xnoclassgc参数来控制是否关闭类的垃圾回收功能。在大量使用动态代理、CGLib等字节码框架的应用中,应关闭此选项,并开启VM的类卸载功能,以防止方法区溢出。

空间分配担保

在进行Minor GC前,VM会检查老年代是否有足够的空间来存放新生代中存活的对象。由于新生代使用复制算法,通常只使用一个Survivor区作为轮换备份。在Minor GC后,如果存活对象过多,超出Survivor区容量,就需要老年代进行空间分配担保,将这些对象直接晋升到老年代。但老年代是否有足够的空间在GC前是未知的,因此VM会先检查老年代的连续空间是否大于新生代对象的总大小或历次晋升对象的平均大小。如果满足条件,则执行Minor GC;否则执行Full GC以腾出更多空间。 然而,依赖历次晋升对象的平均大小存在风险。如果某次Minor GC后存活对象数量突增,远高于平均值,可能导致担保失败,即老年代也无法容纳这些对象。这时,系统将不得不在失败后重新发起一次Full GC。

GC实现

  • 垃圾收集器 垃圾收集器的实现是JVM中重要的组成部分,负责管理内存的回收和分配。不同的垃圾收集器针对不同的应用场景和性能要求有不同的设计和实现策略。选择合适的垃圾收集器对于优化应用性能至关重要。
    图片

GC实现目标GC(垃圾收集器)的实现目标包括准确性、高效性、低停顿时间和空闲内存的规整化。

新生代收集器概述

Serial收集器Serial收集器是Hotspot虚拟机在Client模式下默认使用的新生代收集器。以下是其主要特点:

  1. 单线程操作:Serial收集器使用单一的CPU或线程来完成垃圾收集任务。
  2. STW(Stop The World):在执行垃圾收集时,需要暂停所有其他工作线程,以保证收集过程的准确性和效率。

Serial收集器适用场景

  • 适用于单核处理器的系统。
  • 适合内存资源受限的环境,如小型应用或简单的命令行工具。

性能考量虽然Serial收集器在单线程环境下简单高效,但在多核处理器上使用时,由于只使用一个CPU核心进行垃圾收集,可能会成为性能瓶颈。

总结Serial收集器是Hotspot虚拟机在Client模式下的首选,适用于对延迟不敏感的单线程应用。然而,在多线程或多核环境下,可能需要考虑其他更高效的收集器。

图片

垃圾收集器概述

单线程收集器单线程收集器在进行垃圾收集时,虽然只有一个线程在工作,但其简单性和高效性使其在内存管理方面表现出色。特别是在虚拟机(VM)内存资源有限的情况下,如几十兆到一两百兆的新生代内存,单线程收集器能够将停顿时间控制在几十毫秒到一百多毫秒内。

ParNew收集器ParNew收集器是Serial收集器的多线程版本。除了使用多个线程进行垃圾收集外,ParNew在控制参数、收集算法、STW(Stop-The-World,即世界暂停)、对象分配规则和回收策略等方面与Serial完全一致。ParNew是虚拟机启用CMS(Concurrent Mark Sweep,即并发标记清除)收集器时,默认使用的新生代收集器,具体配置为-XX:+UseConcMarkSweepGC

特点总结

  • 单线程收集:适用于内存资源受限环境,停顿时间短。
  • 多线程收集:通过ParNew实现,提高垃圾收集效率。
  • 控制参数一致性:ParNew与Serial共享控制参数,简化配置过程。
  • CMS默认新生代收集器:在启用CMS时,ParNew作为默认选项,提供稳定性能。

应用场景

  • 对于内存资源有限的虚拟机环境,单线程收集器提供了一个高效的解决方案。
  • 在需要并行处理垃圾收集任务以提高性能的场景下,ParNew收集器是一个合适的选择。
    图片

由于存在线程切换的开销, ParNew在单CPU的环境中比不上Serial, 且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads参数控制GC线程数).

3. Parallel Scavenge收集器

与ParNew类似, Parallel Scavenge也是使用复制算法, 也是并行多线程收集器. 但与其他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge更关注系统吞吐量:

系统吞吐量=运行用户代码时间(运行用户代码时间+垃圾收集时间)

停顿时间越短就越适用于用户交互的程序-良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务-可以最高效率地利用CPU时间,尽快地完成程序的运算任务. Parallel Scavenge提供了如下参数设置系统吞吐量:

图片

老年代

Serial Old收集器

Serial Old是Serial收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法:

Serial Old是Serial收集器的老年代版本, 同样是单线程收集器,使用**“标记-整理”算法**:

图片

Parallel Old收集器介绍

Parallel Old收集器是Java虚拟机(JVM)中的一种垃圾收集器,主要用于老年代的内存管理。以下是关于Parallel Old收集器的详细信息和应用场景:

应用场景

  1. JDK 1.5之前的版本:在Java Development Kit(JDK)1.5之前的版本中,Parallel Old收集器与Parallel Scavenge收集器搭配使用,以优化性能。
  2. CMS收集器的后备方案:当CMS(Concurrent Mark-Sweep)收集器在并发收集过程中发生Concurrent Mode Failure时,Parallel Old作为后备方案被启用,以确保垃圾收集的顺利进行。

特点

  • 多线程支持:Parallel Old收集器使用多线程进行垃圾收集,提高了收集效率。
  • 标记-整理算法:采用“标记-整理”算法,有效管理老年代内存,减少内存碎片。
  • 吞吐量优先:设计上注重吞吐量,适合CPU资源敏感的系统,以及那些需要高吞吐量的应用场景。

配合使用Parallel Old收集器通常与Parallel Scavenge收集器配合使用,形成一套完整的垃圾收集策略,适用于注重吞吐量和CPU资源敏感的系统。

图片

CMS收集器概览

CMS(Concurrent Mark Sweep)收集器是一种并发收集器,它以最短的回收停顿时间为目标,基于“标记-清除”算法实现。它在主流互联网企业中得到广泛应用,例如淘宝和微店。CMS收集器的GC过程分为以下几个步骤:

  1. 初始标记:快速标记GC Roots能直接关联到的对象。
  2. 并发标记:GC Roots Tracing过程,与用户线程并发执行。
  3. 重新标记:修正并发标记期间的标记记录变动。
  4. 并发清除:已死对象就地释放,无压缩。 CMS收集器的主要优势在于其并发收集能力,但同时也存在一些缺点:
  • CPU资源占用:当CPU数小于或等于4时,GC线程可能过多占用用户CPU资源。

  • 浮动垃圾处理:可能出现Promotion Failure或Concurrent Mode Failure,导致另一次Full GC的产生。

  • 内存碎片问题:使用“标记-清除”算法可能导致内存碎片,影响大对象分配。 针对这些问题,CMS提供了一些参数调整策略,例如:

  • -XX:CMSInitiatingOccupancyFraction:设置GC触发的内存使用百分比。

  • -XX:+UseCMSInitiatingOccupancyOnly:启用上述触发百分比。

  • -XX:+UseCMSCompactAtFullCollection:在Full GC后执行碎片整理。

  • -XX:CMSFullGCsBeforeCompaction:设置执行N次不进行内存整理的Full GC后,跟着来一次带整理的。

分区收集

  • G1收集器 G1(Garbage-First)收集器是面向服务端应用的新一代收集器,主要目标是治理配备多颗CPU的服务器上的大内存。G1收集器计划作为CMS收集器的长期替代品。

  • 启用G1收集器的参数:-XX:+UseG1GC

  • G1收集器的特点:将整个Java堆划分为多个大小相等的独立区域(Region),不再物理隔离新生代和老年代,它们都是Region的集合。 G1收集器通过这种分区策略,提高了内存利用率,并减少了停顿时间,是现代Java应用中推荐使用的收集器之一。
    图片
    在Java虚拟机的垃圾收集过程中,G1收集器采用了一种创新的策略来提高效率。G1收集器将堆内存划分为多个区域,这些区域可以是老年代或新生代的一部分。由于每个区域可能包含不同程度的垃圾对象,G1收集器采取了一种并发的方式,优先回收那些垃圾对象较多的区域,从而在有限的时间内实现高效的垃圾收集。

新生代收集

新生代收集是G1收集器策略中的一个重要环节。在新生代中,G1会识别出包含大量可回收对象的区域,这些区域被称为’aaaaaaa’。通过优先处理这些区域,G1可以在较短的时间内清理掉更多的垃圾对象,从而减少对应用性能的影响。 G1收集器的这种策略不仅适用于新生代,同样也适用于老年代。在老年代中,同样会存在一些区域,其垃圾对象的密度高于其他区域。G1收集器会识别这些区域,并在垃圾收集过程中优先处理它们。 总的来说,G1收集器通过智能地识别和优先处理垃圾对象较多的区域,实现了在有限时间内的高效垃圾收集,这对于需要高吞吐量和低延迟的应用场景尤为重要。
图片

G1的新生代收集跟ParNew类似: 存活的对象被转移到一个/多个Survivor Regions. 如果存活时间达到阀值, 这部分对象就会被提升到老年代.

图片
G1收集器是Java虚拟机中一种高效的垃圾收集算法,它主要针对大堆内存的垃圾收集优化。以下是G1收集器的新生代和老年代收集特点的详细解析:

新生代收集特点

  1. 内存划分:G1将整个堆内存划分为多个大小相等的Region,每个Region可以独立进行垃圾收集。
  2. 对象存活:存活的对象会被复制到新的Survivor区或者直接晋升到老年代。
  3. 动态调整:由于年轻代由一组不连续的heap区组成,G1可以动态地调整年轻代和老年代的区域大小,以适应应用程序的内存需求。
  4. STW事件:在Young GC过程中,会出现Stop-The-World(STW)事件,此时所有应用程序线程都会暂停,以确保垃圾收集的准确性。
  5. 多线程并发:G1支持多线程并发执行垃圾收集,以提高收集效率。

老年代收集特点G1老年代GC的执行分为几个阶段,其中一些阶段也是新生代垃圾收集的一部分。老年代收集的特点包括但不限于:

  • 并发标记:G1使用并发标记算法来识别哪些对象是存活的,哪些是垃圾。
  • 混合回收:G1会根据需要,将一部分老年代的Region与年轻代的Region一起回收,以减少Full GC的发生。
  • 整理优化:在老年代收集过程中,G1会对内存进行整理,以减少内存碎片。 G1收集器通过上述特点,实现了对大堆内存的高效管理,减少了垃圾收集对应用程序性能的影响。
    图片

图片

详细步骤可参考 Oracle官方文档-The G1 Garbage Collector Step by Step.

G1老年代GC特点如下:

  • 并发标记阶段(index 3)

  • 在与应用程序并发执行的过程中会计算活跃度信息.

  • 这些活跃度信息标识出那些regions最适合在STW期间回收(which regions will be best to reclaim during an evacuation pause).

  • 不像CMS有清理阶段.

  • 再次标记阶段(index 4)

  • 使用Snapshot-at-the-Beginning(SATB)算法比CMS快得多.

  • 空region直接被回收.

  • 拷贝/清理阶段(Copying/Cleanup Phase)

  • 年轻代与老年代同时回收.

  • 老年代内存回收会基于他的活跃度信息.

补充: 关于Remembered Set

G1收集器中, Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用都是使用Remembered Set来避免扫描全堆. G1中每个Region都有一个与之对应的Remembered Set, VM发现程序对Reference类型数据进行写操作时, 会产生一个Write Barrier暂时中断写操作, 检查Reference引用的对象是否处于不同的Region中(在分代例子中就是检查是否老年代中的对象引用了新生代的对象), 如果是, 便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中. 当内存回收时, 在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏.

V. JVM小工具

在${JAVA_HOME}/bin/目录下Sun/Oracle给我们提供了一些处理应用程序性能问题、定位故障的工具, 包含

图片

VI. VM常用参数整理

图片
在Java开发过程中,了解Java虚拟机(JVM)的各种参数及其应用场景对于性能调优至关重要。然而,由于参数众多,这里无法一一列举。要获取详尽的信息,推荐访问Oracle官方文档提供的Java HotSpot VM Options。以下是一些关键点和相关资源的概览:

Java HotSpot VM Options

  • 文档说明:此文档适用于JDK 7及更早版本,JDK 8的用户请参考Windows和Solaris的参考页面。
  • 参数分类:括标准选项和非标准选项,其中-X开头的是非标准选项,-XX开头的参数不稳定,可能会在后续版本中更改。

有用的-XX参数

  • 布尔类型:通过-XX:+<option>启用,-XX:-<option>禁用。
  • 数值类型:使用-XX<option>=<number>设置,支持MB、KB、GB单位。
  • 字符串类型:使用-XX<option>=<string>设置,通常用于指定文件、路径或命令列表。

参考与扩展阅读

  • 深入理解Java虚拟机:了解JVM的内部机制和工作原理。
  • JVM内幕:《Java虚拟机详解》,力荐阅读。
  • G1垃圾回收器JVM中的G1垃圾回收器了解G1的工作原理和特性。
  • G1入门:基础介绍G1垃圾收集器的使用。
  • G1深入:深入解析G1垃圾收集器的内部机制。
  • JDK 7的Garbage-First收集器:解析JDK 7中引入的Garbage-First收集器。
  • 内存管理:《Memory Management in the Java HotSpot Virtual Machine》,了解JVM的内存管理策略。

实用资源

  • JVM实用参数:介绍JVM类型及编译器模式。
  • 淘宝JVM(TaobaoVM):基于OpenJDK深度定制的JVM,适用于特定场景。

结语希望本文能帮助你更好地理解和使用Java虚拟机。如果你觉得有帮助,请分享给更多人。关注「ImportNew」,获取更多技术干货。

图片