JVM原理&面经
Java的体系结构主要是哪几部分?
宏观看,Java的体系结构主要包括4个独立部分:
- 类装载系统
- Class字节码文件
- Java应用程序接口
- JVM
核心工作原理:Java程序的源代码要通过编译器进行编译,才能生成对应的Class字节码,之后通过类装载器对其进行加载处理,生成一个JVM的运行时数据模型,最后调用Java应用程序接口,从而实现对资源的访问。
什么是类装载系统,聊一聊其安全控制的实现逻辑?
类装载系统类似于一个能将可授信的“数据信息”引入JVM内部的桥梁和网关。
是JVM技术中非常重要的一个技术环节。

对JVM的安全性和网络移动性的发展起到了很大的作用,他负责JVM的安全保障。
其安全控制主要有以下3点:
- 保护Java系统内部核心的运作机制不会被外界的恶意代码所控制或侵入干扰,可以实现舱壁的隔离机制,通过不同的类加载器来加载可信代码或不可信代码,进而保障可信包的安全性。
- 保护JAVA系统已验证的类库。以避免开发人员覆盖核心类库,从而篡改核心类库功能实现,具体通过双亲委派模型实现。
- 开发者可以对Class字节码进行加密,同时在类加载阶段进行解密,从而实现定制化对代码安全和控制。
说一下类加载器对网络移动性的支持,并解释一下类加载器装载Class字节码的总体过程?
类加载器对网络移动性的支持,主要依靠自定义类加载器来装载不同来源的Class字节码。例如:可以通过网络传输下载其他服务器上的Class字节码文件,并将其加载到JVM内部。
类加载器装载Class字节码的总体过程分为3个阶段:加载(读取)阶段,链接阶段,初始化阶段。

Java跨平台如何实现的?
跨平台的标准是Class字节码文件,Class字节码是提供平台无关性的基础模型,使我们无需考虑如何兼容异构系统,它只需要被JVM识别即可。
并且Class字节码文件被设计的非常紧凑,意味着网络传输等所耗资源较少,文件体积与.java文件相比体积有所减少。
类加载器的加载过程是怎么样的?
整个加载过程包括加载,验证,准备,解析,初始化,其中加载,验证,准备,初始化这四个阶段的顺序,常规情况下是固定不变的。

类加载器具体作用是什么?
采用双亲委托的类加载机制加载相关的Class字节码文件,并且转换为相关的运行时内存结构对象。通过一个描述类的全限定名称获取描述该类的二进制字节流,并且转换为相关的运行时内存结构对象。
负责将字节码文件加载到内存中并转换为可执行的类。
JVM有几种类加载器?
有四种,分别是
- 启动类加载器(Bootstrap Class Loader):它是 JVM 的内部组件,负责加载 Java 核心类库(如java.lang)和其他被系统类加载器所需要的类。启动类加载器是由 JVM 实现提供的,通常使用本地代码来实现。
- 扩展类加载器(Extension Class Loader):它是 sun.misc.Launcher$ExtClassLoader 类的实例,负责加载 Java 的扩展类库(如 java.util、java.net)等。扩展类加载器通常从 java.ext.dirs 系统属性所指定的目录或 JDK 的扩展目录中加载类。
- 系统类加载器(System Class Loader):也称为应用类加载器(Application Class Loader),它是sun.misc.Launcher$AppClassLoader 类的实例,负责加载应用程序的类。系统类加载器通常从 CLASSPATH 环境变量所指定的目录或 JVM 的类路径中加载类。
- 用户自定义类加载器(User-defined Class Loader):这是开发人员根据需要自己实现的类加载器。用户自定义类加载器可以根据特定的加载策略和需求来加载类,例如从特定的网络位置、数据库或其他非传统来源加载类。

