当前位置: 首页 > news >正文

深入理解 Java 虚拟机:从原理到实践的全方位剖析

🌟个人主页:编程攻城狮

🌟人生格言:得知坦然 ,失之淡然

目录

🌷前言:

一、JVM 概述

1.1 JVM 的定义与作用

1.2 JVM 的发展历程

1.3 常见的 JVM 实现

二、JVM 内存模型

2.1 内存区域划分

2.2 各内存区域的 OOM 异常分析

2.3 内存模型与线程安全

三、垃圾回收机制

3.1 垃圾回收的基本概念

3.2 对象存活判定算法

3.3 引用的分类

3.4 垃圾回收算法

3.5 常见的垃圾收集器

四、类加载机制

4.1 类加载的过程

4.2 类加载器

4.3 双亲委派模型

五、JVM 调优实践

5.1 调优的目标与指标

5.2 常用的调优参数

5.3 调优的步骤与方法

5.4 常见问题及解决方案

六、总结与展望

🌈共勉:


🌷前言:

在 Java 技术体系中,Java 虚拟机(JVM)扮演着至关重要的角色。它是连接 Java 源代码与操作系统的桥梁,正是因为有了 JVM,才实现了 "一次编写,到处运行" 的跨平台特性。随着 Java 技术的不断发展,JVM 的功能也在持续完善,性能不断优化。本文将从 JVM 的基本原理出发,深入探讨其内部结构、内存模型、垃圾回收机制、类加载机制等核心内容,并结合实际应用场景,分析 JVM 调优的常用策略和最佳实践。无论你是 Java 初学者还是有一定经验的开发人员,相信都能从本文中获得有价值的知识和启发。

一、JVM 概述

1.1 JVM 的定义与作用

Java 虚拟机(Java Virtual Machine,简称 JVM)是一个虚构的计算机,它是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM 有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。

JVM 的主要作用体现在以下几个方面:

  • 实现跨平台性:Java 源代码被编译成字节码(.class 文件),而 JVM 负责将字节码解释或编译为本地机器码执行。不同操作系统只需安装对应的 JVM 实现,就能运行相同的字节码。
  • 内存管理:自动负责对象的内存分配和回收,减少了程序员手动管理内存的负担,降低了内存泄漏和野指针等问题的出现概率。
  • 提供安全机制:通过字节码验证、类加载器的命名空间隔离等机制,保障 Java 程序的安全运行。
  • 支持多线程:JVM 提供了线程调度、同步等机制,使得 Java 程序能够高效地进行多线程编程。

1.2 JVM 的发展历程

自 Java 诞生以来,JVM 已经经历了多个版本的演进,以下是其发展历程中的重要里程碑:

时间事件重要意义
1995 年Java 语言正式发布,首个 JVM 实现诞生开创了跨平台编程语言的新时代
1999 年J2SE 1.3 发布,HotSpot 虚拟机成为默认虚拟机引入了热点代码编译技术,大幅提升了执行效率
2006 年Java SE 6 发布,JVM 规范进一步完善增强了对注解、脚本语言等的支持
2011 年Java SE 7 发布,引入 G1 垃圾收集器提供了低延迟的垃圾回收解决方案
2014 年Java SE 8 发布,引入元空间(Metaspace)取代永久代,解决了永久代内存溢出问题
2017 年Java SE 9 发布,引入模块化系统改变了 JVM 的类加载机制,提高了应用的安全性和可维护性
2018 年Java SE 11 发布,长期支持版本增强了垃圾回收器,提高了性能和稳定性
2021 年Java SE 17 发布,最新长期支持版本进一步优化了 JVM 性能,增强了安全性和功能性

1.3 常见的 JVM 实现

除了 Oracle 官方的 HotSpot 虚拟机外,还有其他一些知名的 JVM 实现:

  • OpenJ9:由 IBM 开发的开源 JVM,以启动速度快、内存占用低著称,适用于云原生和微服务场景。
  • GraalVM:一种高性能的多语言虚拟机,支持 Java、JavaScript、Python 等多种语言,还提供了 AOT(提前编译)功能。
  • Zing:由 Azul Systems 开发的 JVM,专为低延迟、高吞吐量的应用设计,适合金融、电信等对性能要求极高的领域。

