Java虚拟机(JVM)对于Java应用的至关重要性。在这篇文章中,我将带领你深入探索JVM的内部机制,理解其核心概念,并分享一些深刻的个人见解。让我们一起揭开Java虚拟机的神秘面纱,为你的Java编程之旅注入更多活力。
点个关注吧!后续继续分析更多有用的知识,球球帅哥美女们了
目录
一. 介绍
1. 什么是Java虚拟机(JVM)
1.1 JVM的定义和作用
Java虚拟机(JVM)是Java编程语言的运行环境,负责将Java源代码编译成字节码,并在不同平台上执行。它提供了一个抽象的计算机硬件和操作系统平台,使得Java程序具有跨平台的特性。
JVM的主要作用包括:
- 将Java源代码编译为字节码。
- 提供字节码解释器或即时编译器,将字节码转换为机器码执行。
- 管理内存和提供垃圾回收机制。
- 实现Java的多线程和并发机制。
- 提供Java标准库和运行时环境支持。
1.2 Java程序为什么需要虚拟机
Java程序需要虚拟机的主要原因是实现跨平台性。由于JVM提供了一个统一的执行环境,Java程序可以在任何安装了相应JVM的计算机上运行,而无需重新编译。这使得Java成为一种具有高度可移植性的编程语言,适用于各种不同的操作系统和硬件平台。
2. JVM的重要性
2.1 跨平台性和移植性
JVM的最显著特性之一是跨平台性。由于Java程序是在虚拟机上运行的,而不是直接在硬件上执行,因此同一份Java代码可以在各种不同操作系统上运行,而无需修改。这为开发人员提供了更大的灵活性和便利性。
2.2 内存管理和垃圾回收
JVM负责管理Java程序的内存,包括分配内存、回收不再使用的内存。垃圾回收机制是JVM的一项重要功能,它通过自动识别和释放不再被引用的对象,有效地防止内存泄漏和提高程序的执行效率。这为开发人员减轻了内存管理的负担,提高了程序的稳定性和性能
二. JVM的基本结构
1. 类加载器(Class Loader)
1.1 类加载的过程
类加载器是Java虚拟机的重要组成部分,负责将类的字节码加载到内存中并生成Class对象。类加载过程包括以下几个步骤:
- 加载(Loading): 类加载器通过类的全限定名查找类的字节码文件,并将其加载到内存中。
- 链接(Linking): 链接阶段包括三个步骤:验证、准备和解析。
- 验证(Verification): 确保加载的类符合Java虚拟机规范,防止恶意代码的执行。
- 准备(Preparation): 为类的静态变量分配内存空间,并设置默认初始值。
- 解析(Resolution): 将类、字段和方法的符号引用解析为直接引用。
- 初始化(Initialization):执行类的初始化代码,包括静态变量赋值和静态代码块的执行。这是类加载的最后一个阶段。
1.2 不同类型的类加载器
在Java虚拟机中存在不同类型的类加载器,主要分为以下几类:
- 启动类加载器(Bootstrap Class Loader): 负责加载Java的核心类库,是虚拟机的一部分,通常用本地代码实现。
- 扩展类加载器(Extension Class Loader):负责加载Java的扩展类库,位于`<JAVA_HOME>/lib/ext`目录下。
- 应用程序类加载器(Application Class Loader): 也称为系统类加载器,负责加载应用程序的类,是最常用的类加载器。
- 自定义类加载器:开发人员可以根据需要创建自定义的类加载器,实现特定的加载需求,比如动态加载类。
2. 运行时数据区
2.1 程序计数器
程序计数器是当前线程所执行的字节码的行号指示器。在多线程环境下,每个线程都有一个独立的程序计数器,确保线程切换后能够恢复到正确的执行位置。
2.2 Java虚拟机栈
Java虚拟机栈用于存储方法的局部变量、操作数栈、动态链接、方法出口等信息。每个方法在执行的同时都会创建一个栈帧用于存储这些信息,方法执行结束后,对应的栈帧会被弹出。
2.3 本地方法栈
本地方法栈类似于Java虚拟机栈,但服务于本地方法,即由Native方法(使用JNI技术调用的方法)服务。
2.4 Java堆
Java堆是Java虚拟机管理的最大一块内存区域,用于存放对象实例。垃圾回收主要发生在Java堆中。
2.5 方法区(永久代/元空间)
方法区存储类的元数据,包括类的结构信息、运行时常量池、字段和方法数据等。在不同的虚拟机实现中,方法区被称为永久代或元空间。
3. 执行引擎
JVM(Java 虚拟机)的执行引擎是负责执行 Java 字节码的组件。它负责将编译过的 Java 代码(字节码)翻译成本地机器代码,然后在计算机上执行。执行引擎是 JVM 中的核心组件之一,其主要任务是解释或编译字节码并执行相应的机器指令。
JVM 的执行引擎主要有两种实现方式:
1. 解释器(Interpreter):
- 解释器通过逐条解释执行字节码,即将字节码翻译成机器码并逐条执行。
- 解释器的优点是简单、灵活,适用于不同平台。
- 缺点是执行效率相对较低,因为需要在运行时解释字节码。
2. 即时编译器(Just-In-Time Compiler,JIT Compiler):
- 即时编译器在运行时将整个字节码文件翻译成本地机器码,然后直接执行机器码。
- JIT 编译器的优点是执行速度快,因为避免了重复解释字节码的过程。
- 缺点是在程序启动时可能会有一些额外的启动时间,因为需要先将字节码编译成本地机器码。
执行引擎还可以使用混合模式,结合解释器和即时编译器的优势。在混合模式下,解释器可以快速启动应用程序,而 JIT 编译器则在运行时对频繁执行的代码进行即时编译,提高执行效率。
JVM 的执行引擎是 Java 程序在虚拟机中执行的关键组件之一,它使得 Java 能够实现“一次编写,到处运行”的特性,即编写的 Java 代码可以在不同平台上通过 JVM 运行。通过执行引擎,Java 程序能够在虚拟机中执行,并与底层硬件和操作系统解耦。
三. Java内存模型(JMM)
1. JMM的基本概念
Java内存模型定义了Java程序中多线程之间的共享变量的访问规则,确保多线程之间能够正确、安全地进行数据交互。
JMM 通过内存可见性、原子性、有序性和 happens-before 关系等概念来定义多线程间共享内存的行为。理解 JMM 是编写正确且高效的多线程程序的关键,开发者需要考虑到线程间的内存同步、可见性等问题,以确保程序的正确性。
1.1 主内存和工作内存
- 主内存(Main Memory): 所有线程共享的内存区域,存储着共享变量的副本。
- 工作内存(Working Memory):每个线程独有的内存区域,存储着主内存中共享变量的本地副本。线程对共享变量的操作都在工作内存中进行,不直接影响主内存。
1.2 原子性、可见性、有序性
- 原子性(Atomicity): 单个操作是原子的,不可分割。多个操作一起执行,要么全部成功,要么全部失败。
- 可见性(Visibility):当一个线程修改了共享变量的值,其他线程能够立即看到这个变化。
- 有序性(Ordering): 程序执行的顺序按照代码的先后顺序,但是在多线程环境下,由于指令重排等原因,实际执行顺序可能与代码顺序不同。
2. Happens-Before规则
Happens-Before规则定义了Java内存模型中多线程之间操作的顺序规则,确保多线程之间的协作正确进行。
Happens-Before 是 Java 内存模型(Java Memory Model,JMM)中一种偏序关系,用于描述操作之间的顺序关系。具体来说,如果操作 A happens-before 操作 B,那么 A 对 B 的影响就能在 B 之前被其他线程观察到。这种关系的定义有助于程序员理解和预测多线程程序的行为。
2.1 线程之间的协作
Happens-Before规则包括:
- 程序次序规则:一个线程内,按照代码顺序执行。
- 锁定规则:解锁操作Happens-Before于随后的对同一锁的加锁操作。
- volatile变量规则:对volatile变量的写操作Happens-Before于随后的对同一变量的读操作。
- 线程启动规则:Thread对象的start()方法Happens-Before于该线程的任何操作。
- 线程终止规则:线程的所有操作Happens-Before于其他线程检测到该线程已经终止。
- 中断规则:对线程interrupt()方法的调用Happens-Before于被中断线程的代码检测到中断事件的发生。
2.2 volatile关键字的作用
使用volatile关键字修饰的变量具有可见性和禁止指令重排的特性。当一个线程修改了volatile变量的值,其他线程能够立即看到这个变化,同时禁止了volatile变量周围的代码指令重排。这使得volatile变量在一些特定场景下是一种简单而有效的线程同步机制。
四. 垃圾回收与内存优化
程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。
GC是不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行清楚那个对象,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用System.gc 方法来"建议"执行垃圾收集器,但是他是否执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。
JVM 新生代、老年代、永久代(方法区)
在 Java 虚拟机中,内存分为多个区域,其中包括新生代、老年代和永久代(或元空间)。这些区域主要用于存储不同类型的对象,以及类的元信息。在 Java 8 及之后的版本中,永久代被元空间(Metaspace)取代,但为了方便理解,我们仍然使用“永久代”来表示元空间。
1. **新生代(Young Generation)**:
- 新生代是用于存储新创建的对象的区域。
- 新生代被分为三部分:Eden 区和两个 Survivor 区(通常称为 Survivor 0 和 Survivor 1)。
- 大多数新创建的对象会被分配到 Eden 区,经过一定次数的垃圾回收后,仍然存活的对象将会被移到 Survivor 区。两个 Survivor 区之间会发生对象的相互复制,以保证每次只有一个 Survivor 区是空的。
- 过程中,经过多次垃圾回收后仍然存活的对象会被晋升到老年代。
2. 老年代(Old Generation):
- 老年代主要用于存储生命周期较长的对象。
- 经过多次垃圾回收后仍然存活的对象会被晋升到老年代。
- 在新生代中可能发生的 Minor GC(年轻代垃圾回收)不会涉及到老年代。
3. 永久代(Permanent Generation):
- 永久代主要用于存储类的元信息、常量池、静态变量等。
- 在 Java 8 及之前的版本中存在永久代,但由于永久代的一些问题,Java 8 引入了元空间(Metaspace)来替代永久代。
- 元空间不再是虚拟机内存的一部分,而是使用本地内存,因此不受默认堆大小的限制,而且支持动态调整。
4. 元空间(Metaspace):
- 元空间是 Java 8 及之后版本中取代了永久代的新的方法区实现。
- 元空间使用本地内存(而不是 Java 堆内存),因此不再受到默认堆大小的限制。
- 元空间主要用于存储类的元信息、常量池、静态变量等。
需要注意的是,新生代和老年代之间的对象晋升,以及永久代(或元空间)中的类的卸载等都可能触发垃圾回收。在实际应用中,合理调整堆大小和永久代(或元空间)大小,以及选择合适的垃圾回收策略,对于提高应用程序性能是非常重要的。
1. 垃圾回收算法
1.1 标记-清除算法
标记-清除算法分为两个阶段:标记阶段和清除阶段。首先,标记出所有需要回收的对象,然后在清除阶段将这些对象的内存释放。该算法的缺点是产生不连续的内存碎片。
1.2 复制算法
复制算法将内存分为两块,每次只使用其中一块。当一块内存用完后,将存活的对象复制到另一块内存中,然后清空当前内存。这样可以避免内存碎片,但是需要额外的内存空间。
1.3 标记-整理算法
标记-整理算法结合了标记-清除和复制算法的优点。首先标记出需要回收的对象,然后将存活的对象整理到内存的一端,然后清理掉边界外的内存。这样既避免了内存碎片,又节省了额外内存空间。
2. 常见的垃圾收集器
2.1 Serial收集器
Serial收集器是最简单的垃圾收集器,采用单线程进行垃圾回收。适用于单核处理器或小型应用场景。
2.2 Parallel收集器
Parallel收集器是Serial的多线程版本,通过多线程提高垃圾回收的效率。适用于多核处理器和对吞吐量要求较高的场景。
2.3 CMS收集器
CMS(Concurrent Mark-Sweep)收集器是一种并发垃圾收集器,主要减少垃圾回收造成的停顿时间。适用于对响应时间要求较高的应用场景。
2.4 G1收集器
G1(Garbage-First)收集器是一种面向服务端应用的垃圾收集器,具有高吞吐和低停顿的特点。适用于大内存且对延迟要求较高的场景。
3. 内存优化技巧
3.1 对象的创建和销毁
- 对象池技术: 重用对象而不是频繁创建和销毁,减少内存开销。
- 适时手动释放资源: 对于一些占用资源较多的对象,手动进行资源释放,防止内存泄漏。
3.2 避免内存泄漏的常见场景
- 正确使用集合类:注意集合类的使用,及时清理不再需要的对象。
- 避免匿名类持有外部类引用: 使用静态内部类或弱引用来防止内存泄漏。
- 合理使用缓存: 需要根据具体场景合理使用缓存,避免无限制的缓存对象。
通过合理选择垃圾收集器、优化算法和采用内存优化技巧,可以提高Java应用程序的性能和稳定性。
五. Java性能调优
1. JVM调优参数
1.1 堆内存大小调整
调整堆内存大小是JVM性能调优的关键之一。可以通过以下参数进行调整:
--Xms: 初始堆大小。
--Xmx: 最大堆大小。
--XX:NewSize:初始新生代大小。
--XX:MaxNewSize:最大新生代大小。
1.2 线程栈大小设置
调整线程栈大小有助于避免栈溢出。可以使用参数:
- -Xss: 设置每个线程的栈大小。
1.3 GC调优参数
垃圾回收调优是提高性能的重要一环,可以使用以下参数:
- **-XX:+UseParallelGC:** 使用并行垃圾回收器。
- **-XX:+UseConcMarkSweepGC:** 使用CMS垃圾回收器。
- **-XX:+UseG1GC:** 使用G1垃圾回收器。
- **-XX:MaxGCPauseMillis:** 设置垃圾回收最大暂停时间。
2. 工具和监控
2.1 JConsole
JConsole是Java的监控和管理控制台工具,可以用于监视Java应用程序的性能,查看堆内存、线程、类加载等信息。
2.2 VisualVM
VisualVM是一款功能强大的Java虚拟机监控、调优和分析工具,支持多种插件,可视化地展示应用程序的性能状况。
2.3 GC日志分析
通过配置JVM参数,可以生成GC日志,结合工具如GCViewer等进行分析,了解垃圾回收的情况,进而进行性能调优。
通过合理设置JVM调优参数和使用监控工具,可以有效提高Java应用程序的性能和稳定性。调优的关键在于根据具体应用场景和硬件环境选择合适的参数和工具。
六. 类加载机制
1. 类加载的机制
JVM(Java Virtual Machine)类加载机制是Java程序运行时的一部分,它负责加载、链接和初始化Java类。类加载机制的主要任务是将Java类的字节码加载到内存中,并使其在运行时能够被正确执行。
类加载器在加载类时采用双亲委派模型,即类加载器会优先委派给其父类加载器加载类,只有在父类加载器无法加载时,才由当前类加载器来加载。这种模型可以保证类的唯一性,防止类的重复加载。
JVM的类加载机制使得Java具有平台无关性,因为字节码可以在任何支持Java虚拟机的平台上执行。同时,类加载机制也提供了灵活性,允许开发者自定义类加载器以满足特定需求。
1.1. 加载
类加载的第一个阶段是加载。在加载阶段,类加载器负责查找并加载类的字节码文件,将其加载到内存中。加载过程包括以下步骤:
- 定位类文件: 类加载器根据类的全限定名定位类文件,通常从文件系统或网络中加载字节码文件。
- 读取类文件: 类加载器读取类文件的字节码数据,并将其存储在内存中。
- 创建Class对象:加载完成后,JVM会创建一个表示该类的Class对象,该对象包含了类的结构信息。
加载阶段完成后,类的字节码被存储在方法区(元空间)中,但并没有立即执行类的初始化。
1.2 链接
链接阶段包括三个步骤:验证、准备和解析。
- 验证(Verification):确保加载的类符合Java虚拟机规范,防止恶意代码的执行。
- 准备(Preparation):为类的静态变量分配内存空间,并设置默认初始值。
- 解析(Resolution):将类、字段和方法的符号引用解析为直接引用。
1. 验证
在验证阶段,对加载的类进行各种验证,以确保其符合Java虚拟机规范,防止恶意代码的执行。验证过程包括:
- 文件格式验证:确保字节码文件的格式正确。
- 元数据验证:验证类、字段、方法的元数据信息的正确性。
- 字节码验证:确保字节码的执行是类型安全的。
2. 准备
在准备阶段,为类的静态变量分配内存空间,并设置默认初始值。这个阶段不涉及到类中的静态变量的赋值,赋值在初始化阶段完成。
3. 解析
在解析阶段,将类、字段和方法的符号引用解析为直接引用。符号引用是源代码中使用的标识,而直接引用是在内存中直接指向目标的指针或偏移量。
链接阶段完成后,类的准备工作就基本完成了,接下来进入初始化阶段。
1.3. 初始化
初始化阶段是类加载的最后一个阶段,负责执行类的初始化代码,包括静态变量的赋值和执行静态代码块。初始化是类加载的最终步骤,只有在初始化完成后,类才算真正被加载到内存中。
初始化阶段的触发条件包括:
- 创建类的实例
- 访问类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
在初始化阶段,JVM会按照代码中的顺序执行静态变量的赋值和静态代码块的内容。
2. 类加载器的分类
2.1 启动类加载器
启动类加载器是最顶层的类加载器,负责加载Java的核心类库。它是由C++实现的,通常不继承自`java.lang.ClassLoader`,而是由JVM自身实现。
启动类加载器主要负责加载以下内容:
- Java核心类库(如`java.lang`、`java.util`等)
- JVM系统属性指定的类库
由于启动类加载器是用本地代码实现的,因此在Java中无法直接获取它的引用,也无法自定义启动类加载器。
2.2 扩展类加载器
扩展类加载器(Extension ClassLoader)是系统类加载器的父类,负责加载Java的扩展类库。扩展类加载器是由Java实现的,是`java.net.URLClassLoader`的子类。
扩展类加载器主要负责加载以下内容:
- 位于`<JAVA_HOME>/lib/ext`目录下的JAR包和类文件
扩展类加载器在加载类时会先委托给其父类加载器(通常是启动类加载器),只有在父类加载器无法加载的情况下,才由扩展类加载器来加载。
2.3 应用类加载器
应用类加载器(Application ClassLoader),也称为系统类加载器,是Java中最常用的类加载器。它是扩展类加载器的子类,是由Java实现的,通常是`sun.misc.Launcher$AppClassLoader`。
应用类加载器主要负责加载以下内容:
- 应用程序类路径(Classpath)下的类、JAR包和资源文件
应用类加载器是大多数Java应用程序默认的类加载器,也是开发者自定义类加载器的基础。在加载类时,应用类加载器会先委托给其父类加载器(扩展类加载器),如果父类加载器无法加载,则应用类加载器会尝试加载。
类加载机制是Java中重要的一部分,通过深入了解类加载的过程和类加载器的分类,可以更好地理解Java应用程序的运行机制。
七. Java字节码
1. 字节码的概念
Java字节码是一种中间代码,它是Java源代码编译后生成的二进制格式,通常以`.class`文件的形式存在。字节码并不直接针对特定硬件平台,而是为Java虚拟机(JVM)设计的,这使得Java具有跨平台性,可以在任何支持JVM的平台上运行。
Java字节码的特点:
1. 跨平台性:由于字节码是在虚拟机上执行的,而不是在物理硬件上,因此Java程序可以在任何具有兼容JVM的平台上运行,无需重新编译。
4. 字节码指令集:字节码是由一组特定的指令构成,这些指令被设计为能够执行各种Java程序的操作,例如加载、存储、运算等。
5. 反编译:字节码可以通过反编译工具(如`javap`)转换回Java源代码的形式,但这并不是原始源代码,因为编译过程中的某些信息可能已经丢失。
6. 优点:Java字节码的中间形式提供了一种折中的解决方案,既能够保持跨平台性,又能够在运行时进行优化,提高执行效率。
在Java开发中,理解字节码有助于深入了解Java虚拟机的工作原理,进行性能优化和调试。
1.1 字节码的生成
Java字节码的生成是由Java编译器(javac)在将Java源代码编译成字节码文件时完成的。以下是Java字节码的生成过程:
1. 编写Java源代码:开发者编写Java程序的源代码,通常保存在以`.java`为扩展名的文件中。
2. 编译源代码: 使用Java编译器(javac)将Java源代码编译成字节码文件。
将生成一个或多个以`.class`为扩展名的字节码文件,其中每个文件对应一个类。
3. 生成字节码:字节码文件包含了Java源代码的中间形式,其中的指令集是针对Java虚拟机设计的。这些字节码指令执行各种操作,如加载、存储、运算等。
1.2 字节码的执行
Java字节码的执行是由Java虚拟机(JVM)负责的。当Java程序运行时,JVM会加载字节码文件并执行其中的指令,将程序逻辑转化为实际的运行行为。字节码的执行过程包括以下步骤:
1. 加载类文件:JVM首先加载字节码文件,这是通过类加载器完成的。类加载器负责查找并加载类的字节码,将其加载到内存中。
2. 字节码解释:JVM通过解释器逐行解释字节码文件中的指令。解释器负责将字节码翻译成对应的本地机器码,然后执行。
3. 即时编译(JIT Compilation):为了提高执行效率,JVM还可能使用即时编译器将字节码直接编译成本地机器码。即时编译器会将频繁执行的代码块优化为本地代码,以减少解释器的性能开销。
4. 运行时数据区:在执行过程中,JVM维护着运行时数据区,包括方法区、堆、栈、程序计数器等。这些区域存储了程序的状态和运行时数据。
5. 内存管理和垃圾回收:JVM负责管理内存,包括分配和释放内存空间。垃圾回收器会定期清理不再被引用的对象,释放内存。
6. 异常处理:在执行字节码的过程中,JVM会检测并处理程序中可能出现的异常情况。如果发生异常,JVM会找到相应的异常处理代码并执行。
7. 多线程支持:JVM提供对多线程的支持,能够同时执行多个线程,并确保线程间的协同工作。
总体而言,字节码的执行是Java程序运行的关键步骤,JVM通过解释和即时编译等技术来实现对字节码的高效执行。这种机制使得Java程序具有跨平台性和灵活性,同时通过运行时数据区的管理实现了对内存的有效控制。
2. 字节码工具
2.1 javap
`javap`是Java的反编译工具,用于反编译字节码文件,查看类的成员、方法、字段等信息。通过`javap`可以深入了解类的结构和字节码指令。
2.2 ASM库
ASM是一个强大的Java字节码操纵库,用于生成和转换Java字节码。它提供了灵活的API,可以直接操作字节码,用于在运行时生成、修改和转换类文件。
深入理解字节码有助于对Java程序的性能进行优化,并能更好地理解Java虚拟机的工作原理。字节码工具如`javap`和ASM库为开发人员提供了调试和优化的手段。
参考文章:https://blog.csdn.net/weixin_43122090/article/details/105093777