堆和栈有什么区别?
对于 JVM 来说,堆和栈都是 JVM 的重要组成部分,主要用于存储数据和执行程序。它们有以下几个主要区别:
- 数据结构:堆是一种动态分配的内存区域,用于存储对象实例和数组。它是所有线程共享的,对象在堆中通过引用进行访问。栈是为每个线程分配的内存区域,用于存储方法调用和局部变量等信息;栈的数据结构类似于栈(先进后出),每个方法的调用会创建一个栈帧,方法的参数、局部变量和返回值都存储在栈帧中。
- 分配方式:堆是由 Java 虚拟机动态分配和管理的,通过垃圾回收机制来自动释放不再使用的对象内存;栈的内存分配是自动的,随着方法的进入和退出而自动分配和释放。当方法被调用时,栈会为方法的参数和局部变量分配内存,当方法执行完毕时,栈会自动释放这些内存。
- 内存管理:堆的内存管理由 Java 虚拟机负责,它使用垃圾回收机制自动回收不再使用的对象内存,从而避免了内存泄漏和手动释放内存的问题;栈的内存管理是自动的,当方法调用结束时,栈会自动释放该方法所使用的内存,不需要额外的操作。
- 空间大小:堆的大小通常比栈大得多,因为堆需要存储大量的对象实例和数组;栈的大小通常比较小,因为它仅用于存储方法的调用和局部变量等信息。
- 生命周期:堆中的对象生命周期可以比较长,可以在程序的不同部分进行引用和使用;栈中的数据生命周期较短,当方法调用结束时,栈中的数据就会被自动释放。
总结起来,堆和栈在数据结构、分配方式、内存管理、空间大小和生命周期等方面存在差异。堆适合存储动态分配的对象,而栈适合存储方法的调用和局部变量等信息。
永久代和方法区有什么区别?
方法区是《Java虚拟机规范》中定义的内存区域,用于存储类的结构信息(如类的字节码、常量池、字段和方法信息等),而 Java 默认虚拟机 HotSpot 中,在 JDK 1.8 之前的版本中,是通过永久代来实现方法区的,但 JDK 1.8 之后,永久代被元空间(Metaspace)取代。
所以,方法区和永久代的区别在于,方法区是规范,而永久代(和元空间)是具体实现。
内存溢出和内存泄漏有什么区别?
内存溢出(Memory Overflow)和内存泄漏(Memory Leak)是两个与内存管理相关的问题,它们有以下区别:
- 定义不同
- 内存溢出指的是在程序运行过程中申请的内存超出了可用内存资源的情况,导致无法继续分配所需的内存,从而引发异常。
- 内存泄漏指的是在程序中无意中保留了不再需要的对象引用,导致这些对象无法被垃圾回收机制回收,进而占用了不必要的内存空间。
- 产生原因不同
- 内存溢出通常是由于程序运行时需要的内存超过了可用的内存资源,或者是存在大量占用内存的对象无法被及时释放。常见的内存溢出原因包括创建过多的对象、递归调用导致栈溢出等。
- 内存泄漏则是由于程序中存在不正确的对象引用管理,例如对象被误持有引用、缓存未清理等。
- 影响不同
- 内存溢出会导致程序抛出 OutOfMemoryError 异常,程序无法继续执行。
- 内存泄漏则会导致内存资源的浪费,长时间运行下会导致可用内存逐渐减少,最终可能导致内存溢出。
- 解决方案不同
- 对于内存溢出,可以通过增加可用内存、调整程序逻辑、优化资源使用等方式来解决。
- 而对于内存泄漏,需要通过检查和修复对象引用管理问题,确保不再使用的对象能够被垃圾回收机制正确释放。
如何判断对象是否存活?
在 JVM 中,判断对象是否存活的常见算法有以下两种:
- 引用计数算法
- 可达性分析算法
引用计数器算法
引用计数器算法的实现思路是,给对象增加一个引用计数器,每当有一个地方引用它时,计数器就 +1;当引用失效时,计数器就 -1;任何时刻计数器为 0 的对象就是不能再被使用的,即对象已"死"。
引用计数法的优点:实现简单,判定效率也比较高。
引用计数法的缺点:是引用计数法无法解决对象的循环引用问题。
可达性分析算法
可达性分析算法是通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象是不可用的。以下图为例:

对象 Object5-Object7 之间虽然彼此还有关联,但是它们到 GC Roots 是不可达的,因此它们会被判定为可回收对象。 目前主流的 Java 虚拟机使用的都是可达性分析算法来判断对象是否存活的。
常见的垃圾回收算法有哪些?
注意垃圾回收算法区别于垃圾回收期
垃圾收集器有两个重要的功能:第一,先识别和标记死亡对象;第二,使用合理的垃圾回收算法回收垃圾。那常见的垃圾回收算法有哪些呢?HotSpot 官方默认的虚拟机采用的有什么哪种垃圾回收算法呢?接下来我们一起来看。
常见的垃圾回收算法有以下 4 个:
- 标记-清除算法;
- 复制算法;
- 标记-整理算法;
- 分代算法。
标记-清除算法
标记-清除(Mark-Sweep)算法属于早期的垃圾回收算法,它是由标记阶段和清除阶段构成的。标记阶段会给所有的存活对象做上标记,而清除阶段会把没有被标记的死亡对象进行回收。 而标记的判断方法就是前面讲的引用计数算法和可达性分析算法。 标记-清除算法的执行流程如下图所示:


从上图可以看出,标记-清除算法有一个最大的问题就是会产生内存空间的碎片化问题,也就是说标记-清除算法执行完成之后会产生大量的不连续内存,这样当程序需要分配一个大对象时,因为没有足够的连续内存而导致需要提前触发一次垃圾回收动作。
优点:实现简单。
缺点:产生不连续的内存碎片,如果程序需要分配一个连续内存的大对象时,就需要提前触发一次垃圾回收。
复制算法
复制算法是将内存分为大小相同的两块区域,每次只使用其中的一块区域,这样在进行垃圾回收时就可以直接将存活的东西复制到新的内存上,然后再把另一块内存全部清理掉。 这样就不会产生内存碎片的问题了,其执行流程如下图所示:

从上图可以看出:使用复制算法是可以解决内存碎片的问题的,但同时也带来了新的问题。因为需要将内存分为大小相同的两块内存,那么内存的实际可用量其实只有原来的一半,这样此算法导致了内存的可用率大幅降低了。
优点:执行效率高,没有内存碎片的问题。
缺点:空间利用率低,因为复制算法每次只能使用一半的内存。
标记-整理算法
标记-整理算法是由两个阶段组成的:标记阶段和整理阶段。其中标记阶段和标记-清除算法的标记阶段一样,不同的是后面的一个阶段,标记-整理算法的后一个阶段不是直接对内存进行清除,而是把所有存活的对象移动到内存的一端,然后把另一端的所有死亡对象全部清除,执行流程图如下图所示:
优点:解决了内存碎片问题,比复制算法空间利用率高。
缺点:因为有局部对象移动,所以效率不是很高。
分代算法
分代算法并不能是某种具体的算法,而是一种策略,我们就姑且称它为分代算法吧。目前 HotSpot 虚拟机使用的就是此算法,在 HotSpot 虚拟机中将垃圾回收区域堆划分为两个模块:新生代和老生代,如下图所示:

为什么要将堆分为新生代和老生代呢? 因为对象分为两种,绝大多数对象都是朝生夕灭的,也就是用完一次之后就不用了,而剩下一小部分对象是要重复使用多次的,将不同的对象划分到不同的区域,不同的区域使用不同的算法进行垃圾回收,这样可以大大提高 Java 虚拟机的工作效率。
不同区域使用不同算法
在分代算法中对于不同区域采用的具体算法也是不同的,新生代存放的大部分数据是朝生夕灭的,所以新生代使用的是效率最高的复制算法;而老生代使用的是标记-清除或标记-整理算法,如果标记-清除可以满足需要那么就使用效率更好的标记-清除算法,如果标记-清除算法不能满足需要就使用标记-整理算法。
最后
标记-清除算法效率较高,但存在内存碎片;复制算法效率最高,也没有内存碎片,但内存利用率不高,而标记-整理算法效率不算高,但不存在内存碎片,并且不存在内存利用率的问题;而 HotSpot 在 JDK 8 之前使用的是分代算法(分代策略),将垃圾回收区域分为新生代和老生代,新生代采用复制算法,老生代采用标记-清除或标记-整理算法。
常见的垃圾回收器有哪些?
JVM 常见的垃圾回收器有以下几个:
- Serial/Serial Old:单线程垃圾回收器;
- ParNew:多线程的垃圾回收器(Serial 的多线程版本);
- Parallel Scavenge/Parallel Old:吞吐量优先的垃圾回收器【JDK8 默认的垃圾回收器】;
- CMS:最小等待时间优先的垃圾收集器;
- G1:可控垃圾回收时间的垃圾收集器【JDK 9 之后(HotSpot)默认的垃圾回收器】;
- ZGC:停顿时间超短(不超过 10ms)的情况下尽量提高垃圾回收吞吐量的垃圾收集器【JDK 15 之后默认的垃圾回收器】。