二、JVM 内存模型

2.1 内存区域划分

根据《Java 虚拟机规范》,JVM 在运行时将内存划分为以下几个区域,每个区域都有其特定的功能和生命周期:

  • 程序计数器(Program Counter Register):是一块较小的内存空间,可看作是当前线程所执行的字节码的行号指示器。由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为 "线程私有" 的内存。

  • Java 虚拟机栈(Java Virtual Machine Stacks):也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  • 本地方法栈(Native Method Stacks):与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

  • Java 堆(Java Heap):是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做 "GC 堆"(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

  • 方法区(Method Area):与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。对于习惯在 HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为 "永久代"(Permanent Generation),本质上两者并不等价,仅仅是因为 HotSpot 虚拟机的设计团队选择把 GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如 BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样 "永久" 存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收 "成绩" 比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是必要的。

  • 运行时常量池(Runtime Constant Pool):是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern () 方法。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

2.2 各内存区域的 OOM 异常分析

在 JVM 运行过程中,各个内存区域都可能发生 OutOfMemoryError(OOM)异常,了解这些异常的产生原因和表现形式,对于排查问题至关重要:

  • 程序计数器:是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

  • Java 虚拟机栈和本地方法栈:在 Java 虚拟机规范中,对这两个区域规定了两种异常:

    • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。
    • 如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。
  • Java 堆:当 Java 堆中没有内存完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。

  • 方法区和运行时常量池:当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

2.3 内存模型与线程安全

Java 内存模型(Java Memory Model, JMM)定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读 / 写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

JMM 的主要目标是定义程序中各个变量的访问规则,即关注在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里的变量(Variables)与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的,不会被共享,自然就不会有竞争问题。

JMM 规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

三、垃圾回收机制

3.1 垃圾回收的基本概念

垃圾回收(Garbage Collection,简称 GC)是 JVM 的重要功能之一,它自动管理内存的分配和回收,减轻了程序员的负担。垃圾回收主要关注以下三个问题:

  • 哪些内存需要回收?:主要是 Java 堆和方法区中的对象和数据。
  • 什么时候回收?:当对象不再被引用,或者内存空间不足时。
  • 如何回收?:采用不同的垃圾回收算法,如标记 - 清除、复制、标记 - 整理等。

3.2 对象存活判定算法

判断对象是否存活是垃圾回收的前提,常用的判定算法有:

  • 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。这种方法实现简单,判定效率也很高,在大部分情况下都是一个不错的算法。但是,在 Java 领域,至少主流的 Java 虚拟机里面没有选用引用计数法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

  • 可达性分析算法:当前主流的商用程序语言(Java、C# 等)的内存管理子系统,都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列称为 "GC Roots" 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 "引用链"(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。

在 Java 语言中,可作为 GC Roots 的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
  • Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如 NullPointerException、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized 关键字)持有的对象。
  • 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。

3.3 引用的分类

在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。

  • 强引用:就是指在程序代码之中普遍存在的,类似 "Object obj = new Object ()" 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

  • 软引用:用来描述一些还有用,但非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。

  • 弱引用:也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK 1.2 之后,提供了 WeakReference 类来实现弱引用。

  • 虚引用:也称为 "幽灵引用" 或者 "幻影引用",它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了 PhantomReference 类来实现虚引用。

3.4 垃圾回收算法

  • 标记 - 清除算法:是最基础的收集算法,它分为 "标记" 和 "清除" 两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,回收未被标记的对象。它的主要不足有两个:一个是执行效率不稳定,如果 Java 堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除操作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;另一个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

  • 复制算法:为了解决标记 - 清除算法面对大量可回收对象时执行效率低的问题,复制算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。

  • 标记 - 整理算法:复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况,所以在老年代一般不能直接选用这种算法。针对老年代对象的特点,提出了另外一种标记 - 整理(Mark-Compact)算法,标记过程仍然与 "标记 - 清除" 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

  • 分代收集算法:当前商业虚拟机的垃圾收集都采用 "分代收集"(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 "标记 - 清除" 或者 "标记 - 整理" 算法来进行回收。

3.5 常见的垃圾收集器

垃圾收集器是垃圾回收算法的具体实现,不同的垃圾收集器有不同的特点和适用场景:

  • Serial GC:是最基本、发展历史最悠久的收集器,曾经(在 JDK 1.3.1 之前)是 HotSpot 虚拟机新生代收集的唯一选择。它是一个单线程收集器,不仅仅意味着它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。Serial 收集器对于运行在 Client 模式下的虚拟机来说是一个很好的选择。

  • ParNew GC:其实就是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括 Serial 收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一样,在实现上这两种收集器也共用了相当多的代码。ParNew 收集器是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,除了 Serial 收集器外,只有它能与 CMS 收集器配合工作。

  • Parallel Scavenge GC:是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 - XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 - XX:GCTimeRatio 参数。

  • Serial Old GC:是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记 - 整理算法。这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。如果在 Server 模式下,那么它主要还有两大用途:一种是在 JDK 5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用,另一种就是作为 CMS 收集器发生失败时的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

  • Parallel Old GC:是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记 - 整理算法实现。直到 JDK 6 时才开始提供。在此之前,新生代的 Parallel Scavenge 收集器一直处于比较尴尬的状态,因为它只能与 Serial Old 收集器配合使用,而 Serial Old 收集器在服务端应用性能上的 "拖累",使得 Parallel Scavenge 收集器也未必能在整体上获得吞吐量最大化的效果。Parallel Old 收集器的出现,"吞吐量优先" 收集器终于有了比较名副其实的应用组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器的组合。

  • CMS GC:全称是 Concurrent Mark Sweep,是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用都集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。CMS 收集器是基于标记 - 清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:初始标记(Initial Mark)、并发标记(Concurrent Mark)、重新标记(Remark)、并发清除(Concurrent Sweep)。其中初始标记、重新标记这两个步骤仍然需要 "Stop The World"。初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。并发标记阶段就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。而重新标记阶段则是为了修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。CMS 是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低延迟。但是 CMS 收集器还存在着一些明显的缺点:首先,CMS 收集器对 CPU 资源非常敏感。其次,CMS 收集器无法处理浮动垃圾(Floating Garbage),可能出现 "Concurrent Mode Failure" 失败而导致另一次 Full GC 的产生。最后,由于 CMS 是基于标记 - 清除算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 Full GC。

  • G1 GC:G1(Garbage-First)收集器是 JDK 7 中正式投入使用的用于取代 CMS 的收集器。G1 收集器基于标记 - 整理算法实现,也就是说它不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。G1 收集器可以非常精确地控制停顿,既能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。G1 收集器的运作过程大致可划分为以下几个步骤:初始标记(Initial Mark)、并发标记(Concurrent Mark)、最终标记(Final Mark)、筛选回收(Live Data Counting and Evacuation)。G1 收集器的优势在于:并行与并发、分代收集、空间整合、可预测的停顿。

四、类加载机制

4.1 类加载的过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段。其中验证、准备、解析三个部分统称为连接(Linking)。

  • 加载:"加载" 是 "类加载" 过程的第一个阶段。在加载阶段,虚拟机需要完成以下三件事情:

    • 通过一个类的全限定名来获取定义此类的二进制字节流。
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
  • 验证:验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

  • 准备:准备阶段是正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下:首先,这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。其次,这里所说的初始值 "通常情况" 下是数据类型的零值。

  • 解析:解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用(Symbolic References)是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。直接引用(Direct References)是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

  • 初始化:类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码(或者说字节码)。在准备阶段,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段就是执行类构造器<clinit>() 方法的过程。

4.2 类加载器

类加载器虽然只用于实现类的加载动作,但它在 Java 程序中起到的作用却远超类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否 "相等",只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

在 Java 虚拟机中,存在以下几种类加载器:

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器负责加载存放在<JAVA_HOME>\lib 目录,或者被 - Xbootclasspath 参数所指定的路径中存放的,而且是 Java 虚拟机能够识别的(按照文件名识别,如 rt.jar、tools.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库到虚拟机的内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,那直接使用 null 代替即可。

  • 扩展类加载器(Extension ClassLoader):这个类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以 Java 代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库。根据 "扩展类加载器" 这个名称,就可以推断出这是一种为 Java 扩展机制提供服务的类加载器。系统类加载器也无法直接被 Java 程序引用,需要通过父类加载器委派的方式间接使用。

  • 应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher$AppClassLoader 来实现。由于这个类加载器是 ClassLoader 类中的 getSystemClassLoader () 方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所有的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

  • 自定义类加载器:除了上述三种系统提供的类加载器外,用户还可以根据需要自定义类加载器。自定义类加载器需要继承 ClassLoader 类,并覆盖 findClass () 方法。自定义类加载器可以实现一些特殊的功能,如加密解密类文件、从网络加载类等。

4.3 双亲委派模型

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是 Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

五、JVM 调优实践

5.1 调优的目标与指标

JVM 调优的目标是在满足应用程序功能需求的前提下,尽可能提高系统的性能和稳定性。常见的调优指标包括:

  • 吞吐量:单位时间内完成的任务数量,对于批处理应用来说尤为重要。
  • 响应时间:从请求发出到收到响应的时间,对于交互式应用来说至关重要。
  • 内存占用:应用程序运行时占用的内存大小,直接影响系统的可扩展性。
  • GC 停顿时间:垃圾回收过程中导致应用程序暂停的时间,过长的停顿会影响用户体验。
  • GC 频率:垃圾回收发生的次数,过于频繁的 GC 会消耗大量系统资源。

5.2 常用的调优参数

JVM 提供了大量的命令行参数用于调优,以下是一些常用的参数:

参数类别参数名称说明
堆内存设置-Xms初始堆大小
堆内存设置-Xmx最大堆大小
堆内存设置-Xmn新生代大小
堆内存设置-XX:SurvivorRatio新生代中 Eden 区与 Survivor 区的比例
堆内存设置-XX:NewRatio老年代与新生代的比例
垃圾回收器设置-XX:+UseSerialGC使用 Serial 收集器
垃圾回收器设置-XX:+UseParallelGC使用 Parallel Scavenge 收集器
垃圾回收器设置-XX:+UseConcMarkSweepGC使用 CMS 收集器
垃圾回收器设置-XX:+UseG1GC使用 G1 收集器
GC 日志设置-XX:+PrintGCDetails打印详细的 GC 日志
GC 日志设置-XX:+PrintGCDateStamps打印 GC 发生的时间戳
GC 日志设置-Xloggc:filename指定 GC 日志文件的路径
其他设置-XX:MetaspaceSize元空间初始大小
其他设置-XX:MaxMetaspaceSize元空间最大大小
其他设置-XX:MaxDirectMemorySize直接内存最大大小

5.3 调优的步骤与方法

JVM 调优是一个系统性的过程,需要遵循一定的步骤和方法:

  1. 监控与分析:首先需要对应用程序的运行状态进行监控,收集相关的性能数据,如 GC 日志、线程快照、内存使用情况等。常用的监控工具包括 JConsole、VisualVM、JProfiler 等。通过对这些数据的分析,找出系统存在的性能瓶颈。

  2. 确定调优目标:根据应用程序的类型和业务需求,确定明确的调优目标,如提高吞吐量、减少响应时间等。

  3. 调整参数:根据分析结果和调优目标,调整相应的 JVM 参数。参数调整应该遵循循序渐进的原则,每次只调整少数几个参数,并观察调整后的效果。

  4. 验证效果:参数调整后,需要对应用程序的性能进行重新测试和验证,看是否达到了预期的调优目标。如果没有达到,需要重新分析和调整参数。

  5. 持续优化:JVM 调优不是一次性的工作,随着应用程序的版本迭代和业务量的变化,可能需要进行持续的优化和调整。

5.4 常见问题及解决方案

在 JVM 调优过程中,经常会遇到一些常见的问题,以下是一些典型问题及解决方案:

  • 内存溢出(OOM):这是最常见的问题之一,可能是由于堆内存设置过小、内存泄漏等原因引起的。解决方法包括:增大堆内存大小、排查并修复内存泄漏问题、优化对象的创建和使用方式等。

  • GC 停顿时间过长:可能是由于垃圾回收器选择不当、堆内存设置不合理等原因引起的。解决方法包括:更换合适的垃圾回收器(如 G1、CMS)、调整堆内存大小和各代比例、优化对象的生命周期等。

  • CPU 占用过高:可能是由于频繁的 GC、线程死锁、代码逻辑不合理等原因引起的。解决方法包括:分析 CPU 占用高的线程,找出问题代码并优化、调整 GC 参数减少 GC 频率、排查并解决线程死锁问题等。

  • 类加载问题:可能是由于类加载器泄漏、类冲突等原因引起的。解决方法包括:排查类加载器的使用是否合理、避免重复加载类、解决类冲突问题等。

六、总结与展望

本文全面介绍了 Java 虚拟机的核心知识,包括 JVM 的概述、内存模型、垃圾回收机制、类加载机制以及 JVM 调优实践等内容。通过对这些内容的学习,我们可以深入理解 JVM 的工作原理,掌握 JVM 调优的基本方法和技巧,从而提高 Java 应用程序的性能和稳定性。

随着 Java 技术的不断发展,JVM 也在持续演进。未来,JVM 可能会在以下几个方面取得进一步的发展:

  • 性能优化:不断优化垃圾回收算法和类加载机制,进一步提高 JVM 的执行效率和响应速度。
  • 适应新硬件:更好地支持多核处理器、大内存等新硬件环境,充分发挥硬件的性能优势。
  • 增强安全性:加强 JVM 的安全机制,提高 Java 应用程序的安全性。
  • 支持新特性:为 Java 语言的新特性提供更好的支持,如值类型、密封类等。

作为 Java 开发者,我们需要不断学习和掌握 JVM 的新知识、新特性,以便更好地应对实际开发中遇到的问题和挑战。同时,我们也应该积极参与到 JVM 的开源社区中,为 JVM 的发展贡献自己的力量。

希望本文能够为广大 Java 开发者提供有价值的参考,帮助大家更好地理解和使用 Java 虚拟机。在实际应用中,我们还需要结合具体的业务场景和需求,灵活运用所学的知识,不断实践和总结,才能真正掌握 JVM 调优的精髓。

深入剖析JVM内存模型,包括堆、栈、方法区等的具体作用和相互关系。

详细讲解JVM垃圾回收算法的原理和优缺点。

结合实际案例,探讨JVM调优在不同应用场景下的具体策略和技巧。

🌈共勉:

以上就是本篇博客所有内容,如果对你有帮助的话可以点赞,关注走一波~🌻

http://www.dtcms.com/a/423758.html

相关文章:

  • 网站谷歌seo做哪些凌点视频素材网
  • 手机app应用网站C语言做网站需要创建窗口吗
  • uniapp 安卓FTP上传下载操作原生插件
  • 国外知名平面设计网站黄骅打牌吧
  • C++ I/O流与文件操作速查
  • 网站制作哪家好又便宜做电商网站的流程
  • 网络边界突围:运营商QoS限速策略
  • 【笔记】在WPF中Decorator是什么以及何时优先考虑 Decorator 派生类
  • [算法练习]Day 4:定长滑动窗口
  • 外汇交易网站开发做网站前端后台
  • 小红书网站建设目的优化师简历
  • 集群的概述和分类和负载均衡集群
  • 专业的商城网站开发搜索引擎优化不包括
  • 哈尔滨市延寿建设局网站wordpress 主题添加
  • 技术实践指南:多模态RAG从数据预处理到生成响应的完整流程
  • 新中地三维GIS开发智慧城市效果和应用场景
  • 做产品封面的网站赵公口网站建设公司
  • Redis开发07:使用stackexchange.redis库实现简单消息队列
  • RabbitMQ的安装集群、镜像队列配置
  • php 网站后台模板zencart外贸网站建设
  • IS-IS 与 OSPF 路由汇总机制:边界、应用与核心差异深度分析报告
  • 福彩双色球第2025113期篮球号码分析
  • 做网站公司 蓝纤科技百姓网二手车
  • Dubbo源码解读与实战-基础知识(上)
  • 专业网站制作公司招聘造一个官方网站
  • 【网络通信】服务器部署服务的时候服务ip配置127.0.0.1和外网ip的区别
  • 【C++】命名空间
  • [特殊字符] LeetCode 143 重排链表(Reorder List)详解
  • 轻量级webgis环境搭建
  • 内网网站搭建教程做平面设计都在那个网站找免费素材