JVM详解

 

1、JVM介绍

JVM(Java Virtual Machine)是Java编程语言的核心组件之一,它是一个虚拟的计算机,可以执行Java字节码指令。JVM充当了Java程序和底层操作系统之间的中间层,提供了跨平台的特性,使得Java程序能够在不同的操作系统上运行。JVM的跨平台特性使得Java成为一门具有广泛应用领域的编程语言。开发人员只需编写一次Java代码,即可在不同的操作系统和硬件上运行,极大地简化了软件的开发和部署过程。同时,JVM提供了丰富的工具和调试接口,方便开发人员进行性能优化、调试和监控等工作。

1.1 JVM主要组成部分

  1. 类加载器(ClassLoader):类加载器负责将Java源代码编译生成的字节码文件加载到JVM中,并对其进行验证、准备和解析。类加载器还负责动态加载类,在运行时根据需要加载额外的类。

  2. 运行时数据区(Runtime Data Area):JVM的运行时数据区包含了各种不同的内存区域,包括方法区、堆、栈、本地方法栈等。其中,方法区用于存储已加载的类信息、常量、静态变量等;堆用于存储对象实例;栈用于存储方法调用时的局部变量、操作数栈等。

  3. 执行引擎(Execution Engine):执行引擎负责解释和执行字节码指令。常见的执行引擎有解释器(Interpreter)和即时编译器(Just-In-Time Compiler,JIT)。解释器逐条解释字节码指令并执行,而JIT将字节码动态编译成本地机器码后执行,以提高执行效率。

  4. 垃圾回收器(Garbage Collector):JVM的垃圾回收器负责自动管理堆内存中不再使用的对象。通过标记-清除、复制、标记-整理等算法,垃圾回收器能够自动回收内存并进行碎片整理,以提供更大的可用内存空间。

1.2 JVM工作流程

  1. 类加载器加载Java源代码编译生成的字节码,并将其存储在方法区。
  2. JVM将需要执行的字节码指令送给执行引擎。
  3. 执行引擎解释或JIT编译字节码指令,并将其转化为可执行的机器码。
  4. 执行引擎执行机器码,完成相应的操作。
  5. 在程序运行过程中,垃圾回收器定期检查并回收无用的对象。
  6. 最终,JVM将程序执行结果返回给用户。

2、JVM内存模型

JVM的内存模型定义了Java程序在JVM中内存的组织和访问规则。它将JVM的运行时数据区分为不同的内存区域,每个区域有不同的作用和生命周期。JVM的内存模型为Java程序提供了良好的内存管理机制,并充分利用了现代计算机硬件的特性。通过合理地配置和管理内存,可以提高程序的性能和稳定性。同时,了解JVM的内存模型也有助于我们编写更高效和可靠的Java程序。

JVM的内存模型主要包括以下几个部分:

  1. 方法区(Method Area):方法区是线程共享的内存区域,用于存储已加载的类信息、常量、静态变量等。在JDK8及之前的版本,方法区被实现为永久代(Permanent Generation),而在JDK8及之后的版本中,方法区改为使用元空间(Metaspace)来实现。

  2. 堆(Heap):堆是用于存储对象实例的内存区域。所有通过new关键字创建的对象实例都在堆上分配内存。堆是线程共享的,可动态分配和释放内存,具有自动内存管理的能力(垃圾回收)。堆分为新生代(Young Generation)和老年代(Old Generation)两部分。

    • 新生代:新生代存储刚被创建的对象实例。它又分为Eden区和两个Survivor区(通常为From和To)。对象首先在Eden区分配内存,当Eden区满时,会触发Minor GC(年轻代垃圾回收),将存活的对象移动到Survivor区。经过多次Minor GC后仍然存活的对象会被移到老年代。

    • 老年代:老年代存储较长时间存活的对象实例。当堆内存不足时,会触发Major GC(全局垃圾回收),对整个堆进行回收。

  3. 栈(Stack):栈是线程私有的内存区域,用于存储方法调用时的局部变量、操作数栈等。每个线程在创建时都会分配一个独立的栈空间,随着方法的调用和返回而动态地进行压栈和弹栈操作。

  4. 本地方法栈(Native Method Stack):本地方法栈类似于栈,但专门用于执行本地方法(即非Java代码)时的操作。它也是线程私有的。

  5. PC寄存器(Program Counter Register):PC寄存器用于存储当前线程正在执行的字节码指令地址。每个线程都有自己的PC寄存器。

  6. 直接内存(Direct Memory):直接内存是JVM直接使用的内存,而不是通过new关键字创建对象实例时分配的堆内存。它的分配和释放由开发人员手动管理。