谈谈常见的Java虚拟机有哪些?
HotSpot JVM
1.开发商:Oracle公司 2.特点:是目前应用最广泛的Java虚拟机实现。它采用了即时编译(Just-In-Time Compilation, JIT)的技术,将字节码实时编译为本地机器码,从而提高程序的执行效率。 3.性能优化:通过即时编译技术,将Java字节码转换为本机机器代码,以提高执行速度。
OpenJ9 JVM
- 1.开发商:IBM公司
- 2.特点:也采用了即时编译技术,通过将字节码编译为本地机器码来提高程序的执行性能。与HotSpot JVM相比,OpenJ9 JVM在内存占用和启动速度方面更具优势。
GraalVM
- 1.开发商:Oracle公司(与Graal编译器紧密集成)
- 2.特点:GraalVM是一个高性能的运行时环境,支持多种语言,包括Java、JavaScript、Ruby、Python、R等。它基于Graal编译器,提供了出色的性能和语言互操作性。
JRockit
- 1.开发商:原BEA公司(后被Oracle收购)
- 2.特点:JRockit是一个专注于服务器端应用的Java虚拟机,以其高吞吐量和低延迟而著称。它特别适合用于需要高性能和可靠性的企业级应用。
Exact VM
- 1.开发商:Sun Microsystems(已被Oracle收购)
- 2.特点:Exact VM是一个编译器和解释器混合执行的Java虚拟机,曾在Solaris平台上发布,但后来被其他虚拟机所取代。
Sum Classic VM
- 1.开发商:Sun Microsystems(已被Oracle收购)
- 2.特点:这是第一款商用Java虚拟机,只能使用解释器方式执行Java代码。由于性能上的限制,它已经被其他更高效的虚拟机所取代。
KVM (Kaffe Virtual Machine)
- 1.开发商:开源项目
- 2.特点:KVM是一个轻量级的Java虚拟机,主要设计用于嵌入式系统和移动设备。它支持Java SE 1.1.8规范,并且可以在没有操作系统支持的情况下运行。
什么是Full GC?
Full GC(Full Garbage Collection)是指对整个堆内存进行垃圾回收的过程。在进行 Full GC 时,会对年轻代和老年代(以及永久代或元数据区)中的所有对象进行回收。
Full GC 通常发生在以下情况之一:
Full GC(Full Garbage Collection)是指对整个堆内存进行垃圾回收的过程。在进行 Full GC 时,会对年轻代和老年代(以及永久代或元数据区)中的所有对象进行回收。
Full GC 通常发生在以下情况之一:
- 显式触发:通过调用 System.gc() 方法显式触发垃圾回收。虽然调用该方法只是向 JVM 发出建议,但在某些情况下,JVM 可能会选择执行 Full GC。
- 老年代空间不足:当老年代空间不足时,无法进行对象的分配,会触发 Full GC。此时,Full GC 的目标是回收老年代中的无效对象,以释放空间供新的对象分配。
- 永久代或元数据区空间不足:在使用永久代(Java 8 之前)或元数据区(Java 8 及之后)存储类的元数据信息时,如果空间不足,会触发 Full GC。
Full GC 是一种较为耗时的操作,因为它需要扫描和回收整个堆内存。在 Full GC 过程中,应用程序的执行通常会暂停,这可能会导致较长的停顿时间(长时间的停顿会影响应用程序的响应性能)。 为了避免频繁的 Full GC,通常采取一些优化措施,如合理设置堆大小、调优垃圾回收参数、减少对象的创建和存活时间等。
说一下ZGC?
ZGC(Z Garbage Collector)是一种低延迟的垃圾回收器,是 JDK 11 引入的一项垃圾回收技术。它主要针对大内存、多核心的应用场景,旨在减少垃圾回收带来的停顿时间。
主要特点
ZGC 的主要特点包括:
- 低停顿时间:ZGC 设计的目标之一是尽量减少垃圾回收带来的停顿时间。它采用了并发的垃圾回收方式,尽可能地与应用程序并发执行,以减少停顿的时间和影响。在目标范围内,单次垃圾回收的停顿时间通常不超过 10 毫秒。
- 大内存支持:ZGC 被设计为支持非常大的堆内存。它能有效地处理数十到数百 GB 大小的堆,并且具有可控的、较为恒定的停顿时间。
- 并发回收:ZGC 采用了全程并发的垃圾回收方式。它会与应用程序同时运行,通过在并发阶段遍历并标记存活对象,并在并发预备阶段处理待回收的对象。这样可以将停顿时间分散到整个应用程序运行过程中,减少对应用程序的影响。
- 空间压缩:ZGC 会对堆进行动态的空间压缩,以避免堆内存碎片化的问题。这有助于提高堆的使用效率,并减少内存的浪费。
核心技术
ZGC 有以下几项核心技术来达成毫秒级停顿和大内存支持的目标:
- 并发标记:ZGC 采用增量式并发标记算法来实现并发垃圾回收。它不会在标记阶段产生长时间停顿,可以与用户线程并发运行。
- 粉碎压缩:ZGC 采用粉碎压缩算法来避免产生内存碎片。它会将内存按照特定大小(例如 2MB)分为多个区域,然后将存活对象连续放置在相邻区域,释放掉边界外的内存空间。这可以最大限度减少内存碎片。
- 直接内存映射:ZGC 会直接映射内存空间,而不需要进行内存分配。这可以避免统计堆内存碎片情况所带来的性能消耗。
- 微任务:ZGC 采用了微任务(Microtasks)机制来增量完成垃圾回收工作,从而不会产生长时间停顿。它会将总工作分割为多个微任务,这些微任务会在安全点(Safepoint)之间执行。
- 可扩展的堆内存:ZGC 不需要指定最小堆(Xmn)和最大堆(Xmx)大小,它可以跟踪堆内存变化并根据需要动态调整堆空间大小。这使得 ZGC 可以支持将近 4TB 的堆内存。
- 可插拔组件:ZGC 是一个独立的 GC 组件,它不依赖于 Gradle 等构建工具,可以与不同的工具或框架一起使用,这增强了其可移植性。
这些核心技术的运用使得 ZGC 可以实现毫秒级别的 GC 停顿,并支持将近 4TB 的大内存,适用于对低延迟和大内存有要求的应用场景。
什么是JIT、逃逸分析、锁消除、栈上分配和标量替换?
JIT、逃逸分析、锁消除、栈上分配和标量替换等都属于 JVM 的优化手段,JVM 优化手段是指在运行 Java 程序时,通过对字节码的编译和执行过程进行优化,以提升程序的性能和效率。
JVM 优化手段主要有以下几个:
JIT(Just-In-Time,即时编译):是一种在程序运行时将部分热点代码编译成机器代码的技术,以提高程序的执行性能的机制。
逃逸分析
用于确定对象动态作用域是否超过当前方法或线程,通过逃逸分析,编译器可以决定一个对象的作用范围,从而进行相应的优化,但确定对象没有逃逸时,可以进行以下优化:
- 栈上分配:如果编译器可以确定一个对象不会逃逸出方法,它可以将对象分配在栈上而不是堆上。在栈上分配的对象在方法返回后就会自动销毁,不需要进行垃圾回收,提高了程序的执行效率。
- 锁消除:如果对象只在单线程中使用,那么同步锁可能会被消除,提高程序性能。
- 标量替换:将原本需要分配在堆上的对象拆解成若干个基础数据类型存储在栈上,进一步减少堆空间的使用。
字符串池(String Pool)优化:JVM 通过共享字符串常量,重用字符串对象,以减少内存占用和提升字符串操作的性能。
JIT优点和热点代码
JIT 优点包含以下两个:
- 性能优化:由于编译成本地机器代码,程序的执行速度通常比解释性执行或预编译的代码要快得多。
- 平台无关性:JIT 编译器可以根据不同的硬件平台生成不同的机器代码,使得相同的程序可以在不同的计算机上运行,而无需重新编写。
什么是热点代码?
在 HotSpot 虚拟机中,热点代码(Hot Code)是指那些被频繁执行的代码。 热点代码的执行次数在不同的 JDK 版本和不同的 JVM 中是不同的,例如,它在 JDK 21 Client 模式下为 1500 次,Server 模式下为 10000 次,这个值可以通过 JVM 参数设置。 通常来说,热点代码的识别基于以下两种策略:
- 方法调用次数:当一个方法被调用一定次数后,会被视为热点代码并触发即时编译。这个次数在不同 JDK 版本中可能有所变化,并且可以通过 JVM 参数 -XX:CompileThreshold 进行设置。
- 回边计数:对于循环体等热点区域,通过统计从循环体返回到循环条件检查点的次数(即回边次数),达到一定次数也会触发即时编译。同样,这个阈值也可以通过 JVM 参数 -XX:OnStackReplacePercentage 进行设置。回边计数器有一个计算公式【回边计数器阈值=方法调用计数器阈值 * (OnStackReplacePercentage - InterpreterProfilePercentage)】,通过计算,在 JDK 21 Server 模式下,虚拟机回边计数器的阈值为 10700【10000*(140-33)】。
可以使用 java -XX:+PrintFlagsFinal -version 命令查看 JVM 默认配置
栈上分配&标量替换
栈上分配和标量替换是编译器的两种优化技术,它们虽然有一些相似之处,但并不完全相同。
- 栈上分配(Stack Allocation):一种优化技术,它将对象分配在栈上而不是堆上。这种技术适用于编译器可以确定对象不会逃逸出方法,并且对象的生命周期在方法内部结束的情况。通过在栈上分配对象,可以避免在堆上进行内存分配和垃圾回收的开销,从而提高程序的性能和内存使用效率。
- 标量替换(Scalar Replacement):与栈上分配类似,也是一种优化技术。它将一个复杂对象拆分成独立的成员变量,使其成为基本类型或基本类型数组的局部变量。这种技术适用于编译器可以确定对象的成员变量不会逃逸的情况。标量替换可以提供更细粒度的控制,使得编译器可以对独立的成员变量进行更精细的优化,例如寄存器分配和代码优化。
也就是说栈上分配,只是将对象从堆上分配到栈上了;而标量替换是更进一步的优化技术,将对象拆解成基本类型分配到栈上了。
锁消除代码演示
锁消除(Lock Elimination)也叫做同步消除,是一种编译器优化技术,它可以消除对于变量的不必要的锁定操作。锁消除的目的是减少锁的开销,提高程序的性能。 例如以下代码:
public void method() {
Object lock = new Object();
synchronized(lock){
System.out.println("www.javacn.site");
}
}而锁消除之后的代码为:
public void method(){
System.out.println("www.javacn.site");
}标量替换代码提示
未优化前的代码如下:
private static class Point {
private int x;
private int y;
}
public static void main(String[] args) {
Point point = createPoint(10, 20);
int sum = point.x + point.y;
System.out.println("Sum: " + sum);
}
private static Point createPoint(int x, int y) {
Point point = new Point();
point.x = x;
point.y = y;
return point;
}标量替换优化后的代码如下:
public static void main(String[] args) {
int x = 10;
int y = 20;
int sum = x + y;
System.out.println("Sum: " + sum);
}通过逃逸分析的优化能够减少垃圾回收的压力、减少内存分配和释放带来的性能损耗,并且有可能减少对锁的依赖,以及实现标量替换等,从而整体上提升了应用程序的运行效率。
如何进行JVM调优?
JVM 调优是一个很大的话题,在回答“如何进行 JVM 调优?”之前,首先我们要回答一个更为关键的问题,那就是,我们为什么要进行 JVM 调优?
只有知道了为什么要进行 JVM 调优之后,你才能准确的回答出来如何进行 JVM 调优?
要进行 JVM 调优无非就是以下两种情况:
- 目标驱动型的 JVM 调优,如,我们是为了最短的停顿时间所以要进行 JVM 调优,或者是我们为了最大吞吐量所以要进行 JVM 调优等。
- 问题驱动型的 JVM 调优,因为生产环境出现了频繁的 FullGC 了,导致程序执行变慢,所以我们要进行 JVM 调优。
所以,针对不同的 JVM 调优的手段和侧重点也是不同的。
总的来说,JVM 进行调优的流程如下:
- 确定 JVM 调优原因
- 分析 JVM(目前)运行情况
- 设置 JVM 调优参数
- 压测观测调优后的效果
- 应用调优后的配置
具体来说它们的执行如下。
确定JVM调优原因
先确定是目标驱动型的 JVM 调优,还是问题驱动型的 JVM 调优。
如果是目标性的 JVM 调优,那么 JVM 调优实现思路就比较简单了,如:
- 以最短停顿时间为目标的调优,只需要将垃圾收集器设置成以最短停顿时间的为目标的垃圾收集器即可,如 CMS 收集器或 G1 收集器。
- 以吞吐量为目标的调优,只需要将垃圾收集器设置为 Parallel Scavenge 和 Parallel Old 这种以吞吐量为主要目标的垃圾回收器即可。
如果是以问题驱动的 JVM 调优,那就要先分析问题是什么,然后再进行下一步的调优了。
分析JVM运行情况
我们可以借助于目前主流的监控工具 Prometheus + Grafana 和 JDK 自带的命令行工具,如 jps、jstat、jinfo、jstack 等进行 JVM 运行情况的分析。
主要分析的点是 Young GC 和 Full GC 的频率,以及垃圾回收的执行时间。
设置JVM调优参数
常见的 JVM 调优参数有以下几个:
- 调整堆内存大小:通过设置 -Xms(初始堆大小)和 -Xmx(最大堆大小)参数来调整堆内存大小,避免频繁的垃圾回收。
- 选择合适的垃圾回收器:根据应用程序的性能需求和特点,选择合适的垃圾回收器,如 Serial GC、Parallel GC、CMS GC、G1 GC 等。
- 调整新生代和老年代比:通过设置 -XX:NewRatio 参数来调整新生代和老年代的比例,优化内存分配。
- 设置合适的堆中的各个区域比例:通过设置 -XX:SurvivorRatio 参数和 -XX:MaxTenuringThreshold 参数来调整 Eden 区、Survivor 区和老年代的比例,避免过早晋升和过多频繁的垃圾回收。
- 设置对象从年轻代进入老年代的年龄值:-XX:InitialTenuringThreshold=7 表示 7 次年轻代存活的对象就会进入老年代。
- 设置元空间大小:在 JDK 1.8 版本中,元空间的默认大小会根据操作系统有所不同。具体来说,在 Windows 上,元空间的默认大小为 21MB;而在 Linux 上,其默认大小为 24MB。然而如果元空间不足也有可能触发 Full GC 从而导致程序执行变慢,因此我们可以通过 -XX:MaxMetaspaceSize=size 设置元空间的最大容量。
压测观测调优后的效果
JVM 参数调整之后,我们要通过压力测试来观察 JVM 参数调整前和调整后的差别,以确认调整后的效果。
应用调优的配置
在确认了 JVM 参数调整后的效果满足需求之后,就可以将 JVM 的参数配置应用与生产环境了。