3、JVM内存分配

3.1 堆空间结构 

⼤部分情况,对象都会首先在 Eden 区域分配,在⼀次新生代垃圾回收后,如果对象还存活,则会进⼊ s0 或者 s1,并且对象的年龄还会加 1(Eden区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到⼀定程度(默认为⼤于 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置默认值,这个值会在虚拟机运行过程中进行调整,可以通过 -XX:+PrintTenuringDistribution 来打印出当次GC后的Threshold

1140a877d9634ea7ba1438890c3028f5.png

3.2 堆分配策略

  1. 对象优先在 eden 区分配(因为⼤部分对象存活周期较短,即将存活周期较短的对象集中起来,可以在最短的停顿时间里 释放最⼤的内存空间)
  2. ⼤对象直接进⼊老年代 (为了避免为⼤对象分配内存时由于分配担保机制带来的复制而降低效率)
  3. 长期存活的对象将进⼊老年代,如果对象在 Eden 出生并经过第⼀次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过⼀次 MinorGC,年龄就增加 1 岁,当它的年龄增加到⼀定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

分配担保机制:老年代的连续空间⼤于新生代对象总⼤⼩或者历次晋升的平均⼤⼩,就会进行 Minor GC,否则将进行 Full GC

4、GC

4.1 GC介绍

JVM的垃圾回收(Garbage Collection,GC)是自动管理堆内存中不再使用的对象的过程。通过垃圾回收,JVM能够回收已经分配给对象实例的内存,并将其释放以供后续的对象使用。JVM的垃圾回收器采用了不同的算法和策略来进行垃圾回收,常见的垃圾回收算法包括标记-清除(Mark and Sweep)、复制(Copying)、标记-压缩(Mark and Compact)等。

4.2 GC分类

部分收集 (Partial GC):
  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区
 
4.3 触发时机
 
最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:
young GC:当young gen中的eden区分配满的时候触发。
full GC:
  • 分配担保失败
  • Metaspace达到阀值
  • ⼤对象无可用空间分配
不同的垃圾回收器在full GC的时机不同,比如CMS当老年代内存占用率超过阀值,执行Full GC
-XX:CMSInitiatingOccupancyFraction

 

4.3 垃圾回收算法

4.3.1 标记-清除算法

该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统⼀回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:

  • 效率问题
  • 空间问题(标记清除后会产生大量不连续的空间碎片)
  • d25247be63594e2a8c963aff84e8c371.png

4.3.2 标记-复制算法

为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为⼤⼩相同的两块,每次使用其中的⼀块。当这⼀块的内存使用完后,就将还存活的对象复制到另⼀块去,然后再把使用的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进行回收。
 
d7ff12d16c3c43949cc372cdd8b67263.png
 
 

4.3.3 标记-整理算法

根据老年代的特点提出的⼀种标记算法,标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。

32bf535d03184b7f8d619abc1ccb2d13.png

4.3.4 分代回收算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为⼏块。⼀般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有⼤量对象死去,所以可以选择“标记-复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代中的对象存活⼏率是比较高的,而且没有额外的空间对它进行分配担保,所以我们就必须选择“标记 -清除 ”或“标记 -整理 ”算法进行垃圾收集。

4.4 垃圾回收器

4.4.1 常见的垃圾回收器

  • 停顿时间: 平均停顿时间加上停顿时间方差
  • 吞吐量: CPU中用于运行用户代码的时间与CPU总消耗时间的比值
_Young_TenuredJVM options
SerialSerial-XX:+UseSerialGC
Parallel ScavengeSerial

-XX:+UseParallelGC

-XX:-UseParallelOldGC

Parallel ScavengeParallel Old

-XX:+UseParallelGC

-XX:+UseParallelOldGC

Parallel NewCMS

-XX:+UseParNewGC

-XX:+UseConcMarkSweepGC

G1 -XX:+UseG1GC

4.4.2 CMS

CMS和G1在并发标记时使用的是同⼀个算法:三色标记法 三 ,使用白灰黑三种颜色标记对象。白色是未标记;灰色自身被标记,引用的对象未标记;黑 色自身与引用对象都已标记。

fb65971ef5a94dbaa9155234ce009dff.png

 

14b429a58e14417d95e2409d6af7fa12.png

93f3622862554d17a0ccf4ba41fabcf2.png

 

CMS使用增量模式:即将增加引用A->D时将A标成灰色

G1使用STAB快照模式,会将删除的引用B->D记录下来 整个过程分为四个步骤:

  1. 初始标记:暂停所有的其他线程,并记录下直接与root相连的对象,速度很快(STW)
  2. 并发标记:同时开启GC和用户线程,用⼀个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方
  3. 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那⼀部分对象的标记记录,这个阶段的停顿时间 ⼀般会比初始标记阶段的时间稍长,远比并发标记阶段时间短(STW)
  4. 并发清除:开启用户线程,同时GC线程开始对未标记的区域做清扫

优缺点:

  • 优点:并发收集、低停顿
  • 缺点:对CPU资源敏感 ,无法处理浮动垃圾(并发标记时应用程序线程和垃圾收集器线程并发运行,垃圾收集器线程跟踪的对象可能随后在收集过程结束时变得无法访问),“标记-清除”算法会导致收集结束时会有⼤量空间碎片产生

4.4.3 Parallel Scavenge + Parallel Old

并行:指多条垃圾收集线程并行⼯作,但此时用户线程仍然处于等待状态。

并发:指用户线程与垃圾收集线程同时执行(但不⼀定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另⼀个CPU上。

Parallel Scavenge收集器关注点是吞吐量(高效利用CPU),默认开启UseAdaptiveSizePolicy,根据young GC平均存活对象的⼤⼩调整From To区域⼤⼩;

调优参数:

  • -XX:ParallelGCThreads=:并行GC线程个数,默认机器低于8核则与机器核数⼀致,否则是机器核数的5/8,增⼤GC线程个数可以减少停顿时间,但是会增加CPU资源的占用
  • -XX:MaxGCPauseMillis=:最⼤停顿时间
  • -XX:GCTimeRatio=19:目标吞吐量,1/(1+19), 则会花5%的时间在垃圾回收上

4.4.4 G1

c79a4ed038ce40d099e1ed83b36cdda0.png

  • Humongous对象:⼤于Region50%的对象
  • RSet:记录了其他Region中的对象到本Region的引用
  • CSet:已回收的分区的集合
  • CardTable:HotSpot VM使用字节数组作为卡片表。每个字节称为⼀张卡。⼀张卡对应于堆中的⼀系列地址。脏卡是指将字节的值更改为脏值;⼀个脏值可能包含⼀个新的指针,从老年代到卡覆盖的地址范围内的年轻代

a6ae9813972e4345904fd9d5d31571ea.pngG1 (Garbage-First) 是⼀款面向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器;以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。G1收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最⼤的Region(这也就是它的名字Garbage-First的由来) 。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
  • 空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
  • 可预测的停顿 :这是G1相对于CMS的另⼀个⼤优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在⼀个长度为M 毫秒的时间片段内。

G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。

  • Young GC:选定所有年轻代里的Region。通过控制年轻代的 region个数,即年轻代内存⼤⼩,来控制young GC的时间开销。
  • Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking 统计得出收集收益高的若⼲老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。

G1 收集器的运作⼤致分为以下⼏个步骤:

  1. 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发⼀次年轻代GC。
  2. 根区域扫描阶段:G1 GC 扫描在初始标记阶段标记的幸存者区域以获取 对老年代的引用,并标记引用的对象。此阶段与应用程序(不是 STW)同 时运行,并且必须在下⼀个 STW 年轻垃圾收集开始之前完成。
  3. 并发标记阶段:G1 GC 在整个堆中查找可访问的(活动的)对象,此阶段与应用程序同时发生,并且可以被 STW 年轻垃圾收集中断。
  4. Remark阶段:由于应用程序持续进行,需要修正上⼀次的标记结果。是STW的。G1中采用了比CMS更快的初始快照法: snapshot-at-the- beginning(SATB)。
  5. 清理阶段:计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域是STW的,同时并发找到完全空闲的区域删除RSets。 Mix GC:在成功完成并发标记周期后,G1 GC 从执行年轻垃圾收集切换到执行混合垃圾收集。 在混合垃圾收集中,G1 GC 可以选择将⼀些老年代区域添加到将要收集的Eden和Survivor区域集合中( 根据混合垃圾回收的目标数量、堆中每个区域中存活对象的百分比以及总体可接受的堆 浪费百分比来调整收集的老年代区域数量)

使用场景:

  • 实时数据占用了超过半数的堆空间
  • 对象分配率或“晋升”的速度变化明显
  • 期望消除耗时较长的GC或停顿(超过0.5~1秒)

调优思路:

当实用G1垃圾回收时,避免设置年轻代⼤⼩(-Xmn -XX:NewRatio),否则会覆盖目标停顿时间 设置目标停顿时间与吞吐量是成反比。应当调整mix gc,参数如下
  • -XX:InitiatingHeapOccupancyPercent:用于改变标记阈值。
  • -XX:G1MixedGCLiveThresholdPercent和-XX:G1HeapWastePercent:用于更改混合垃圾收集决策。
  • -XX:G1MixedGCCountTarget和-XX:G1OldCSetRegionThresholdPercent:用于调整旧区域的 CSet。
  • -XX:G1HeapRegionSize:⼤对象分配导致频繁并发周期且导致区域碎片化

5、JVM调优相关参数

4.1 辅助参数

  •  -XX:+PrintGC
  • -XX:+PrintGCDetails 打印GC详细信息
  • -XX:+PrintGCTimeStamps 打印GC时间
  • -XX:+PrintTenuringDistribution 打印年轻代对象年龄信息
  • -Xloggc:filename GC文件名

4.2 调整空间⼤⼩参数

  • -Xms 初始堆⼤⼩
  • -Xmx 最⼤堆⼤⼩
  • -XX:NewSize=n 设置年轻代⼤⼩
  • -XX:NewRatio=n 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
  • -XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,⼀个Survivor 区占整个年轻代的1/5
  • -XX:MetaspaceSize=n 设置元空间⼤⼩
  • -XX:MaxTenuringThreshold 年轻代对象晋升到老年代最⼤年龄
  • -XX:PretenureSizeThreshold ⼤对象阀值

4.3 CMS相关

-XX:+UseCMSInitiatingOccupancyOnly 允许使用占用值作为启动 CMS 收集器的唯⼀标准

-XX:CMSInitiatingOccupancyFraction=percent 设置开始CMS收集周期的老年代占用率(0 到 100)的百分比

-XX:+CMSScavengeBeforeRemark 会在重新标记之前执行⼀次young GC,加快remark的速度 默认关闭

4.4 G1相关 

  • -XX:G1HeapRegionSize=n 设置G1区域的⼤⼩。该值将是 2 的幂,范围从1MB到32MB。目标是基于最⼩ Java 堆⼤⼩拥有⼤约2048个区域。
  • -XX:MaxGCPauseMillis=200 为所需的最⼤暂停时间设置目标值。默认值为 200 毫秒
  • -XX:G1NewSizePercent=5 将堆的百分比设置为年轻代⼤⼩的最⼩值。默认值为 Java 堆的 5%。
  • -XX:G1MaxNewSizePercent=60 设置堆⼤⼩的百分比以用作年轻代⼤⼩的最⼤值。默认值为Java 堆的60%。
  • -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%。
  • -XX:G1MixedGCLiveThresholdPercent=85 设置要包含在混合垃圾回收周期中的老年区域的占用阈值。默认占用率为 85%。
  • -XX:G1HeapWastePercent=5 设置您愿意浪费的堆百分比。当可回收百分比⼩于堆浪费百分比时,Java HotSpot VM 不会启动混合垃圾收集周 期。默认值为5%
  • -XX:G1MixedGCCountTarget=8 在标记周期后设置混合垃圾收集的目标数量,以收集最多具有 G1MixedGCLIveThresholdPercent 实时数据的 老年区域。默认为8个混合垃圾回收。混合集合的目标是在这个目标数内。
  • -XX:G1OldCSetRegionThresholdPercent=10 设置混合垃圾收集周期中要收集的老年区域数量的上限。默认值为Java堆的10%。
  • -XX:G1ReservePercent=10 设置保留内存的百分比以保持空闲状态,以降低空间溢出的风险。默认值为10%。当您增加或减少百分比时,请确保将总Java堆调整相同的数量

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

发生客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值